commit c7e3f36b0610adbbb795193d90fc852fcd8b7196 Author: zypherift Date: Sat Aug 9 18:17:34 2025 +0200 1.0.0 diff --git a/ .codecov.yml b/ .codecov.yml new file mode 100644 index 0000000..a38bf65 --- /dev/null +++ b/ .codecov.yml @@ -0,0 +1,21 @@ +comment: + require_changes: true + +coverage: + status: + project: + default: + target: auto + threshold: 1% + patch: + default: + target: 50% + threshold: 10% + precision: 1 + range: "80...100" + +# Ignore all the file inside the example and +# end eventually also the autogenerate file +ignore: + - '**/example/' + - '**/*.g.dart' \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..bce2830 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: [imaNNeo] +custom: ["https://www.buymeacoffee.com/fl_chart"] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..db93fbd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,24 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +** Don't make a duplicate issue. +You can search in issues to make sure there isn't any already opened issue with your concern. + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Provide us a completely reproducible code (contains the main function) in a `main.dart` file, it helps us to find the bug immediately. + +**Screenshots** +If applicable, add screenshots, or videoshots to help explain your problem. + +**Versions** + - which version of the Flutter are you using? + - which version of the FlChart are you using? diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..2c75f11 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,23 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +** Don't make a duplicate issue. +You can search in issues to make sure there isn't any already opened issue with your concern. + +**Is your feature request relasted to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..497b241 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,23 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + assignees: + - "dependabot" + commit-message: + prefix: "chore: " + - package-ecosystem: "pub" + directory: "/" + schedule: + interval: "daily" + assignees: + - "dependabot" + commit-message: + prefix: "chore: " diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml new file mode 100644 index 0000000..6ca751b --- /dev/null +++ b/.github/workflows/codecov.yml @@ -0,0 +1,24 @@ +name: Code Coverage + +on: [ push, pull_request ] + +jobs: + upload: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + - name: Get packages + run: flutter pub get + - name: Generate coverage file + run: flutter test --coverage + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + fail_ci_if_error: true + files: ./coverage/lcov.info + flags: flutter + diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml new file mode 100644 index 0000000..40797cd --- /dev/null +++ b/.github/workflows/gh-pages.yml @@ -0,0 +1,32 @@ +name: Gh-Pages + +on: + push: + branches: [ main ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Install Flutter + uses: subosito/flutter-action@v2 + - name: Set fl_chart Version + run: | + VERSION=$(grep "version:" pubspec.yaml | awk '{ print $2 }') + echo "USING_FL_CHART_VERSION=$VERSION" >> $GITHUB_ENV + - run: flutter config --enable-web + working-directory: example + - run: flutter build web --release --wasm --base-href=/ --dart-define="USING_FL_CHART_VERSION=${{ env.USING_FL_CHART_VERSION }}" + working-directory: example + - run: git config user.name github-actions + working-directory: example + - run: git config user.email github-actions@github.com + working-directory: example + - run: git --work-tree build/web add --all + working-directory: example + - run: git commit -m "Automatic deployment by github-actions" + working-directory: example + - run: git push origin HEAD:gh-pages --force + working-directory: example diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..6abe16e --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,18 @@ +name: Publish plugin + +on: + release: + types: [ published ] + +jobs: + publish: + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Publish + uses: k-paxian/dart-package-publisher@master + with: + credentialJson: ${{ secrets.CREDENTIAL_JSON }} \ No newline at end of file diff --git a/.github/workflows/verification.yml b/.github/workflows/verification.yml new file mode 100644 index 0000000..3f14d7a --- /dev/null +++ b/.github/workflows/verification.yml @@ -0,0 +1,24 @@ +name: Code Verification + +on: [ push, pull_request ] + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - name: Clone repository + uses: actions/checkout@v4 + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + - name: Print Flutter version + run: flutter --version + - name: Get packages + run: flutter pub get + - name: Check formatting + run: make checkFormat + - name: Analyze the source code + run: make analyze + - name: Run tests + run: make runTests diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2025cda --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +.DS_Store +.dart_tool/ +.idea/ +.packages +.pub/ + +build/ +ios/.generated/ +ios/Flutter/Generated.xcconfig +ios/Runner/GeneratedPluginRegistrant.* +pubspec.lock +.vscode/launch.json +example/android/.project +example/android/.settings/org.eclipse.buildship.core.prefs +example/android/app/.classpath +example/android/app/.project +example/android/app/.settings/org.eclipse.buildship.core.prefs +example/macos/Flutter/ephemeral/ +coverage/ +.fvm/ + +# Files generated by dart tools +.dart_tool \ No newline at end of file diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..7d1d1dc --- /dev/null +++ b/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 7a4c33425ddd78c54aba07d86f3f9a4a0051769b + channel: beta + +project_type: package diff --git a/.pubignore b/.pubignore new file mode 100644 index 0000000..7442a34 --- /dev/null +++ b/.pubignore @@ -0,0 +1,2 @@ +/docs/* +/repo_files/* \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..2198633 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "dart.lineLength": 80, + "editor.formatOnSave": true, + "editor.formatOnType": true, + "editor.suggest.snippetsPreventQuickSuggestions": false, + "editor.tabCompletion": "onlySnippets", + "editor.wordBasedSuggestions": "off", + "files.insertFinalNewline": true, + "editor.defaultFormatter": "Dart-Code.dart-code" +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1ad1101 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,649 @@ +## newVersion +* **FEATURE** (by @huanghui1998hhh) Add `gradientArea` property to `LineChartBarData` to allow you to control the scope of gradient effects, #1925 + +## 1.0.0 +Image + +* **FEATURE** (by @imaNNeo) Implement a new chart type called CandlestickChart. You can take a look at the documentation [here](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/candlestick_chart.md). And I just implemented a basic example to show the Bitcoin price in 2024, you can take a look at it in our sample app [here](https://app.flchart.dev/#/candlestick). #433, #1143 +Image + +* **BREAKING** (by @imaNNeo) Remove the deprecated `tooltipRoundedRadius` property -> you should use `tooltipBorderRadius` instead. +* **BUGFIX** (by @imaNNeo) Fix the BarChartData mismatch issue when changing the data, #1911 +* **FEATURE** (by @frybitsinc) Add fillGradient property in [RadarDataSet](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/radar_chart.md#radardataset) +* **BREAKING** (by @imaNNeo) Upgrade the min flutter version to `3.27.4`. So please make sure that your project is not using an old flutter version, #1846 +* **IMPORTANT** (by @imaNNeo) You can read more about this release and the history of fl_chart here in my [blog post](https://flutter4fun.com/fl-chart-1-0-0) + +## 0.71.0 +* **IMPROVEMENT** (by @MattiaPispisa) Add a new property called `BorderRadius tooltipBorderRadius` instead of (deprecated) `double tooltipRoundedRadius` in `BarTouchTooltipData`, `LineTouchTooltipData` and `ScatterTouchTooltipData` #1715 +* **FEATURE** (by @frybitsinc) Add `children` property in our [RadarChartTitle](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/radar_chart.md#radarcharttitle), #1840 +* **BUGFIX** (by @morvagergely) Fix the initial zoom issue in our scrollable LineChart, #1863 + +## 0.70.2 +* **FEATURE** (by @imaNNeo) Add error range feature in our axis-based charts. You can set `xError` and `yError` in the [FlSpot](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#flspot) or `toYErrorRange` in [BarChartRodData](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/bar_chart.md#barchartroddata). Also we have `errorIndicatorData` property in our [LineChartData](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/line_chart.md#linechartdata), [BarChartData](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/bar_chart.md#barchartdata) and [ScatterChartData](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/scatter_chart.md#scatterchartdata) that is responsible to render the error bars. You can take a look at the [LineChartSample 13](https://github.com/imaNNeo/fl_chart/blob/main/example/lib/presentation/samples/line/line_chart_sample13.dart) and [BarChartSample 8](https://github.com/imaNNeo/fl_chart/blob/main/example/lib/presentation/samples/bar/bar_chart_sample8.dart) in our [sample app](https://app.flchart.dev), #1483 + +## 0.70.1 +* **FEATURE** (by @Peetee06) Add `panEnabled` and `scaleEnabled` properties in the TransformationController, #1818 +* **FEATURE** (by @mitulagr2) Add `renderPriority` feature in our [ScatterSpot](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/scatter_chart.md#scatterspot), #1545 +* **FEATURE** (by @imaNNeo) Add `rotationQuarterTurns` property in our Axis-Based charts (such as [LineChart](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/line_chart.md), [BarChart](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/bar_chart.md) and [ScatterChart](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/scatter_chart.md)). It allows you to rotate the chart 90 degrees (clockwise) in each turn. For example you can have Horizontal Bar Charts by setting `rotationQuarterTurns` to 1 (which rotates the chart 90 degrees clockwise). It works exactly like [RotatesBox](https://api.flutter.dev/flutter/widgets/RotatedBox-class.html) widget, #113 +* **FEATURE** (by @soraef) Add `isMinValueAtCenter` property in the [RadarChart](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/radar_chart.md) to allow the user to set the minimum value at the center of the chart, #1351, #1442 + +## 0.70.0 +* **FEATURE** (by @Peetee06) Implemented a 5 years-old feature request about scroll and zoom support in our axis-based charts. Special thanks to @Peetee06 who made it happen, #71 +* **IMPROVEMENT** (by @Peetee06) Added functionality to control the transformation of axis-based charts using `FlTransformationConfig` class. You can now enable scaling and panning for `LineChart`, `BarChart` and `ScatterChart` using this class +* **IMPROVEMENT** (by @Peetee06) Added some new unit tests in `bar_chart_data_extensions_test.dart`, `gradient_extension_test.dart` and fixed a typo in `bar_chart_data.dart` +* **BREAKING** (by @Peetee06) Fixed the equatable functionality in our BarChart. We hope it will not affect anything in our chart, but because the behaviour is changed, we marked it as a breaking change. (read more [here](https://github.com/imaNNeo/fl_chart/pull/1789#discussion_r1858371718)) +* **BREAKING** (by @Peetee06) `BarChart` is not const anymore due to adding an assert to check if transformations are allowed depending on the `BarChartData.alignment` property (read more [here](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/migration_guides/0.70.0/MIGRATION_00_70_00.md)) +* **IMPROVEMENT** (by @Peetee06) Upgrade to the new Flutter version ([3.27.0](https://medium.com/flutter/whats-new-in-flutter-3-27-28341129570c)), #1804 +* **IMPROVEMENT** (by @AliAkberAakash) Minor typo fix in our line chart documentation, #1795 +* **IMPROVEMENT** (by @imaNNeo) Fixed the code coverage API rate-limit issue +* **Improvement** (by @imaNNeo) Published the example app in Google Play and App Store. Other stores (such as [snap store](https://snapcraft.io/store) and [Microsoft Store](https://apps.microsoft.com/home)) will come next. You can download the Android version here in [Google Play](https://play.google.com/store/apps/details?id=dev.flchart.app) and the iOS version here in [App Store](https://apps.apple.com/us/app/fl-chart/id6476523019) + +## 0.69.2 +* **IMPROVEMENT** (by @imaNNeo) Fix the analyzer warnings (to have maximum score in the [pub.dev](https://pub.dev/packages/fl_chart/score)) + +## 0.69.1 +* **IMPROVEMENT** (by @moshe5745) Update the docs related to line chart's `duration` and `curve` properties, #1618 +* **IMPROVEMENT** (by @imaNNeo) Deprecate `swapAnimationDuration` and `swapAnimationCurve` properties to use `curve` and `duration` instead to keep the consistency over the project, #1618 +* **BUGFIX** (by @aimawari) Fixed lots of issues related to the zero value in the PieChartSectionData, #697, #817 and #1632 + +## 0.69.0 +* **BUGFIX** (by @imaNNeo) Fix a memory leak issue in the axis-based charts, there was a logic to calculate and cache the minX, maxX, minY and maxY properties to reduce the computation cost. But it caused some memory issues, as we don't have a quick solution for this, we disabled the caching logic for now, later we can move the calculation logic to the render objects to keep and update them only when the data is changed, #1106, #1693 +* **BUGFIX** (by @imaNNeo) Fix showing grid lines even when there is no line to show in the LineChart, #1691 +* **IMPROVEMENT** (by @sczesla) Allow users to control minIncluded and maxIncluded using SideTitles, #906 +* **IMPROVEMENT** (by @elizabethzhenliu) Reverse the touch order in ScatterChart, so now the top spots are touched first, #1675 +* **IMPROVEMENT** (by @ksw2000) Remove redundant math import, #1683 +* **IMPROVEMENT** (by @Neer-Pathak) Fix linux example build issue, #1668 +* **IMPROVEMENT** (by @TobiasRump) Update the bar chart documentation, #1662 + +## 0.68.0 +* **Improvement** (by @imaNNeo) Update LineChartSample6 to implement a way to show a tooltip on a single spot, #1620 +* **Feature** (by @herna) Add `titleSunbeamLayout` inside the [BarChartData](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/bar_chart.md#barchartdata) to allow the user to customize the layout of the title sunbeam +* **Improvement** (by @imaNNeo) Add LineChart and BarChart explanation videos on top of the respective documentation pages ([LineChart video](https://youtu.be/F3wTxTdAFaU?si=8lwlypKjt-0aJJK0), [BarChart video](https://youtu.be/vYe0RY1nCAA?si=30q_7eNn9MDLcph4)) + +## 0.67.0 +* **FEATURE** (by @julien4215) Add direction property to the [HorizontalLineLabel](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#horizontallinelabel) and [VerticalLineLabel](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#verticallinelabel), #1574 +* **FEATURE** (by @apekshamehta) Added new method called getTooltipColor for axis charts (bar,line,scatter) to change background color of tooltip dynamically, #1279. +* **BREAKING** (by @apekshamehta) Removed tooltipBgColor property from Bar, Line and Scatter Charts (you can now use `getTooltipColor` which provides more customizability), checkout the [full migration guide here](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/migration_guides/0.67.0/MIGRATION_00_67_00.md). +```dart +/// Migration guide: +/// This is the old way: +BarChartData( + barTouchData: BarTouchData( + touchTooltipData: BarTouchTooltipData( + tooltipBgColor: Colors.blueGrey, + ) + ) +) + +/// This is the new way: +BarChartData( + barTouchData: BarTouchData( + touchTooltipData: BarTouchTooltipData( + getTooltipColor: (BarChartGroupData group) => Colors.blueGrey, + ) + ) +) +``` + +## 0.66.2 +* **BUGFIX** (by @stwarwas) Remove dart.io to fix web platform issue, #1577 + +## 0.66.1 +* **BUGFIX** (by @imaNNeo) Fix PieChart blackout issue, #1538 +* **BUGFIX** (by @imaNNeo) Fix memory leak in LineChart and BarChart, #1106 + +## 0.66.0 +* **IMPROVEMENT** (by @imaNNeo) Add Flutter sdk constraints to the pubspec.yaml to force the user/developer to upgrade the Flutter version to 3.16.0 (latest), #1509 +* **IMPROVEMENT** (by @imaNNeo) Add `dotPainter` property to ScatterSpot to allow customizing the dot painter, #568 +* **BREAKING** (by @imaNNeo) Remove `color` and `radius` properties from ScatterSpot (use `dotPainter` instead), #568 +* **BREAKING** (by @imaNNeo) Change the default value of FlDotCirclePainter.`strokeWidth` to 0.0 +```dart +/// Migration guide: +/// This is the old way: +ScatterSpot( + 2, + 5, + color: Colors.red, + radius: 12, +) + +/// This is the new way: +ScatterSpot( + 2, + 8, + dotPainter: FlDotCirclePainter( + color: Colors.red, + radius: 22, + ), +), +``` +* **BUGFIX** (by @imaNNeo) Fix barChart tooltip for values below or above the 0 point, #1462 +* **BUGFIX** (by @imaNNeo) Fix pieChart drawing single section on iPhone, #1515 +* **IMPROVEMENT** (by @imaNNeo) Add gradient property to the [HorizontalLine](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#horizontalline) and [VerticalLine](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#verticalline), #1525 +* **FEATURE** (by @raldhafiri) Add gradient property to the [PieChartSectionData](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/pie_chart.md#piechartsectiondata), #1511 +* **IMPROVEMENT** (by @imaNNeo) Rename default branch `master` to `main` +* **IMPROVEMENT** (by @imaNNeo) Update flutter sdk constraints to remove the upper bound limit (Read more [here](https://dart.dev/go/flutter-upper-bound-deprecation)). + +## 0.65.0 +* **FEATURE** (by @Dartek12) Added gradient to [FlLine](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#FlLine), #1197 +* **BUGFIX** (by @imaNNeo) Fix bar line shadow crash when we have only one (or zero) spot, #1466 +* **BUGFIX** (by @imaNNeo) Fix having negative `toY` (or positive `fromY`) in BarChart's `minY` and `maxY` calculations, #1470 +* **BUGFIX** (by @bobatsar) Fix bars drawn outside of diagram +* **FEATURE** (by @k0psutin) Add dashed border to BarChartRodData, #1144 +* **FEATURE** (by @imaNNeo) Allow to show single point line in LineChart, #1438 + +## 0.64.0 +* **BUGFIX** (by @Anas35) Fix Tooltip not displaying when value from BackgroundBarChartRodData is less than zero. #1345. +* **BUGFIX** (by @imaNNeo) Fix Negative BarChartRodStackItem are not drawn correctly bug, #1347 +* **BUGFIX** (by @imaNNeo) Fix bar_chart_helper minY calculation bug, #1388 +* **IMPROVEMENT** (by @imaNNeo) Consider fraction digits when formatting chart side titles, #1267 + +## 0.63.0 +* **BUGFIX** (by @imaNNeo) Fix PieChart crash on web-renderer html by ignoring `sectionsSpace` when `Path.combine()` does not work (it's flutter engine [issue](https://github.com/flutter/flutter/issues/44572)), #955 +* **BUGFIX** (by @imaNNeo) Fix ScatterChart long-press interaction bug (disappears when long-pressing on the chart), #1318 +* **FEATURE** (by @imaNNeo) Upgrade dart version to [3.0](https://dart.dev/resources/dart-3-migration) + +## 0.62.0 +* **BUGFIX** (by @JoshMart) Fix extra lines not painting when at chart min or max, #1255. +* **BUGFIX** (by @imaNNeo) Check if mounted before calling setState in _handleBuiltInTouch methods in bar, line and scatter charts, #1101 +* **FEATURE** (by @MagdyYacoub1): Added gradient color to [RangeAnnotations](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#rangeannotations) by adding gradient attribute to [horizontalRangeAnnotations](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#horizontalrangeannotation) and [VerticalRangeAnnotation](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#verticalrangeannotation), #1195. +* **BUGFIX** (by @Motionz-Von)Fix windows build for example app +* **FEATURE** (by @Motionz-Von)BarChart groupSpace also takes effect when alignment is BarChartAlignment.end or BarChartAlignment.start. +* **FEATURE** (by @Motionz-Von) supports setting line StrokeCap on HorizontalLine/VerticalLine +* **BUGFIX** (by @nav-28) Fix radar chart tick and graph point not matching #1078 +* **IMPROVEMENT** (by @imaNNeo) Update LineChartSample5 to demonstrate click to toggle show/hide tooltip, #118 + +## 0.61.0 +* **IMPROVEMENT** (by @imaNNeo) Remove assertion to check to provide only one of `color` or `gradient` property in the [BarChartRodData](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/bar_chart.md#barchartroddata) and [BackgroundBarChartRodData](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/bar_chart.md#backgroundbarchartroddata), #1121. +* **IMPROVEMENT** (by @imaNNeo) Make `drawBehindEverything` property default to `true` in [AxisTitles](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#axistitle) class, #1097. +* **BUGFIX** (by @imaNNeo) Show `0` instead of `-0` in some edge-cases in the default titles +* **FEATURE** (by @tamasapps): Add `tooltipHorizontalAlignment` and `tooltipHorizontalOffset` property in [LineTouchTooltipData], [BarTouchTooltipData], [ScatterTouchTooltipData]. +* **FEATURE** (by @dhiyaaulauliyaa) Add ability to force SideTitle to be placed inside its corresponding axis bounding box, #603. + +## 0.60.0 +* **IMPROVEMENT** (by @lsaudon) Replace flutter_lints by very_good_analysis +* **BREAKING** (by @lsaudon) Update dart sdk to 2.17.0 (flutter 3.0.0) +* **BUGFIX** (by @imaNNeo) Fix indicator out of range error in line chart, #1187 +* **FEATURE** (by @HTsuruo): Add `longPressDuration` optional property that allows to control the duration LongPress gesture occurs, #1114 #1127. +* **IMPROVEMENT** (by @imaNNeo) Add some screenshots in `pubspec.yaml` to support new [pub.dev](pub.dev) feature. Read more about it [here](https://dart.dev/tools/pub/pubspec#screenshots) and [here](https://medium.com/dartlang/screenshots-and-automated-publishing-for-pub-dev-9bceb19edf79). +* **IMPROVEMENT** (by @imaNNeo) Update the homepage url in `pubspec.yaml` (I just renamed my username) +* **FEATURE** (by @JoshMart) Add ability to draw extra horizontal lines on BarChart, #476 +* **FEATURE** (by @soraef) Add a `positionPercentageOffset` optional property to RadarChartTitle to allow individual title positioning +* **BUGFIX** (by @imaNNeo) Allow to draw empty radarChart (with all zero values), #1217 +* **IMPORTANT** **IRAN NEEDS YOU. SPREAD THE NEWS.** + + +## 0.55.2 +* **BUGFIX** (by @imaNNeo): Fix inner border of pieChart with single section, #1089 +* **IMPORTANT** **IRAN NEEDS HELP** + + + +As you might know, Islamic Republic of Iran is murdering people in silence right now in Iran +They shut the Internet down to do that. That’s why I cannot maintain this library for a while. +Now we need your help, please be our voice by spreading news in your media to support us +Search these hashtags: + +[#MahsaAmini](https://twitter.com/search?q=%23MahsaAmini&src=typeahead_click) +[مهسا_امینی](https://twitter.com/search?q=%23%D9%85%D9%87%D8%B3%D8%A7_%D8%A7%D9%85%DB%8C%D9%86%DB%8C&src=typeahead_click&f=top) +[OpIran](https://twitter.com/search?q=%23OpIran&src=typeahead_click&f=top) + +Also, [this article](https://www.bbc.com/news/world-middle-east-62984076) might help. + + +## 0.55.1 +* **BUGFIX** (by @ateich): Fix infinite loop in RadarChart when all values in RadarDataSet are equal, #882. +* **BUGFIX** (by @ateich): Fix uneven titles in RadarChart when using titlePositionPercentageOffset, #1074. +* **BUGFIX** (by @imaNNeo): Fix PieChart single section stroke issue, #1089 + +## 0.55.0 +* **FEATURE** (by @emelinepal): Add `tooltipBorder` property in [LineTouchTooltipData], [BarTouchTooltipData], [ScatterTouchTooltipData], #692. +* **BUGFIX** (by @imaNNeo): Fix tooltip issue on negative bar charts, #978. +* **IMPROVEMENT** (by @imaNNeo): Use Container to draw axis-based charts border. +* **FEATURE** (by @FlorianArnould) Add the ability to select the RadarChart shape (circle or polygon), #1047. +* **BUGFIX** (by @imaNNeo): Fix LineChart titles problem with single FlSpot, #1053. +* **FEATURE** (by @FlorianArnould) Add the ability to rotate the RadarChar titles, #883. +* **BREAKING** (by @FlorianArnould) [RadarChartData.getTitle](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/radar_chart.md#RadarChartData) have a new parameter `angle` and now returns a [RadarChartTitle](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/radar_chart.md#RadarChartTitle) instead of a simple `string`. (Read our [Migration Guide](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/migration_guides/0.55.0/MIGRATION_00_55_00.md) to learn more about it) + +## 0.51.0 +* **FEATURE** (by @imaNNeo): Add `SideTitleWidget` to help you use it in [SideTitles.getTitlesWidget]. It's a wrapper around your widget. It keeps your provided `child` widget close to the chart. It has `angle` and `space` properties to handle margin and rotation. There is a `axisSide` property that you should fill, it has provided to you in the MetaData object. Check the below sample: +```dart +getTitlesWidget: (double value, TitleMeta meta) { + return SideTitleWidget( + axisSide: meta.axisSide, + space: 8.0, + angle: 0.0, + child: const Text("This is your widget"), + ); +}, +``` +* **IMPROVEMENT** (by @imaNNeo): Fix default LineChart interval issue on small view sizes, #909. + +## 0.50.6 +* **IMPROVEMENT** Fix a backward compatibility issue with Flutter 3.0, #1016 + +## 0.50.5 +* **IMPROVEMENT** Fix test coverage problem again :/ + +## 0.50.4 +* **IMPROVEMENT** Fix test coverage problem + +## 0.50.3 +* **IMPROVEMENT** Fix order of drawing lineChart bar indicator problem, #198. +* **FEATURE** Add `isStrokeJoinRound` property in [LineChartBarData](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/line_chart.md#linechartbardata). +* **IMPROVEMENT** Upgrade to Flutter 3, #997. +* **FEATURE** Add `chartRendererKey` property to the [LineChart](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/line_chart.md), [BarChart](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/bar_chart.md), and [ScatterChart](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/scatter_chart.md). We pass it directly to our chart renderers that are responsible to render the chart itself (without anything around it like titles), #987. + +## 0.50.1 +* **BUGFIX** Allow to show axisTitle without sideTitles, #963 + +## 0.50.0 +**This release has some breaking changes. So please check out the migration guide [here](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/migration_guides/0.50.0/MIGRATION_00_50_00.md)** +* **IMPROVEMENT** Allow to return a Widget in [SideTitles.getTitlesWidget](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#sidetitles) instead of a `String`. For example, you can pass an [Icon](https://api.flutter.dev/flutter/widgets/Icon-class.html) widget as a title, #183. Check below samples: +> **LineChartSample 8** ([Source Code](https://github.com/imaNNeo/fl_chart/blob/main/example/lib/presentation/samples/line/line_chart_sample8.dart)) +> +> +> **BarChartSample 7** ([Source Code](https://github.com/imaNNeo/fl_chart/blob/main/example/lib/presentation/samples/bar/bar_chart_sample7.dart)) +> +> +* **BREAKING** Structure of `FlTitlesData`, `AxisTitles`, and `SideTitles` are changed. Because we are using a new system which allows you to pass any [Flutter Widget](https://docs.flutter.dev/development/ui/widgets) as a title instead of passing `string`, `textStyle`, `textAlign`, `rotation`, ... (Read our [Migration Guide](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/migration_guides/0.50.0/MIGRATION_00_50_00.md)) +* **FEATURE** Now we can use any [Gradient](https://api.flutter.dev/flutter/dart-ui/Gradient-class.html) such as [LinearGradient](https://api.flutter.dev/flutter/painting/LinearGradient-class.html) and [RadialGradient](https://api.flutter.dev/flutter/painting/RadialGradient-class.html) everywhere we have gradient. +* **BUGFIX** Fix BarChart rods gradient problem, #703. +* **BREAKING** `colors` property renamed to `color` to keep only one solid color. And now we have a `gradient` field instead of `colorStops`, `gradientFrom` and `gradientTo` in following classes: [BarChartRodData](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/bar_chart.md#barchartroddata), [BackgroundBarChartRodData](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/bar_chart.md#backgroundbarchartroddata), [BarAreaData](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/line_chart.md#BarAreaData), [BetweenBarsData](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/line_chart.md#betweenbarsdata), [LineChartBarData](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/line_chart.md#linechartbardata). (Read our [Migration Guide](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/migration_guides/0.50.0/MIGRATION_00_50_00.md) to learn more about it) + +## 0.46.0 +* **BUGFIX** Fix drawing BetweenBarsArea problem when there are `nullSpots` in fromLine and toLine, #912. +* **FEATURE** Allow to have vertically grouped BarChart using `fromY` and `toY` properties in [BarChartRodData](https://github.com/imaNNeo/fl_chart/blob/feature/multi-rods-on-bar-chart/repo_files/documentations/bar_chart.md#BarChartRodData) It means you can have a negative and a positive bar chart at the same X location. #334, #875. Check [BarChartSample5](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/bar_chart.md#sample-5-source-code) and [BarChartSample6](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/bar_chart.md#sample-6-source-code. +* **BREAKING** Renamed `y` property to `toY` in [BarChartRodData](https://github.com/imaNNeo/fl_chart/blob/feature/multi-rods-on-bar-chart/repo_files/documentations/bar_chart.md#BarChartRodData) and [BackgroundBarChartRodData](https://github.com/imaNNeo/fl_chart/blob/feature/multi-rods-on-bar-chart/repo_files/documentations/bar_chart.md#backgroundbarchartroddata) due to the above feature. +* **BUGFIX** Fix smaller radius bubble hiding behind bigger radius bubble in ScatterChart, #930. +* **BUGFIX** Fix tooltip text alignment and direction in line chart, #927. + +## 0.45.1 +* **IMPORTANT** **Fuck Vladimir Putin** +* **BUGFIX** Fix `FlSpot.nullSpot` at the first of list bug, #912. +* **FEATURE** Add `scatterLabelSettings` property in [ScatterChart](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/scatter_chart.md) which lets you to add titles on the spots, #902. + +## 0.45.0 +* **BUGFIX** Fix `clipData` implementation in ScatterChart and LineChart, #897. +* **BUGFIX** Fix PieChart changing sections issue (we have disabled semantics for pieChart badgeWidgets), #861. +* **BUGFIX** Fix LineChart width smaller width or height lower than 40, #869, #857. +* **BUGFIX** Allow to show title when axis diff is zero, #842, #879. +* **IMPROVEMENT** Improve iteration over axis values logic (it solves some minor problems on showing titles when min, max values are below than 1.0). +* **IMPROVEMENT** Add `baselineX` and `baselineY` property in our axis-based charts, It fixes a problem about `interval` which mentioned in #893 (check [this sample](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/line_chart.md#gist---baselinex-baseliney-sample-source-code). +* **IMPROVEMENT** Added `distanceCalculator` to `LineTouchData` which is used to calculate the distance between spots and touch events, #716, #261, #892 +* **BREAKING** `LineTouchResponse` response now contains a list of `TouchLineBarSpot` instead of `LineBarSpot`. They are ordered based on their distance to the touch event and also contain that distance. + +## 0.41.0 +* **BUGFIX** Fix getNearestTouchedSpot. Previously it returned the first occurrence of a spot within the threshold, and not the nearest, #641, #645. +* **FEATURE** Add `textAlign` property in the [SideTitles](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#sidetitles), #784. +* **IMPROVEMENT** Write some unit-tests and enable code coverage reports in our CI + +## 0.40.6 +* **IMPROVEMENT** Fix showing zero value in side titles and grid lines when we add negative value. Now we always go through the zero value in each axis, #739. +* **BUGFIX** Fix example app unsupported operation problem on web, #844. + +## 0.40.5 +* **BUGFIX** Fix BarChart empty groups state error, #797. +* **BUGFIX** Fix drawTooltipOnTop direction minor bug, #815. +* **BUGFIX** Fix section with zero value problem in PieChart (disabled animation on changing value to zero and from zero), #817 +* **BUGFIX** Fix pie chart stroke problem when adding space between sections (using new approach), #818. +* **IMPROVEMENT** Fix interval below one, #811 + +## 0.40.2 +* **IMPROVEMENT** Use 80 characters for code format line-length instead of 100 (because pub.dev works with 80 and decreased our score). + +## 0.40.1 +* **IMPROVEMENT** Fix pub.dev determining web support, #780. +* **IMPROVEMENT** Implement flutter_lints in the code. +* **BUGFIX** Fix below/above area data transparency issue, #770. + +## 0.40.0 +* **BUGFIX** Fixed pieChart `centerRadius = double.infinity` problem, #747.c +* **BREAKING** Charts touchCallback signature has changed to `(FlTouchEvent event, BaseTouchResponse? response)` which [FlTouchEvent](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#fltouchevent) determines which touch/pointer event happened (such as `FlTapUpEvent`, `FlPanUpdateEvent`, ...), and BaseTouchResponse gives us the chart response. +* **BREAKING** Chart touchResponse classes don't have `touchInput` and `clickHappened` properties anymore. Use [FlTouchEvent](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#fltouchevent) provided in the callback instead of `touchInput`. Check `event is FlTapUpEvent` to detect touch events instead of checking `clickHappened`; +* **IMPROVEMENT** Again we support `longPress` touch events. check [FlTouchEvent](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#fltouchevent) to see all kind of supported touch/pointer events (which can be `FlLongPressStart`, `FlLongPressMoveUpdate`, `FlLongPressEnd`, ...). Also you can check out [touch handling doc](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/handle_touches.md), #649. +* **IMPROVEMENT** Added `mouseCursorResolver` callback in touchData classes such as [LineTouchData](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/line_chart.md#linetouchdata-read-about-touch-handling) and [BarTouchData](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/bar_chart.md#bartouchdata-read-about-touch-handling). You can change the [MouseCursor](https://api.flutter.dev/flutter/services/MouseCursor-class.html) based on the provided [FlTouchEvent](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#fltouchevent) and touchResponse using this callback. (We have used this feature in [PieChartSample2](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/scatter_chart.md#sample-2-source-code)) +* **BUGFIX** Fixed `ScatterChart` default touchHandling crash +* **BUGFIX** Fix text styles when updating the theme. Check this [theme-aware-sample](https://gist.github.com/imaNNeo/bf95e720621d799ab980a7a3287c56e2). +* **IMPROVEMENT** Show narrow horizontal and vertical grid lines by default. +* **IMPROVEMENT** Show all left, top (except BarChart), right, bottom titles in Axis based charts by default. +* **IMPROVEMENT** Set `BarChartAlignment.spaceEvenly` as `alignment` property of [BarChartData](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/bar_chart.md#barchartdata) by default +* **IMPROVEMENT** Allow [BarChart](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/bar_chart.md) and [LineChart](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md) have empty values instead of throwing exception (we don't show anything if there is nothing provided) +* **BREAKING** `textStyle` of [ScatterTooltipItem](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/scatter_chart.md#ScatterTooltipItem) is now nullable and optional. `bottomMargin` is also optional (default is zero). So both are named parameters now. +* **IMPROVEMENT** We improved touch precision of `ScatterChart`. +* **BUGFIX** Fix overlapping last gridlines on border lines problem. +* **NEWS** Your donation **motivates** me to work more on the `fl_chart` and resolve more issues. Now you can [buy me a coffee](https://www.buymeacoffee.com/fl_chart)! + +## 0.36.4 +* **IMPROVEMENT** Added `borderSide` property in [BarChartRodData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/bar_chart.md#BarChartRodData) and [BarChartRodStackItem](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/bar_chart.md#BarChartRodStackItem) to draw strokes around each bar and rod stack items, #714. +* **IMPROVEMENT** Now all textStyles are nullable and theme-aware by default, #269. +* **BREAKING** All `getTextStyles` callback now give you a `context` and `value` (previously it was only a `value`). +* **BUGFIX** Fixed `colorStops` calculation which used in gradient colors, #732. + +## 0.36.3 +* **IMPROVEMENT** Show proper error message when there is less than 3 [RadarEntry](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/radar_chart.md#radarentry) in [RadarChart](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/radar_chart.md), #694. +* **IMPROVEMENT** Added `borderSide` property in [PieChartSectionData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/pie_chart.md#piechartsectiondata) to draw strokes around each section, #606. + +## 0.36.2 +* **IMPROVEMENT** Support `onMouseExit` event in all charts. +* **IMPROVEMENT** Add `rotateAngle` property in [LineTouchTooltipData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#linetouchtooltipdata), [BarTouchTooltipData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/bar_chart.md#bartouchtooltipdata), [ScatterTouchTooltipData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/scatter_chart.md#scattertouchtooltipdata), #260, #679. +* **BUGFIX** Fix PieChart section index problem, when there is a section with 0 value, #697. + + +## 0.36.1 +* **IMPROVEMENT** Allow to set zero value on PieChartSectionData (we remove zero sections instead of crashing), #640. +* **BUGFIX** Fix NPE crash in our renderers touchCallback, #651. +* **BUGFIX** Fix line index problem in LineChart, #665. (It has appeared in `0.36.0`, we had to revert 2nd change of `0.36.0`) +* **BREAKING** Remove unused `lineIndex` property from (ShowingTooltipIndicators)[https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#showingtooltipindicators]. + +## 0.36.0 +* **BUGFIX** Fixed bug of lerping FlSpot.nullSpot, #487. +* **BUGFIX** Fixed showing tooltip problem when animating chart, #647. +* **BUGFIX** Fixed RadarChart drawing problem, #627. +* **IMPROVEMENT** Now [SideTitles](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/base_chart.md#SideTitles).`interval` is working correctly in bottomTitles in the BarChart, #648. +* **BREAKING** You should provide `spotsIndices` instead of `showingSpots` in [ShowingTooltipIndicators](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#showingtooltipindicators). + +## 0.35.0 +* **IMPROVEMENT** Added `children` property in the [LineTooltipItem](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#linetooltipitem), [BarTooltipItem](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/bar_chart.md#bartooltipitem) and [ScatterTooltipItem](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/scatter_chart.md#scattertooltipitem) which accepts a list of [TextSpan](https://api.flutter.dev/flutter/painting/TextSpan-class.html). It allows you to have more customized texts inside the tooltip. See [BarChartSample1](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/bar_chart.md#sample-1-source-code) and [ScatterSample2](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/scatter_chart.md#sample-2-source-code), #72, #294. +* **IMPROVEMENT** Added `getTouchLineStart` and `getTouchLineEnd` in [LineTouchData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#linetouchdata-read-about-touch-handling) to give more customizability over showing the touch lines. see [SampleLineChart9](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#sample-8-source-code). +* **IMPROVEMENT** Enabled `sectionsSpace` in PieChart for the web. +* **IMPROVEMENT** Added [Makefile](https://makefiletutorial.com) commands which makes it comfortable for verifying your code before push (It is related to contributors, red more about it in [CONTRIBUTING.md](https://github.com/imaNNeoFighT/fl_chart/blob/main/CONTRIBUTING.md)). +* **IMPROVEMENT** Added `FlDotCrossPainter` which extends `FlDotPainter` to paint X marks on line chart spots. +* **IMPROVEMENT** Added `textDirection` property in [LineTooltipItem](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#linetooltipitem), [BarTooltipItem](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/bar_chart.md#bartooltipitem) and [ScatterTooltipItem](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/scatter_chart.md#scattertooltipitem). It allows you to support rtl languages in tooltips. +* **IMPROVEMENT** Added `textDirection` property in [SideTitles](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/base_chart.md#sidetitles) class, #531. It allows you to support rtl languages in side titles. +* **IMPROVEMENT** Added `textDirection` property in [AxisTitles](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/base_chart.md#AxisTitle) class. It allows you to support rtl languages in axis titles. +* **BUGFIX** Fixed some bugs on drawing PieChart (for example when we have only one section), #582, +* **BREAKING** Border of pieChart now is hide by default (you can show it using `borderData: FlBorderData(show: true)`. +* **BREAKING** You cannot set `0` value on [PieChartSectionData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/pie_chart.md#piechartsectiondata).value anymore, instead remove it from list. +* **BREAKING** Removed `fullHeightTouchLine` property from [LineTouchData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#linetouchdata-read-about-touch-handling). Now you can have a full line with following snippet: +```dart +LineTouchData( + ... + getTouchLineStart: (barData, index) => -double.infinity // default: from bottom, + getTouchLineEnd: (barData, index) => double.infinity //to top, + ... +) +``` + +## 0.30.0 +* [IMPROVEMENT] We now use [RenderObject](https://api.flutter.dev/flutter/rendering/RenderObject-class.html) as our default drawing system. It brings a lot of stability. Such as size handling, hitTest handling (touches), and It makes us possible to paint Widgets inside our chart (It might fix #383, #556, #582, #584, #591). +* [IMPROVEMENT] Added [Radar Chart Documentations](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/radar_chart.md) +* [IMPROVEMENT] Added `textAlign` property in the [BarTooltipItem](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/bar_chart.md#bartooltipitem), [LineTooltipItem](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#linetooltipitem), and [ScatterTooltipItem](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/scatter_chart.md#scattertooltipitem), default is `TextAlign.center`. +* [IMPROVEMENT] Added `direction` property in the [BarTouchTooltipData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/bar_chart.md#bartouchtooltipdata), and [LineTouchTooltipData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#linetouchtooltipdata) to specify the position of the tooltip (can be `auto`, `top`, `bottom`), default is `auto`. +* [IMPROVEMENT] Updated touch flow, we now use [hitTest](https://api.flutter.dev/flutter/rendering/RenderProxyBoxWithHitTestBehavior/hitTest.html) for handling touch and interactions. +* [IMPROVEMENT] Added 'clickHappened' property in all of our TouchResponses (such as [LineTouchResponse](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#LineTouchResponse), [BarTouchResponse](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/bar_chart.md#bartouchresponse), ...), #210. +* [IMPROVEMENT] Added `swapAnimationCurve` property to all chart widgets which handles the built-in animation [Curve](https://api.flutter.dev/flutter/animation/Curves-class.html), #436. +* [BREAKING] Some properties in [ScatterTouchResponse](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/scatter_chart.md#scattertouchresponse), and [PieTouchResponse](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/pie_chart.md#pietouchresponse) moved to a wrapper class, you need to access them through that wrapper class. +* [BREAKING] Renamed `tooltipBottomMargin` to `tooltipMargin` property in the [BarTouchTooltipData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/bar_chart.md#bartouchtooltipdata), and [LineTouchTooltipData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#linetouchtooltipdata) +* [Bugfix] Fixed `double.infinity` in [PieChartData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/pie_chart.md#piechartdata) .centerSpaceRadius, #584. + +## 0.20.1 +* [BREAKING] We now support flutter version 2.0 (null-safety), check out the [migration guide](https://dart.dev/null-safety/migration-guide). +* [NEW_CHART] We have added [RadarChart](https://github.com/payam-zahedi/fl_chart/blob/main/repo_files/documentations/radar_chart.md). Thanks to [Payam Zahedi](https://github.com/payam-zahedi)! + +## 0.20.0-nullsafety1 +* [BREAKING] **We have migrated our project to null-safety. You may need to change your source-code to compile**. check [migration guide](https://dart.dev/null-safety/migration-guide). +* [BREAKING] You cannot set null value on FlSpot any more (use FlSpot.nullSpot instead). + +## 0.12.3 +* [Bugfix] Fixed PieChart exception bug on sections tap, #514. +* [Bugfix] Fixed PieChart badges problem, #538. +* [Bugfix] Fixed Bug of drawing lines with strokeWidth zero, #558. +* [Improvement] Updated example app to support web. +* [Improvement] Show tooltips on mouse hover on Web, and Desktop. + +## 0.12.2 +* [Bugfix] Fixed PieChart badges draw in first frame problem, #513. +* [Improvement] Use CanvasWrapper to proxy draw functions (It does not have any effect on the result, it makes the code testable) + +## 0.12.1 +* [Bugfix] Fixed PieChart badges bug with re-implementing the solution, #507 +* [Bugfix] Fix the setState issue using PieChart in the ListView, #467 +* [Bugfix] Fixed formatNumber bug for negative numbers, #486. +* [Improvement] Added applyCutOffY property in [BarAreaSpotsLine](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#barareaspotsline) to inherit cutOffY property of its parent, #478. + +## 0.12.0 +* [Improvement] [BREAKING] Replaced `color` property with `colors` in [BarChartRodData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/bar_chart.md#barchartroddata), and [BackgroundBarChartRodData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/bar_chart.md#backgroundbarchartroddata) to support gradient in BarChart, instead of solid color, #166. Check [BarChartSample3](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/bar_chart.md#sample-3-source-code) +* [Improvement] Improved gradient stops calculating algorithm. +* [Improvement] [BREAKING] Changed SideTitle's `textStyle` property to `getTextStyles` getter (it gives you the axis value, and you must return a TextStyle based on it), It helps you to have a different style for specific text, #439. Check it here [LineChartSample3](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#sample-3-source-code) +* [Improvement] Added `badgeWidget`, and `badgePositionPercentageOffset` in each [PieChartSectionData](https://github.com/imaNNeoFighT/fl_chart/blob/dev/repo_files/documentations/pie_chart.md#piechartsectiondata) to provide a widget to show in the chart, see [this sample](https://github.com/imaNNeoFighT/fl_chart/blob/dev/repo_files/documentations/pie_chart.md#sample-3-source-code), #443. Providing a widget is an important step in our library, if it works perfectly, we will aplly this solution on other parts. Then I appreciate any feedback. +* [Bugfix] Fixed aboveBarArea flickers after setState, #440. + +## 0.11.1 +* [Bugfix] Fixed drawing BarChart rods with providing minY (for positive), maxY (for negative) values bug, #404. +* [Bugfix] Fixed example app build fail error, by upgrading flutter_svg package to `0.18.1` + +## 0.11.0 +* [Bugfix] Prevent show ScatterSpot if show is false, #385. +* [Improvement] Set default centerSpaceRadius to double.infinity in [PieChartData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/pie_chart.md#piechartdata), #384. +* [Improvement] Allowed to have topTitles in the [BarChart](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/bar_chart.md), see [BarChartSample5](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/bar_chart.md#sample-5-source-code), #394. +* [Improvement] Added `touchedStackItem` and `touchedStackItemIndex` properties in the [BarTouchedSpot](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/bar_chart.md#bartouchedspot) to determine in which [BarChartRodStackItem](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/bar_chart.md#barchartrodstackitem) click happened, #393. +* [Improvement] [BREAKING] Renamed `rodStackItem` to `rodStackItems` in [BarChartRodData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/bar_chart.md#barchartroddata). + +## 0.10.1 +* [Improvement] Show barGroups `x` value instead of `index` in bottom titles, #342. +* [Improvement] [BREAKING] Use `double.infinity` instead of `double.nan` for letting `enterSpaceRadius` be as large as possible in the (PieChartData)[https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/pie_chart.md#piechartdata], #377. +* [Bugfix] Fixed PieChart bug with 1 section, #368. + +## 0.10.0 +* [IMPORTANT] **BLACK LIVES MATTER** +* [Improvement] Auto calculate interval in [SideTitles](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/base_chart.md#sidetitles) and [FlGridData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/base_chart.md#flgriddata), instead of hard coding 1, to prevent some performance issues like #101, #322. see [BarChartSample4](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/bar_chart.md#sample-4-source-code). +* [Bugfix] drawing dot on null spots +* [Bugfix] Fixed LineChart have multiple NULL spot bug. +* [Feature] Added `checkToShowTitle` property to the [SideTitles](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/base_chart.md#sidetitles), for checking show or not show titles in the provided value, #331. see [LineChartSample8](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#sample-8-source-code). +* [Feature] Added compatibily to have customized shapes for [FlDotData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#fldotdata), just override `FlDotData.etDotPainter` and pass your own painter or use built-in ones, see this [sample](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#sample-3-source-code). +* [Improvement] [BREAKING] Replaced `clipToBorder` with `clipData` in [LineChartData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#linechartdata) to support clipping 4 sides of a chart separately. + +## 0.9.4 +* [Bugfix] Fixed showing PieChart on web (we've ignored `groupSpace` on web, because some BlendModes are [not working](https://github.com/flutter/flutter/issues/56071) yet) + +## 0.9.3 +* [BugFix] Fixed groupBarsPosition exception, #313. +* [Improvement] Shadows default off, #316. + +## 0.9.2 +* [Feature] Added `shadow` property in [LineChartData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#linechartdata) to have shadow effect in our [LineChart](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md), take a look at [LineChartSampl5](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#sample-5-source-code), #304. +* [Feature] Added `isStepLineChart`, and `lineChartStepData` in the [LineChartData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#linechartdata) to support Step Line Chart, take a look at [lineChartSample3](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#sample-3-source-code), #303. +* [Improvement] Added `barData` parameter to checkToShowDot Function in the [FlDotData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#fldotdata). + +## 0.9.0 +* Added `strokeWidth`, `getStrokeColor`, `getDotColor` in the [FlDotData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#fldotdata), also removed `dotColor` from it (you should use `getDotColor` instead, it gives you more customizability), now we have more customizability on [FlDotData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#fldotdata), check [line_chart_sample3](https://github.com/imaNNeoFighT/fl_chart/blob/dev/repo_files/documentations/line_chart.md#sample-3-source-code), and [line_chart_sample5](https://github.com/imaNNeoFighT/fl_chart/blob/dev/repo_files/documentations/line_chart.md#sample-5-source-code), #233, #99, #274. +* Added `equatable` library to solve some equation issues. +* Implemented negative values feature for the BarChart, #106, #103. +* add Equatable for all models, it leads to have a better performance. +* Fixed a minor touch bug in the [BarChart](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/bar_chart.md). +* Fixed ScatterChart built-in touch behaviour. +* Fixed drawing grid lines bug, #280. +* Implemented [FlDotData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#fldotdata).`getDotColor` in a proper way, it returns a color based on the [LineChartBarData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#linechartbardata) color, #274, #282. +* Updated [LineChartData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#linechartdata).`showingTooltipIndicators` field type to list of [ShowingTooltipIndicators](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#showingtoltipindicators) to have a clean naming. + +## 0.8.7 +* Added `show` property in the `VerticalLineLabel` and set default to `false`, #256. +* Fixed bug, when the screen size is square, #258. + +## 0.8.6 +* Fixed exception on extraLinesData, #251. +* Show extra lines value with 1 floating-point. +* Implemented multi-section lines in LineChart, check this issue (#26) and this merge request (#252) + +## 0.8.5 +* Added `fitInsideHorizontally` and `fitInsideVertically` in [ScatterTouchTooltipData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/scatter_chart.md#scattertouchtooltipdata) +* Fixed `clipToBorder` functionality basdd on the border sides. + +## 0.8.4-test1 +* Improved documentations + +## 0.8.4 +* Added `preventCurveOvershootingThreshold` in `LineChartBarData` for applying prevent overshooting algorithm, #193. +* Fixed `clipToBorder` bug in the [LineChartData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#linechartdata), #228, #214. +* Removed unused `enableNormalTouch` property from all charts TouchData. +* Implemented ImageAnnotations feature (added `image`, and `sizedPicture` in the [VerticalLine](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#verticalline), and the [HorizontalLine](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#horizontalline), check [this sample](https://github.com/imaNNeoFighT/fl_chart/blob/dev/repo_files/documentations/line_chart.md#sample-8-source-code) for more information. +* Enable 'fitInsideTheChart' to support vertical tooltip overflow as well, #225. +* BREAKING CHANGE-> changed `fitInsideTheChart` to `fitInsideHorizontally` and added `fitInsideVertically` to support both sides, #225. + +## 0.8.3 +* prevent to set BorderRadius with numbers larger than (width / 2), fixed #200. +* added `fitInsideTheChart` property inside `BarTouchTooltipData` and `LineTouchTooltipData` to force tooltip draw inside the chart (shift it to the chart), fixed #159. + +## 0.8.2 +* added `fullHeightTouchLine` in [LineTouchData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#linetouchdata-read-about-touch-handling) to show a full height touch line, see sample in merge request #208. +* added `label` ([HorizontalLineLabel](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#horizontallinelabel)) inside [HorizontalLine](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#horizontalline) and [VerticalLine](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#verticalline) to show a lable text on the lines. + +## 0.8.1 +* yaaay, added some basic unit tests +* skipped the first and the last grid lines from drawing, #174. +* prevent to draw touchedSpotDot if `show` is false, #180. +* improved paint order, more details in #175. +* added possibility to set `double.nan` in `centerSpaceRadius` for the PieChart to let it to be calculated according to the view size, fixed #179. + +## 0.8.0 +* added functionallity to have dashed lines, in everywhere we draw line, there should be a property called `dashArray` (for example check [LineChartBarData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#linechartbardata), and see [LineChartSample8](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#sample-8-source-code)) +* BREAKING CHANGE: +* swapped [HorizontalExtraLines](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#horizontalline), and [VerticalExtraLines](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#verticalline) functionalities (now it has a well definition) +* and also removed `showVerticalLines`, and `showHorizontalLines` from [ExtraLinesData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#ExtraLinesData), if the `horizontalLines`, or `verticalLines` is empty we don't show them + +## 0.7.0 +* added rangeAnnotations in the [LineChartData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#linechartdata) to show range annotations, #163. +* removed `isRound` fiend in the [BarChartRodData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/bar_chart.md#barchartroddata) to add more customizability, and fixed #147 bug. +* fixed sever bug of click on pie chart, #146. + +## 0.6.3 +* Fixed drawing borddr bug, #143. +* Respect text scale factor when drawing text. + +## 0.6.2 +* added `axisTitleData` field to all axis base charts (Line, Bar, Scatter) to show the axes titles, see [LineChartSample4](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#sample-4-source-code) and [LineChartSample5](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#sample-5-source-code). + +## 0.6.1 +* added `betweenBarsData` property in [LineChartData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#linechartdata), fixed #93. + +## 0.6.0 +* fixed calculating size for handling touches bug, #126 +* added `rotateAngle` property to rotate the [SideTitles](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/base_chart.md#sidetitles), fixed issue [#75](https://github.com/imaNNeoFighT/fl_chart/issues/75) , see in this [sample](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/bar_chart.md#sample-5-source-code) +* BREAKING CHANGES: +* some property names updated in the [FlGridData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/base_chart.md#flgriddata): `drawHorizontalGrid` -> `drawHorizontalLine`, `getDrawingHorizontalGridLine` -> `getDrawingHorizontalLine`, `checkToShowHorizontalGrid` -> `checkToShowHorizontalLine` (and same for vertical properties), fixed issue [#92](https://github.com/imaNNeoFighT/fl_chart/issues/92) + +## 0.5.2 +* drawing titles using targetData instead of animating data, fixed issue #130. + +## 0.5.1 +* prevent to show touch indicators if barData.show is false in LineChart, [#125](https://github.com/imaNNeoFighT/fl_chart/issues/125). + +## 0.5.0 +* 💥 Added ScatterChart ([read about it](https://jbt.github.io/markdown-editor/repo_files/documentations/scatter_chart.md)) 💥 +* Added Velocity to in [FlPanEnd](https://github.com/imaNNeoFighT/fl_chart/blob/feature/scatter-chart/repo_files/documentations/base_chart.md#fltouchinput) to determine the Tap event. + +## 0.4.3 +* fixed a size bug, #100. +* direction support for gradient on the LineChart (added `gradientFrom` and `gradientTo` in the [LineChartBarData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#linechartbardata)). + +## 0.4.2 +* implemented stacked bar chart, check the [samples](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/bar_chart.md#sample-5-source-code) +* added `groupSpace in [BarChartData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/bar_chart.md#barchartdata) to apply space between bar groups +* fixed drawing left and right titles of the BarChart +* fixed showing gridLines bug (the grid line of exact max value of each direction doesn't show) + +## 0.4.1 +* fixed handling disabled `handleBuiltInTouches` state bug + +## 0.4.0 +* BIG BREAKING CHANGES +* There is no `FlChart` class anymore, instead use [LineChart](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md), [BarChart](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/bar_chart.md), and [PieChart](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/pie_chart.md) directly as a widget. +* Touch handling system is improved and for sure we have some changes, there is no `touchedResultSink` anymore and use `touchCallback` function which is added to each TouchData like ([LineTouchData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#linetouchdata-read-about-touch-handling)), [read more](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/handle_touches.md). +* `TouchTooltipData` class inside `LineTouchData` and `BarTouchData` renamed to `LineTouchTooltipData` and `BarTouchTooltipData` respectively, and also `TooltipItem` class renamed to `LineTooltipItem` and `BarTooltipItem`. +* `spots` inside `LineTouchResponse` renamed to `lineBarSpots` and type changed from `LineTouchedSpot` to `LineBarSpot`. +* `FlTouchNormapInput` renamed to `FlTouchNormalInput` (fixed typo) +* added `showingTooltipIndicators` in [LineChartData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#linechartdata) to show manually tooltips in `LineChart`. +* added `showingIndicators` in [LineChartBarData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#linechartbardata) to show manually indicators in `LineChart`. +* added `showingTooltipIndicators` in [BarChartGroupData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/bar_chart.md#barchartgroupdata) to show manually tooltips in `BarChart`. + + + +## 0.3.4 +* BREAKING CHANGES +* swapped horizontal and vertical semantics in [FlGridData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/base_chart.md#FlGridData), fixed this [issue](https://github.com/imaNNeoFighT/fl_chart/issues/85). + +## 0.3.3 +* BREAKING CHANGES +* added support for drawing below and above areas separately in LineChart +* added cutOffY feature in LineChart, see this [issue](https://github.com/imaNNeoFighT/fl_chart/issues/62) +* added `aboveBarData` in [LineChartBarData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#linechartbardata) +* `BelowBarData` class renamed to [BarAreaData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#barareadata) to reuse for both above and below areas +* `belowSpotsLine` renamed to `spotsLine` in [BarAreaData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#barareadata) +* `cutOffY` and `applyCutOffY` fields are added in [BarAreaData](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#barareadata) to handle cutting of drawing below or above area +* `BelowSpotsLine` renamed to [BarAreaSpotsLine](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#barareaspotsline), and inside it `checkToShowSpotBelowLine` renamed to `checkToShowSpotLine` + +## 0.3.2 +* provided default size (square with 30% smaller than screen) for the FLChart, fixed this [issue](https://github.com/imaNNeoFighT/fl_chart/issues/74). + +## 0.3.1 +* added `interval` field in [SideTitles](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/base_chart.md#sidetitles), fixed this [issue](https://github.com/imaNNeoFighT/fl_chart/issues/67) + +## 0.3.0 +* 💥 Added Animations 💥, [read about it](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/handle_animations.md). + +## 0.2.2 +* fixed a typo on CHANGELOG +* reformatted dart files with `flutter format` command + +## 0.2.1 +* fixed #64, added a technical debt :( + +## 0.2.0 +* fixed a critical got stuck in draw loop bug, +* set `BarChartGroupData` x as required property to keep consistency and prevent unpredictable bugs + +## 0.1.6 +* added `enableNormalTouch` property to chart's TouchData to handle normal taps, and enabled by default. + +## 0.1.5 +* reverted getPixelY() on axis_chart_painter to solve the regression bug (fixed issue #48) +* (fix) BelowBar considers its own color stops refs #46 + +## 0.1.4 +* bugfix -> fixed draw bug on BarChart when y value is very low in high scale y values (#43). + +## 0.1.3 +* added `SideTitles` class to hold titles representation data, and used in `FlTitlesData` to show left, top, right, bottom titles, instead of legacy direct parameters, and implemented a reversed chart [sample](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/line_chart.md#sample-6-source-code) using this update. + +## 0.1.2 +* added `preventCurveOverShooting` on BarData, check this [issue](https://github.com/imaNNeoFighT/fl_chart/issues/25) + +## 0.1.1 +* nothing important + +## 0.1.0 +* added **Touch Interactivity**, read more about it [here](https://github.com/imaNNeoFighT/fl_chart/blob/main/repo_files/documentations/handle_touches.md) + +## 0.0.8 +* added backgroundColor to axis based charts (LineChart, BarChart) to draw a solid background color behind the chart +* added getDrawingHorizontalGridLine, getDrawingVerticalGridLine on FlGridData to determine how(color, strokeWidth) the grid lines should be drawn with the given value on FlGridLine + +## 0.0.7 +* added ExtraLinesData in the LineChartData to draw extra horizontal and vertical lines on LineChart +* added BelowSpotsLine in the BlowBarData to draw lines from spot to the bottom of chart on LineChart + +## 0.0.6 +* fixed charts repainting bug, #16 + + +## 0.0.5 +* added clipToBorder to the LineChartData to clip the drawing to the border, #3 + + +## 0.0.4 +* fixed bug of adding bar with y = 0 on bar chart #13 + + +## 0.0.3 +* renamed `FlChartWidget` to `FlChart` (our main widget) and now you have to import `package:fl_chart/fl_chart.dart` instead of `package:fl_chart/fl_chart_widget.dart` +* renamed `FlChart*` to `BaseChart*` (parent class of our charts like `PieChart`) +* renamed `FlAxisChart*` to `AxisChart*` + + +## 0.0.2 +* fixed `minX`, `maxX` functionality on LineChart +* restricted to access private classes of the library + + +## 0.0.1 - Released on (2019 June 4) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..01bc1db --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,101 @@ +# Contributing +Hello, we are glad to have a contributor like you here. + +Don't forget that `open-source` makes no sense without contributors. No matter how big your changes are, it helps us a lot even it is a line of change. + +This file is intended to be a guide for those who are interested in contributing to the FL Chart. + +#### Below are the people who have contributed to the FL Chart. We hope we have your picture here soon. +[![](https://opencollective.com/fl_chart/contributors.svg?width=890&button=false)](https://github.com/imaNNeo/fl_chart/graphs/contributors) + +## Let's get Started + +Make sure you have Flutter installed and on your path (follow the [installation guide](https://docs.flutter.dev/get-started/install)). + +Follow these steps to clone FL Chart and set up the development environment: + +1. Fork the repository + +2. Clone the project, you can find it in your repositories: `git clone https://github.com/your-username/fl_chart.git` + +3. Go into the cloned directory: `cd fl_chart` + +4. Install all packages: `flutter packages get` + +5. Try to run the sample app. It should work on all platforms (Android, iOS, Web, Linux, MacOS, Windows) + +## Before Modifying the Code + +If the work you intend to do is non-trivial, it is necessary to open +an issue before starting to write your code. This helps us and the +community to discuss the issue and choose what is deemed to be the +best solution. + +### Mention the related issues: +If you are going to fix or improve something, please find and mention the related issues in commit message and Pull Request description. +In case you couldn't find any issue, it's better to create an issue to explain what's the issue that you are going to fix. + +## Let's start by our drawing architecture +We have a *_chart_painter.dart class per each chart type. It draws elements into the Canvas. +We made the CanvasWrapper class, because we wanted to test draw functions. +CanvasWrapper class holds a canvas and all draw functions proxies through it. +You should use it for drawing on the canvas, Instead of directly accessing the canvas. +It makes draw functions testable. + + + +(made with [draw.io](https://drive.google.com/file/d/1bj-2TqTRUh80dRKJk10drPNeA3fp3EA8/view)) + +## Keep your branch updated +While you are developing your branch, It is common that your branch gets outdated and you need to update your branch with the `master` branch. +To do that, please use `rebase` instead of `merge`. Because when you finish the PR, we must `rebase` your branch and merge it with the master. +The reason that we prefer `rebase` over `merge` is the simplicity of the commit history. It allows us to have sequential commits in the `master` +[This article](https://www.atlassian.com/git/tutorials/merging-vs-rebasing) might help you understand it better. + +## Checking Your Code's Quality + +After you have made your changes, you have to make sure your code works +correctly and meets our guidelines. Our guidelines are: + +You can simply run `make checkstyle`, and if you faced any formatting problem, run `make format`. + +##### Run `make checkstyle` to ensure that your code is formatted correctly +- It runs `flutter analyze` to verify that there are no warnings or errors. +- It runs `dart format --set-exit-if-changed --dry-run .` to verify that code has formatted correctly. + +#### Run `make format` to reformat the code +- It runs `dart format .` to format your code. + + +#### Run `make runTests` to ensure that all tests are passing. +- It runs `flutter test` under the hood. + +#### Run `make sure` before pushing your code. +- It runs both `make runTests` and then `make checkstyle` sequentially with a single command. + +## Test coverage (unit tests) +We should write unit tests for our written code. If you are not familiar with unit-tests, please start from [here](https://docs.flutter.dev/cookbook/testing/unit/introduction). + +[Mockito](https://pub.dev/packages/mockito) is the library that we use to mock our classes. Please read more about it in their docs [here](https://github.com/dart-lang/mockito#lets-create-mocks). + +Our code coverage is calculated by [Codecov](https://app.codecov.io/gh/imaNNeo/fl_chart) (Our coverage is [![codecov](https://codecov.io/gh/imaNNeo/fl_chart/branch/main/graph/badge.svg?token=XBhsIZBbZG)](https://codecov.io/gh/imaNNeo/fl_chart) + at the moment) + +When you push something in your PR (after approving your PR by one of us), you see a coverage report which describes how much coverage is increased or decreased by your code (You can check the details to see which part of your code made the change). + +Please make sure that your code is **not decreasing** the coverage. + +## Creating a Pull Request + +Congratulations! Your code meets all of our guidelines :100:. Now you have to +submit a pull request (PR for short) to us. These are the steps you should +follow when creating a PR: + +- Make a descriptive title that summarizes what changes were in the PR. + +- Mention the issues that you are fixing (if they don't exist, try to make one and explain the issue clearly) + +- Change your code according to feedback (if any). + +After you follow the above steps, your PR will hopefully be merged. Thanks for +contributing! diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..40327d8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Flutter 4 Fun + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d046ebe --- /dev/null +++ b/Makefile @@ -0,0 +1,44 @@ +ifeq ($(OS),Windows_NT) + FIND_CMD=dir /S /B lib\*.dart test\*.dart | findstr /V .mocks.dart +else + FIND_CMD=find lib test -name '*.dart' -not -name '*.mocks.dart' +endif + +analyze: + flutter analyze + +checkFormat: + dart format -o none --set-exit-if-changed $$( $(FIND_CMD) ) + +checkstyle: + make analyze && make checkFormat + +format: + dart format $$( $(FIND_CMD) ) + +runTests: + flutter test + +checkoutToPR: + git fetch origin pull/$(id)/head:pr-$(id) --force; \ + git checkout pr-$(id) + +# Tells you in which version this commit has landed +findVersion: + git describe --contains $(commit) | sed 's/~.*//' + +# Runs both `make runTests` and `make checkstyle`. Use this before pushing your code. +sure: + make runTests && make checkstyle + +# To create generated files (for example mock files in unit_tests) +codeGen: + dart run build_runner build --delete-conflicting-outputs + +showTestCoverage: + flutter test --coverage + genhtml coverage/lcov.info -o coverage/html + source ./scripts/makefile_scripts.sh && open_link "coverage/html/index.html" + +buildRunner: + flutter packages pub run build_runner build --delete-conflicting-outputs diff --git a/README.md b/README.md new file mode 100644 index 0000000..06741e2 --- /dev/null +++ b/README.md @@ -0,0 +1,120 @@ +![FL Chart Logo](https://github.com/imaNNeo/fl_chart/raw/main/repo_files/images/landing_logo.png) + +[![pub package](https://img.shields.io/pub/v/fl_chart.svg)](https://pub.dartlang.org/packages/fl_chart) +[![codecov](https://codecov.io/gh/imaNNeo/fl_chart/branch/main/graph/badge.svg?token=XBhsIZBbZG)](https://codecov.io/gh/imaNNeo/fl_chart) +Awesome Flutter +GitHub Repo stars +GitHub contributors +GitHub closed issues +![GitHub Sponsors](https://img.shields.io/github/sponsors/imaNNeo) + +Buy Me A Coffee donate button + + +### Our Financial Heroes +Your financial support acts as fuel for fl_chart's development. [Support here](https://github.com/sponsors/imaNNeo). + + + + + + + + + +
+ + + + + + + + + Become a sponsor + + + + + + + + + + +
+ +### Overview +FL Chart is a highly customizable Flutter chart library that supports **[Line Chart](https://app.flchart.dev/#/line)**, **[Bar Chart](https://app.flchart.dev/#/bar)**, **[Pie Chart](https://app.flchart.dev/#/pie)**, **[Scatter Chart](https://app.flchart.dev/#/scatter)**, and **[Radar Chart](https://app.flchart.dev/#/radar)**. + +

+ + +### Chart Types + +|LineChart |BarChart |PieChart | +|:------------:|:------------:|:-------------:| +| [![](https://github.com/imaNNeo/fl_chart/raw/main/repo_files/images/line_chart/line_chart_sample_1.gif)](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/line_chart.md#sample-1-source-code) [![](https://github.com/imaNNeo/fl_chart/raw/main/repo_files/images/line_chart/line_chart_sample_2.gif)](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/line_chart.md#sample-2-source-code) | [![](https://github.com/imaNNeo/fl_chart/raw/main/repo_files/images/bar_chart/bar_chart_sample_1.gif)](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/bar_chart.md#sample-1-source-code) [![](https://github.com/imaNNeo/fl_chart/raw/main/repo_files/images/bar_chart/bar_chart_sample_2.gif)](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/bar_chart.md#sample-2-source-code) | [![](https://github.com/imaNNeo/fl_chart/raw/main/repo_files/images/pie_chart/pie_chart_sample_1.gif)](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/pie_chart.md#sample-1-source-code) [![](https://github.com/imaNNeo/fl_chart/raw/main/repo_files/images/pie_chart/pie_chart_sample_2.gif)](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/pie_chart.md#sample-2-source-code) | +|[Read More](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/line_chart.md)|[Read More](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/bar_chart.md)|[Read More](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/pie_chart.md)| + +|ScatterChart |RadarChart| CandlestickChart| +|:------------:|:------------:|:-------------:| +| [![](https://github.com/imaNNeo/fl_chart/raw/main/repo_files/images/scatter_chart/scatter_chart_sample_1.gif)](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/scatter_chart.md#sample-1-source-code) [![](https://github.com/imaNNeo/fl_chart/raw/main/repo_files/images/scatter_chart/scatter_chart_sample_2.gif)](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/scatter_chart.md#sample-2-source-code) | ![](https://github.com/imaNNeo/fl_chart/raw/main/repo_files/images/radar_chart/radar_chart_sample_1.jpg) ![](https://github.com/imaNNeo/fl_chart/raw/main/repo_files/images/blank.png)|![](https://github.com/imaNNeo/fl_chart/raw/main/repo_files/images/candlestick_chart/candlestick_chart_sample_1.gif) ![](https://github.com/imaNNeo/fl_chart/raw/main/repo_files/images/blank.png)| +|[Read More](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/scatter_chart.md)|[Read More](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/radar_chart.md)|[Read More](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/candlestick_chart.md)| + +Banner designed by [Soheil Saffar](https://www.linkedin.com/in/soheilsaffar), and +samples inspired from +[David Kovalev](https://dribbble.com/shots/5560237-Live-Graphs-XD), +[Ricardo Salazar](https://dribbble.com/shots/1956890-Data-Stats), +[Dmitro Petrenko](https://dribbble.com/shots/5425378-Mobile-Application-Dashboard-for-Stock-Platform), +[Ghani Pradita](https://dribbble.com/shots/6379476-Calories-Management-App), +[MONUiXD](https://www.uplabs.com/posts/chart-pie-chart-bar-chart). +Thank you all! + + + +# Let's get started + +First of all, you need to add the `fl_chart` in your project. In order to do that, follow [this guide](https://pub.dev/packages/fl_chart/install). + +Then you need to read the docs. Start from [here](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/index.md). + +We suggest you to check samples source code. + +##### - You can read about the animation handling [here](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/handle_animations.md) +|Sample1 |Sample2 |Sample3 | +|:------------:|:------------:|:-------------:| +| [![](https://github.com/imaNNeo/fl_chart/raw/main/repo_files/images/line_chart/line_chart_sample_1_anim.gif)](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/line_chart.md#sample-1-source-code) | [![](https://github.com/imaNNeo/fl_chart/raw/main/repo_files/images/line_chart/line_chart_sample_2_anim.gif)](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/line_chart.md#sample-2-source-code) | [![](https://github.com/imaNNeo/fl_chart/raw/main/repo_files/images/bar_chart/bar_chart_sample_1_anim.gif)](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/bar_chart.md#sample-1-source-code) | + +### Try it out +You can try the FL Chart sample app on the platforms that are available below: + +[![Get it on Google Play](https://img.shields.io/badge/Get%20it%20on-Google%20Play-green?style=for-the-badge&logo=google-play&logoColor=white)](https://play.google.com/store/apps/details?id=dev.flchart.app) +[![Download on the App Store](https://img.shields.io/badge/Download-on%20the%20App%20Store-blue?style=for-the-badge&logo=app-store&logoColor=white)](https://apps.apple.com/us/app/fl-chart/id6476523019) +[![Try it on Web](https://img.shields.io/badge/Try%20it%20on-Web-yellow?style=for-the-badge&logo=google-chrome&logoColor=white)](https://app.flchart.dev) + +[//]: # ([![Download for macOS](https://img.shields.io/badge/Download-for%20macOS-darkblue?style=for-the-badge&logo=apple&logoColor=white)](https://apps.apple.com/app/your-macos-app-id)) +[//]: # ([![Download for Linux](https://img.shields.io/badge/Download-for%20Linux-orange?style=for-the-badge&logo=linux&logoColor=white)](https://your-linux-distribution-link.com)) +[//]: # ([![Download for Windows](https://img.shields.io/badge/Download-for%20Windows-blue?style=for-the-badge&logo=windows&logoColor=white)](https://your-windows-app-link.com)) + + +### Donation +Your donation **motivates** me to work more on the **fl_chart** and resolve more issues. +There are multiple ways to donate me: + +1. You can be my sponsor on [GitHub](https://github.com/sponsors/imaNNeo) (This is the most reliable way to donate me) +2. You can buy me a coffee! +3. Or if you are a fan of crypto, you can donate me Bitcoins here: `1L7ghKdcmgydmUJAnmYmMaiVjT1LoP4a45` + +### Contributing +##### :beer: Pull requests are welcome! +Don't forget that `open-source` makes no sense without contributors. No matter how big your changes are, it helps us a lot even it is a line of change. + +There might be a lot of grammar issues in the docs. It's a big help to us to fix them if you are fluent in English. + +Check out [CONTRIBUTING.md](https://github.com/imaNNeo/fl_chart/blob/main/CONTRIBUTING.md), which contains a guide for those who want to contribute to the FL Chart. + +Reporting bugs and issues are contribution too, yes it is. + +#### Below are the people who has contributed to the FL Chart. We hope we have your picture here soon. +[![](https://opencollective.com/fl_chart/contributors.svg?width=890&button=false)](https://github.com/imaNNeo/fl_chart/graphs/contributors) diff --git a/SOURCES.md b/SOURCES.md new file mode 100644 index 0000000..775d364 --- /dev/null +++ b/SOURCES.md @@ -0,0 +1,95 @@ +### Sources to learn more about the fl_chart: + +All sources are sorted by date. + +Did you find any new article or source? please contribute to have them all here. + +#### Blog post: + +* [Design Stunning Charts with fl_charts in Flutter](https://www.atatus.com/blog/design-stunning-charts-with-fl-charts-in-flutter/) + + +* [Build beautiful charts in Flutter with FL Chart](https://blog.logrocket.com/build-beautiful-charts-flutter-fl-chart) + + +* [Flutter4Fun UI Challenge 7](https://flutter4fun.com/ui-challenge-7/) + + +* [Stock charts](https://dev.to/kamilpowalowski/stock-charts-with-flchart-library-1gd2) + +#### Video: + + +* [how to create line chart in flutter | fl_chart](https://www.youtube.com/watch?v=Iv3F2HO5Jvc) + + +* [line chart in flutter - flutter tutorial](https://www.youtube.com/watch?v=xHzDAewbSGY) + + +* [Portfolio Dashboard Flutter UI Desktop & Web](https://www.youtube.com/watch?v=H9vXUine7Zo) + + +* [Flutter UI | Stocks App UI Design - Day 55](https://www.youtube.com/watch?v=oILraFu8OE8) + + +* [Implementing Chart in Flutter - Pair Programming with Fl_Chart Author](https://www.youtube.com/watch?v=msMxuUERtg8) + + +* [how to create line chart in flutter | fl_chart](https://www.youtube.com/watch?v=Iv3F2HO5Jvc) + + +* [Responsive Admin Dashboard or Panel using Flutter - Flutter Web UI - Part 1](https://www.youtube.com/watch?v=MRiZpwdy1CM) + + +* [Admin Panel Dashboard - Flutter Responsive UI Design](https://www.youtube.com/watch?v=n7O3pXfENPU) + + +* [How to build Flutter UI - 3 Steps](https://www.youtube.com/watch?v=I0NBtFS_ibc) + + +* [Flutter Web - Dashboard Website Template (Responsive)](https://www.youtube.com/watch?v=3SMdJE_dSxU) + + +* [How to create charts in Flutter](https://www.youtube.com/watch?v=JBJ6o4blgPA) + + +* [Flutter Charts 📊📈](https://www.youtube.com/watch?v=ibkcwCv9Lyw) + + +* [Flutter Library for Customizable](https://www.youtube.com/watch?v=1pjAItIDNz8) + + +* [Pie Chart - FLChart](https://www.youtube.com/watch?v=rZx_isqXrhg&t=77s) + + +* [Flutter Tutorial - Bar Chart](https://www.youtube.com/watch?v=7wUmzYOPQ8w) + + +* [wallet-app-ui-piechart](https://www.youtube.com/watch?v=M4w-dighmMU) + + +* [Flutter UI Tutorial - Fitness App](https://www.youtube.com/watch?v=hTg4DDl8Ixo) + + +* [Gradient Chart](https://www.youtube.com/watch?v=OR2DMRnEXkA) + + +* [Flutter charts tutorial for beginners](https://www.youtube.com/watch?v=nCmihMrWS38) + + +* [The easy way with fl-Chart](https://www.youtube.com/watch?v=R_vpnW5QZEw) + + +* [Get the data form COVID-19 API](https://www.youtube.com/watch?v=QXMWzbdGDkA) + + +* [Flutter COVID-19 Dashboard UI](https://www.youtube.com/watch?v=krU-ASLb8lM) + + +* [Flutter UI](https://www.youtube.com/watch?v=axWBN1aotQk) + + +* [Flutter](https://www.youtube.com/watch?v=rwHFslLo6ho) + + +* [Setup Pie Charts](https://www.youtube.com/watch?v=zRZiJdbp3_E) diff --git a/_config.yml b/_config.yml new file mode 100644 index 0000000..3397c9a --- /dev/null +++ b/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-architect \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..1b1ad3e --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,20 @@ +include: package:very_good_analysis/analysis_options.yaml +analyzer: + exclude: + - "**.mocks.dart" +linter: + rules: + always_put_required_named_parameters_first: false + avoid_bool_literals_in_conditional_expressions: false + avoid_positional_boolean_parameters: false + comment_references: false + library_private_types_in_public_api: false + lines_longer_than_80_chars: false + no_default_cases: false + parameter_assignments: false + prefer_asserts_with_message: false + prefer_constructors_over_static_methods: false + public_member_api_docs: false + use_if_null_to_convert_nulls_to_bools: false + use_setters_to_change_properties: false + avoid_catches_without_on_clauses: false diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 0000000..8655937 --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,76 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +pubspec.lock + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.packages +.pub-cache/ +.pub/ +/build/ +.flutter-plugins-dependencies + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* +**/ios/Flutter/Flutter.podspec + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/example/.metadata b/example/.metadata new file mode 100644 index 0000000..7fdf3f2 --- /dev/null +++ b/example/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 + channel: stable + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 + base_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 + - platform: windows + create_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 + base_revision: 90c64ed42ba53a52d18f0cb3b17666c8662ed2a0 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..d02c91b --- /dev/null +++ b/example/README.md @@ -0,0 +1,4 @@ +This is the FL Chart App. +Check it out live here: + +[app.flchart.dev](https://app.flchart.dev) diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml new file mode 100644 index 0000000..61b6c4d --- /dev/null +++ b/example/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/example/android/.gitignore b/example/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/example/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle new file mode 100644 index 0000000..94d9508 --- /dev/null +++ b/example/android/app/build.gradle @@ -0,0 +1,55 @@ +plugins { + id "com.android.application" + id "kotlin-android" + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id "dev.flutter.flutter-gradle-plugin" +} + +def keystoreProperties = new Properties() +def keystorePropertiesFile = rootProject.file('key.properties') +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) +} + +android { + namespace = "dev.flchart.app" + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8 + } + + + defaultConfig { + applicationId "dev.flchart.app" + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + signingConfigs { + release { + keyAlias = keystoreProperties['keyAlias'] + keyPassword = keystoreProperties['keyPassword'] + storeFile = keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null + storePassword = keystoreProperties['storePassword'] + } + } + + buildTypes { + release { + signingConfig = signingConfigs.release + } + } +} + +flutter { + source '../..' +} diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..cd1ea04 --- /dev/null +++ b/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..e65aebe --- /dev/null +++ b/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/android/app/src/main/ic_launcher-playstore.png b/example/android/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..1bf7e03 Binary files /dev/null and b/example/android/app/src/main/ic_launcher-playstore.png differ diff --git a/example/android/app/src/main/kotlin/dev/flchart/app/MainActivity.kt b/example/android/app/src/main/kotlin/dev/flchart/app/MainActivity.kt new file mode 100644 index 0000000..5b21453 --- /dev/null +++ b/example/android/app/src/main/kotlin/dev/flchart/app/MainActivity.kt @@ -0,0 +1,6 @@ +package dev.flchart.app + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/example/android/app/src/main/res/drawable-v21/launch_background.xml b/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + 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-anydpi-v26/ic_launcher.xml b/example/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/example/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/example/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/example/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/example/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..a3b0e5b Binary files /dev/null and b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..cf6ae6d Binary files /dev/null and b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..41ec75b Binary files /dev/null and b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..9d9b7d1 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..4f9cb0a Binary files /dev/null and b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..331409b Binary files /dev/null and b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..3bfdd6f Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..190718c Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..c02f7e0 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..4290f09 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..11b107e Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..4635ec6 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..b6cdec3 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..3de07ca Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..7a002ea Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/example/android/app/src/main/res/values-night/styles.xml b/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/example/android/app/src/main/res/values/ic_launcher_background.xml b/example/android/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..faf3969 --- /dev/null +++ b/example/android/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #282E45 + \ No newline at end of file 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..cb1ef88 --- /dev/null +++ b/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/example/android/app/src/profile/AndroidManifest.xml b/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..cd1ea04 --- /dev/null +++ b/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/example/android/build.gradle b/example/android/build.gradle new file mode 100644 index 0000000..d2ffbff --- /dev/null +++ b/example/android/build.gradle @@ -0,0 +1,18 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = "../build" +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/example/android/gradle.properties b/example/android/gradle.properties new file mode 100644 index 0000000..2597170 --- /dev/null +++ b/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..7bb2df6 --- /dev/null +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip diff --git a/example/android/settings.gradle b/example/android/settings.gradle new file mode 100644 index 0000000..b9e43bd --- /dev/null +++ b/example/android/settings.gradle @@ -0,0 +1,25 @@ +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "8.1.0" apply false + id "org.jetbrains.kotlin.android" version "1.8.22" apply false +} + +include ":app" diff --git a/example/assets/data/amsterdam_2024_weather.csv b/example/assets/data/amsterdam_2024_weather.csv new file mode 100644 index 0000000..b4f4f74 --- /dev/null +++ b/example/assets/data/amsterdam_2024_weather.csv @@ -0,0 +1,367 @@ +name,datetime,tempmax,tempmin,temp,feelslikemax,feelslikemin,feelslike,dew,humidity,precip,precipprob,precipcover,preciptype,snow,snowdepth,windgust,windspeed,winddir,sealevelpressure,cloudcover,visibility,solarradiation,solarenergy,uvindex,severerisk,sunrise,sunset,moonphase,conditions,description,icon,stations +"Amsterdam,Netherlands",2024-01-01,9.1,6.4,8,5.3,2.5,4.1,5.1,82.4,14.26,100,37.5,rain,0,0,53.9,40.2,225.9,1000.1,88.7,20.5,20.6,1.8,2,,2024-01-01T08:50:34,2024-01-01T16:37:06,0.68,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-01-02,12.2,6.6,10.2,12.2,2.7,9,8.8,91.4,17.935,100,66.67,rain,0,0,91,62.5,205.1,987.8,100,12.9,7.4,0.6,1,,2024-01-02T08:50:27,2024-01-02T16:38:10,0.71,"Rain, Overcast",Cloudy skies throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-01-03,10.5,8.1,9.6,10.5,4.5,7.5,7.7,88,9.238,100,62.5,rain,0,0,106.8,58.1,248.3,987.8,96.3,15.4,10.2,1,1,,2024-01-03T08:50:16,2024-01-03T16:39:18,0.74,"Rain, Overcast",Cloudy skies throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-01-04,8.5,6.7,7.4,7.1,4,5.6,6.6,94.6,8.205,100,62.5,rain,0,0,28.1,17.7,235.3,1000.4,99.6,22.1,14.3,1.3,1,,2024-01-04T08:50:01,2024-01-04T16:40:29,0.75,"Rain, Overcast",Cloudy skies throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-01-05,8.5,5.5,7.1,5.7,2.3,4.3,6.1,93.7,8.285,100,58.33,rain,0,0,45,29.9,118.1,996.5,99.9,21.5,5.2,0.6,0,,2024-01-05T08:49:44,2024-01-05T16:41:42,0.8,"Rain, Overcast",Cloudy skies throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-01-06,5,1.4,3.7,1.2,-3.6,-0.2,1.8,87.9,0.098,100,8.33,rain,0,0,35.2,21.8,11,1011.1,99.3,29.8,9.3,0.8,1,,2024-01-06T08:49:22,2024-01-06T16:42:57,0.84,"Rain, Overcast",Cloudy skies throughout the day with rain in the morning and afternoon.,rain,"06260099999,D3248,06249099999,C0449,06240099999,06269099999,06257099999" +"Amsterdam,Netherlands",2024-01-07,1.3,-0.7,0.5,-2.9,-5.9,-4.9,-2.7,79.1,0.015,100,4.17,"rain,snow",0,0,48.8,31.7,52.5,1024.9,67.5,39.1,22,1.9,2,,2024-01-07T08:48:58,2024-01-07T16:44:15,0.87,"Snow, Rain, Partially cloudy",Partly cloudy throughout the day with morning rain or snow.,rain,"06260099999,D3248,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-01-08,0.1,-2,-0.8,-5.9,-9.1,-7.2,-5.4,71.7,0,0,0,,0,0,49.5,37.2,63,1032.8,57.5,44.3,22.9,1.8,2,,2024-01-08T08:48:30,2024-01-08T16:45:36,0.9,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"06260099999,D3248,06249099999,C0449,06240099999,06269099999,06257099999" +"Amsterdam,Netherlands",2024-01-09,-0.3,-4.1,-2.3,-6.6,-11.5,-9.1,-8.4,64,0,0,0,,0,0,46.1,30.7,64.6,1034,0,41.4,40,3.4,3,,2024-01-09T08:47:58,2024-01-09T16:46:59,0.93,Clear,Clear conditions throughout the day.,clear-day,"06260099999,D3248,06249099999,C0449,06240099999,06269099999,06257099999" +"Amsterdam,Netherlands",2024-01-10,0.3,-3.6,-2,-5.4,-9.6,-7.7,-8.4,62.2,0,0,0,,0,0,33.9,24.2,60.6,1031.1,0.1,41.8,39,3.4,2,,2024-01-10T08:47:24,2024-01-10T16:48:24,0.97,Clear,Clear conditions throughout the day.,clear-day,"06260099999,D3248,06249099999,C0449,06240099999,06269099999,06257099999" +"Amsterdam,Netherlands",2024-01-11,4.7,-5.3,-0.3,3.2,-8.7,-2.6,-2.4,86,0,0,0,,0,0,27.3,10.6,9.4,1034.4,59.7,14.8,13.4,1.1,1,,2024-01-11T08:46:46,2024-01-11T16:49:51,0,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"06260099999,D3248,06249099999,C0449,06240099999,06269099999,06257099999" +"Amsterdam,Netherlands",2024-01-12,5.6,2.3,4,3.9,1,2.4,2.6,90.9,0.01,100,4.17,rain,0,0,31.8,13.7,328.2,1033.1,100,18.5,20.2,1.7,2,,2024-01-12T08:46:05,2024-01-12T16:51:20,0.03,"Rain, Overcast",Cloudy skies throughout the day with late afternoon rain.,rain,"06260099999,D3248,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-01-13,5.9,3.2,4.6,2.1,-0.9,1,3.4,92.3,1.002,100,41.67,rain,0,0,38.6,26.7,265.8,1022.5,99.6,17,10,0.8,1,,2024-01-13T08:45:21,2024-01-13T16:52:51,0.07,"Rain, Overcast",Cloudy skies throughout the day with rain.,rain,"06260099999,D3248,06249099999,C0449,06240099999,06257099999" +"Amsterdam,Netherlands",2024-01-14,5.7,2.7,4.3,2.1,-1.6,0.5,2.5,88,2.747,100,62.5,rain,0,0,42,27.2,275.1,1009.3,95.4,25.6,11.8,1,1,,2024-01-14T08:44:33,2024-01-14T16:54:24,0.1,"Rain, Overcast",Cloudy skies throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06249099999,C0449,06240099999,06269099999,06257099999" +"Amsterdam,Netherlands",2024-01-15,3.9,1.4,2.5,0.2,-4.4,-2.1,-0.3,81.9,3.366,100,70.83,"rain,snow",0.1,0,60.6,34.4,301,1003.2,69.4,28.2,25,2.1,3,,2024-01-15T08:43:43,2024-01-15T16:55:59,0.14,"Snow, Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain or snow throughout the day.,snow,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999" +"Amsterdam,Netherlands",2024-01-16,3,-0.2,1.3,-1.3,-5.6,-3,-1.8,80.5,2.814,100,41.67,"rain,snow",0.1,0.5,49.7,30.3,231,1006.7,88.4,34.2,45.8,3.9,3,,2024-01-16T08:42:50,2024-01-16T16:57:36,0.17,"Snow, Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain or snow throughout the day.,snow,"06260099999,D3248,06249099999,C0449,06240099999,06269099999,06257099999" +"Amsterdam,Netherlands",2024-01-17,0.5,-2.1,-0.5,0.2,-7.3,-4,-2.8,84.5,0.129,100,8.33,"rain,snow",0,0.5,31.6,24.3,184.6,993.6,99.8,25.1,18.3,1.6,1,,2024-01-17T08:41:53,2024-01-17T16:59:14,0.21,"Snow, Rain, Overcast",Cloudy skies throughout the day with rain or snow clearing later.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999" +"Amsterdam,Netherlands",2024-01-18,3.5,-3.4,-0.3,2.9,-6.3,-2.3,-2.5,85.7,0.337,100,8.33,"rain,snow",0,0,24.5,12.9,292.9,1001.5,40.9,34.2,22.9,2.1,2,,2024-01-18T08:40:54,2024-01-18T17:00:53,0.25,"Snow, Rain, Partially cloudy",Partly cloudy throughout the day with morning rain or snow.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999" +"Amsterdam,Netherlands",2024-01-19,4.8,-1.3,2,0.9,-4.8,-2.1,-1.1,80.5,0.751,100,33.33,"rain,snow",0.2,0,38.5,23.9,245.2,1018.5,33.8,32.3,47.6,4,3,,2024-01-19T08:39:52,2024-01-19T17:02:34,0.28,"Snow, Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain or snow throughout the day.,snow,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-01-20,2.2,-0.2,0.9,-3.1,-6.1,-4.5,-2.1,80.8,0,0,0,,0,0,38.9,29.8,201,1025.9,89.7,21.2,30.4,2.6,1,,2024-01-20T08:38:47,2024-01-20T17:04:17,0.31,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-01-21,7.4,-0.1,3.4,2.2,-5.7,-2,-0.6,76,0.201,100,4.17,rain,0,0,66.7,45.1,189.5,1016.4,99.6,34.2,9.9,0.7,0,,2024-01-21T08:37:40,2024-01-21T17:06:00,0.35,"Rain, Overcast",Cloudy skies throughout the day with late afternoon rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-01-22,11.5,7.6,9.5,11.5,3.2,6.8,6.6,82.3,1.635,100,50,rain,0,0,83.1,55.8,228,1005.8,64.2,16.6,38.5,3.3,3,,2024-01-22T08:36:29,2024-01-22T17:07:45,0.38,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-01-23,11.5,6.2,8.1,11.5,2.1,4.4,5.3,83,4.604,100,33.33,rain,0,0,82,57.5,230.6,1018.5,71.9,14.7,26.8,2.3,2,,2024-01-23T08:35:16,2024-01-23T17:09:31,0.42,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"06260099999,C0449,06257099999,EHAM,D3248,EHRD,06348099999,06249099999,06240099999,EHLE,EHKD,06269099999,06344099999" +"Amsterdam,Netherlands",2024-01-24,12.2,8.6,10,12.2,4.9,8,6.6,79.2,1.409,100,16.67,rain,0,0,94.2,61.6,247.1,1018.3,78.8,14.1,31.8,2.7,2,,2024-01-24T08:34:01,2024-01-24T17:11:17,0.45,"Rain, Partially cloudy",Partly cloudy throughout the day with rain clearing later.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-01-25,9.6,3.4,6.9,6.8,0.9,4.2,5.7,92.3,1.425,100,33.33,rain,0,0,38.4,26.8,222.3,1026.6,80.5,8.7,20.9,1.8,2,,2024-01-25T08:32:43,2024-01-25T17:13:05,0.5,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-01-26,11.4,3.9,8.5,11.4,1.1,5.7,5.8,84,4.101,100,29.17,rain,0,0,68.2,48.4,256.5,1024.2,59.2,18.1,44.4,3.8,4,,2024-01-26T08:31:22,2024-01-26T17:14:54,0.52,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-01-27,7.9,2,4.2,5.2,-1.7,0.9,2.5,89.1,0.268,100,4.17,rain,0,0,28.1,20.2,207.5,1034.4,56.5,16.7,43.9,3.9,2,,2024-01-27T08:30:00,2024-01-27T17:16:43,0.56,"Rain, Partially cloudy",Partly cloudy throughout the day with morning rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-01-28,8.6,0.8,4.2,5.6,-3.3,0.9,0,74.9,0,0,0,,0,0,30.7,20.7,150.7,1027.6,88.2,33,62,5.3,3,,2024-01-28T08:28:34,2024-01-28T17:18:33,0.59,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-01-29,10.5,3,6.4,10.5,-0.2,3.9,4.1,85.4,0,0,0,,0,0,27.8,16.6,171.7,1025.1,99.7,28.6,39.1,3.4,2,,2024-01-29T08:27:07,2024-01-29T17:20:24,0.62,Overcast,Cloudy skies throughout the day.,cloudy,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-01-30,10.5,4.7,7.8,10.5,2.8,5,6.7,92.3,0.644,100,20.83,rain,0,0,58.9,34.3,227.3,1026.7,99.9,17.4,13.5,1.2,1,,2024-01-30T08:25:37,2024-01-30T17:22:15,0.65,"Rain, Overcast",Cloudy skies throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-01-31,8.3,5.1,6.5,5.3,2.5,3.4,3.4,81,0.075,100,8.33,rain,0,0,56.2,37.5,221.6,1030.4,100,34.6,12.7,1.1,1,,2024-01-31T08:24:05,2024-01-31T17:24:07,0.69,"Rain, Overcast",Cloudy skies throughout the day with rain clearing later.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-02-01,9.1,4.4,6.6,5.3,1.3,3.3,4.6,87.4,4.35,100,20.83,rain,0,0,59.4,37,272.7,1029.2,64.5,12.1,56.7,4.9,4,,2024-02-01T08:22:31,2024-02-01T17:25:59,0.72,"Rain, Partially cloudy",Partly cloudy throughout the day with rain clearing later.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06257099999" +"Amsterdam,Netherlands",2024-02-02,9.1,5.9,7.6,5.5,2.4,3.8,6.2,90.6,0,0,0,,0,0,49.7,34.1,237.3,1026.9,99.6,12.7,25,2.1,2,,2024-02-02T08:20:55,2024-02-02T17:27:52,0.75,Overcast,Cloudy skies throughout the day.,cloudy,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06257099999" +"Amsterdam,Netherlands",2024-02-03,10.9,9.1,10,10.9,6,7.7,8.4,89.9,0,0,0,,0,0,54.2,34.1,249.4,1024.4,99.5,23.2,37,3.1,3,,2024-02-03T08:19:17,2024-02-03T17:29:45,0.75,Overcast,Cloudy skies throughout the day.,cloudy,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-02-04,10.5,8.3,9.7,10.5,5.4,8,8.3,91.1,2.126,100,20.83,rain,0,0,61,38.5,252.3,1020.7,99.9,18.6,13.2,1.1,1,,2024-02-04T08:17:37,2024-02-04T17:31:38,0.82,"Rain, Overcast",Cloudy skies throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-02-05,10.4,8.7,9.4,10.4,4.6,5.6,7.1,85.8,0.051,100,12.5,rain,0,0,72.5,50.4,240.1,1017.4,99.8,16.7,22.2,2,1,,2024-02-05T08:15:56,2024-02-05T17:33:32,0.85,"Rain, Overcast",Cloudy skies throughout the day with rain.,rain,"06260099999,06348099999,06249099999,C0449,06240099999,06257099999" +"Amsterdam,Netherlands",2024-02-06,12,7,10.5,12,3.5,10,7.9,84,4.573,100,25,rain,0,0,82.3,57.4,237.3,1007.6,100,14.3,24.6,2.1,2,,2024-02-06T08:14:12,2024-02-06T17:35:25,0.88,"Rain, Overcast",Cloudy skies throughout the day with rain.,rain,"D3248,06260099999,06348099999,06249099999,06240099999,C0449,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-02-07,6.7,1.4,4.8,6.7,1.4,3.2,1.8,82,3.834,100,29.17,rain,0,0,27.8,15.6,345.1,1005.3,100,22.2,46.1,4,3,,2024-02-07T08:12:27,2024-02-07T17:37:19,0.92,"Rain, Overcast",Cloudy skies throughout the day with rain clearing later.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06257099999" +"Amsterdam,Netherlands",2024-02-08,4,0.5,2.4,1.7,-2.7,-0.3,1.6,94.4,8.38,100,45.83,rain,0.4,0,42.8,24.9,92.6,997.7,99.6,11.9,13.1,1.2,1,,2024-02-08T08:10:39,2024-02-08T17:39:13,0.95,"Rain, Overcast",Cloudy skies throughout the day with rain or snow.,snow,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06257099999" +"Amsterdam,Netherlands",2024-02-09,12.2,4.9,10.3,12.2,2,9.1,8.7,90.6,12.645,100,54.17,rain,0,0,44.9,27.6,176.5,983.4,99.8,25.9,20.9,1.7,1,,2024-02-09T08:08:51,2024-02-09T17:41:07,0,"Rain, Overcast",Cloudy skies throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-02-10,11.9,9.4,10.4,11.9,7.3,9.6,8.9,90.6,0.454,100,12.5,rain,0,0,24.3,17.2,137.6,985.9,99.4,31.1,37.3,3.3,2,,2024-02-10T08:07:00,2024-02-10T17:43:01,0.02,"Rain, Overcast",Cloudy skies throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-02-11,10,7.9,8.9,8.7,5.2,6.9,8.1,94.4,3.279,100,54.17,rain,0,0,31.9,20.8,211.1,989.7,99.9,10.5,22.8,2,2,,2024-02-11T08:05:08,2024-02-11T17:44:55,0.05,"Rain, Overcast",Cloudy skies throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-02-12,8.9,3.7,7,7,0.5,4.2,5.4,90.2,0.591,100,20.83,rain,0,0,38,26.6,246,1003.4,76.5,21.8,57.4,4.9,4,,2024-02-12T08:03:15,2024-02-12T17:46:49,0.09,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-02-13,9.1,3.8,6.6,6,0.5,3.1,4.6,87,0.083,100,16.67,rain,0,0,39.1,27.7,200.4,1014.3,95.5,24.5,41.9,3.5,3,,2024-02-13T08:01:20,2024-02-13T17:48:43,0.12,"Rain, Overcast",Cloudy skies throughout the day with late afternoon rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-02-14,11.7,8,10.6,11.7,4.2,9.9,10.1,96.6,11.723,100,70.83,rain,0,0,49.8,34.4,224.9,1013.4,100,8.9,13.6,1.2,1,,2024-02-14T07:59:23,2024-02-14T17:50:36,0.15,"Rain, Overcast",Cloudy skies throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-02-15,15.6,11.2,12.8,15.6,11.2,12.8,11.3,90.8,6.248,100,37.5,rain,0,0,31.9,23.2,180.4,1012.6,99.2,23.2,31.5,2.8,2,,2024-02-15T07:57:26,2024-02-15T17:52:30,0.19,"Rain, Overcast",Cloudy skies throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-02-16,12,9.2,10.7,12,7.2,10.3,9,89.9,2.067,100,20.83,rain,0,0,33,23.6,228.8,1012.9,97.4,28.2,21,1.8,1,,2024-02-16T07:55:26,2024-02-16T17:54:23,0.25,"Rain, Overcast",Cloudy skies throughout the day with rain in the morning and afternoon.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-02-17,11.4,7.9,9.8,11.4,6.2,9.1,8.4,91.4,0.415,100,12.5,rain,0,0,23.8,16.8,228.3,1029.2,99.2,20.9,35.3,3,2,,2024-02-17T07:53:26,2024-02-17T17:56:17,0.26,"Rain, Overcast",Cloudy skies throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-02-18,10.4,8.4,9.4,10.4,4.6,6.9,8.6,94.5,16.3,100,79.17,rain,0,0,56.4,35,225.4,1023.8,100,12.1,10.5,0.8,1,,2024-02-18T07:51:24,2024-02-18T17:58:10,0.29,"Rain, Overcast",Cloudy skies throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-02-19,10.4,5.7,8.7,10.4,3.4,6.2,7.4,92,4.968,100,25,rain,0,0,44.6,33.1,269.7,1026,95,14.2,23.8,2,1,,2024-02-19T07:49:22,2024-02-19T18:00:03,0.33,"Rain, Overcast",Cloudy skies throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-02-20,10.4,5.1,8.3,10.4,2.6,5.6,6.7,90,0.207,100,4.17,rain,0,0,51.2,33.9,231.7,1025.6,99.7,13.7,43.3,3.7,2,,2024-02-20T07:47:18,2024-02-20T18:01:55,0.36,"Rain, Overcast",Cloudy skies throughout the day with morning rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-02-21,10.1,8.7,9.3,10.1,4.8,5.9,7.7,90,5.415,100,37.5,rain,0,0,49.7,37.4,203.7,1011.3,100,15.3,23,1.9,2,,2024-02-21T07:45:13,2024-02-21T18:03:48,0.39,"Rain, Overcast",Cloudy skies throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-02-22,12.5,5.2,9.9,12.5,-0.5,8.1,8.9,93.8,6.403,100,58.33,rain,0,0,67.7,49.2,208.2,986.4,98.1,13.7,27,2.3,1,,2024-02-22T07:43:07,2024-02-22T18:05:40,0.43,"Rain, Overcast",Cloudy skies throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06257099999" +"Amsterdam,Netherlands",2024-02-23,8.2,4.9,6.2,4.6,-0.8,1.8,3.4,82.3,6.364,100,66.67,rain,0,0,69.8,41.3,209.9,988.7,94.9,32.8,34.5,3,3,,2024-02-23T07:40:59,2024-02-23T18:07:32,0.46,"Rain, Overcast",Cloudy skies throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-02-24,6.2,2.7,4.7,3.3,-0.8,1.4,3.2,89.8,4.341,100,29.17,rain,0,0,38.4,27.8,184.4,996.8,96.7,29.3,34.1,3,1,,2024-02-24T07:38:51,2024-02-24T18:09:23,0.5,"Rain, Overcast",Cloudy skies throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-02-25,8.9,4.1,6.2,8.9,1.4,3.6,4.2,87.5,4.063,100,29.17,rain,0,0,36.7,17.6,164.3,999.5,94.9,26.3,44.7,4,3,,2024-02-25T07:36:42,2024-02-25T18:11:15,0.53,"Rain, Overcast",Cloudy skies throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-02-26,6.5,4.6,5.5,2.5,-0.1,1,3,83.9,0.122,100,29.17,rain,0,0,56.3,34.6,41,1006.5,91.3,36.3,37.9,3.3,2,,2024-02-26T07:34:32,2024-02-26T18:13:06,0.57,"Rain, Overcast",Cloudy skies throughout the day with rain in the morning and afternoon.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-02-27,8.1,1.7,4.7,7,-0.8,2.3,1.8,82.1,0.05,100,4.17,rain,0,0,44.8,20.9,22.1,1018.7,55.9,33.1,85.5,7.3,5,,2024-02-27T07:32:22,2024-02-27T18:14:56,0.6,"Rain, Partially cloudy",Partly cloudy throughout the day with morning rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-02-28,9.4,2.8,6.9,6.6,0,4.1,5.4,90.2,0,0,0,,0,0,37.1,23.9,190.2,1019.2,91.5,15.6,29.1,2.5,2,,2024-02-28T07:30:10,2024-02-28T18:16:47,0.63,Overcast,Cloudy skies throughout the day.,cloudy,"06260099999,C0449,06257099999,EHAM,D3248,EHRD,06348099999,06249099999,06240099999,EHLE,EHKD,06269099999,06344099999" +"Amsterdam,Netherlands",2024-02-29,9.1,7,8.3,5.9,3.4,4.9,7.6,95.7,4.966,100,79.17,rain,0,0,35.8,25.3,174.3,1008,100,10,15.8,1.5,1,,2024-02-29T07:27:58,2024-02-29T18:18:37,0.67,"Rain, Overcast",Cloudy skies throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-03-01,9.2,5.1,7.6,6.5,2,4.2,5.4,86,2.291,100,33.33,rain,0,0,57.5,37.6,171.4,999.9,80.7,28.5,55.7,4.8,3,,2024-03-01T07:25:45,2024-03-01T18:20:27,0.7,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"06260099999,C0449,06257099999,EHAM,D3248,EHRD,06348099999,06249099999,06240099999,EHLE,EHKD,06269099999,06344099999" +"Amsterdam,Netherlands",2024-03-02,11.5,5.4,8.9,11.5,1.9,6.8,4.2,73.6,0.755,100,20.83,rain,0,0,49.4,37.1,150.7,998.6,72.6,39.8,98.6,8.4,5,,2024-03-02T07:23:31,2024-03-02T18:22:17,0.73,"Rain, Partially cloudy",Partly cloudy throughout the day with rain clearing later.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-03-03,14.3,7.8,10.6,14.3,5,9.6,6.6,77,0,0,0,,0,0,36.1,21.5,143.2,1001.1,99.5,32,76.7,6.6,4,,2024-03-03T07:21:16,2024-03-03T18:24:06,0.75,Overcast,Cloudy skies throughout the day.,cloudy,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-03-04,10.4,4.2,7.5,10.4,1.9,5.6,4.2,81,0,0,0,,0,0,33.7,23.8,306.9,1011.1,98.3,35.7,88.3,7.7,5,,2024-03-04T07:19:01,2024-03-04T18:25:55,0.8,Overcast,Cloudy skies throughout the day.,cloudy,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-03-05,8.2,2.9,6,8.1,1,5.1,5.2,94.6,0.466,100,33.33,rain,0,0,27.8,7.4,77.8,1014.2,91.2,8.8,21.5,1.9,1,,2024-03-05T07:16:45,2024-03-05T18:27:44,0.83,"Rain, Overcast",Cloudy skies throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-03-06,11.3,5.4,7.5,11.3,3.4,6.4,5.4,87.9,0.426,100,8.33,rain,0,0,23.4,13.9,108.3,1022.1,53.8,11.7,96.5,8.5,5,,2024-03-06T07:14:29,2024-03-06T18:29:32,0.87,"Rain, Partially cloudy",Partly cloudy throughout the day with rain clearing later.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-03-07,8.9,3.2,5.6,5.5,-0.3,2.3,2.9,83.5,0,0,0,,0,0,39.5,27.8,75.9,1023.6,27,13.2,117.4,10.1,5,,2024-03-07T07:12:12,2024-03-07T18:31:20,0.9,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"06260099999,C0449,06257099999,EHAM,D3248,EHRD,06348099999,06249099999,06240099999,EHLE,EHKD,06269099999,06344099999" +"Amsterdam,Netherlands",2024-03-08,9.7,1.8,5.5,6,-3.2,1.3,0.3,70.1,0,0,0,,0,0,47.2,33.1,90.1,1012.6,2.6,17.7,118.2,10.3,5,,2024-03-08T07:09:55,2024-03-08T18:33:08,0.94,Clear,Clear conditions throughout the day.,clear-day,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-03-09,12.5,3.6,7.9,12.5,-0.4,5.6,2.7,70.3,0,0,0,,0,0,36.3,26,97.8,1001.4,78.8,18.6,89.4,7.7,5,,2024-03-09T07:07:37,2024-03-09T18:34:56,0.97,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-03-10,11.3,6.2,8.7,11.3,2.5,6.5,4.2,73.5,0,0,0,,0,0,38.8,27.3,71.8,997.1,99.7,18.2,59.8,5.3,3,,2024-03-10T07:05:19,2024-03-10T18:36:44,0,Overcast,Cloudy skies throughout the day.,cloudy,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-03-11,8.3,6.7,7.1,7.2,4.1,5.5,6.1,93.3,3.569,100,58.33,rain,0,0,28.9,13.9,18.7,1002.1,99,5.9,18.2,1.6,1,,2024-03-11T07:03:01,2024-03-11T18:38:31,0.04,"Rain, Overcast",Cloudy skies throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-03-12,10.7,7,8.4,10.7,4,6.3,6.8,90.4,1.414,100,33.33,rain,0,0,31.6,21.5,215.3,1011.9,97.7,6.9,41,3.5,2,,2024-03-12T07:00:42,2024-03-12T18:40:18,0.07,"Rain, Overcast",Cloudy skies throughout the day with rain.,rain,"06260099999,06356099999,C0449,06257099999,EHAM,D3248,06348099999,06249099999,06240099999,06330099999,EHLE,06269099999,06344099999" +"Amsterdam,Netherlands",2024-03-13,12.2,8.8,10.9,12.2,6.6,10.7,9.3,90.2,1.099,100,37.5,rain,0,0,51.2,32.5,225.8,1013.3,99.6,21.2,27,2.2,1,,2024-03-13T06:58:23,2024-03-13T18:42:04,0.1,"Rain, Overcast",Cloudy skies throughout the day with rain clearing later.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-03-14,16.5,8.3,12.5,16.5,6,12,8.6,78.1,0.005,100,4.17,rain,0,0,41.3,27.7,194.2,1010.4,94.5,32.4,116.8,10.2,5,,2024-03-14T06:56:03,2024-03-14T18:43:51,0.14,"Rain, Overcast",Cloudy skies throughout the day with morning rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-03-15,14.2,10.5,12.5,14.2,10.5,12.5,9.8,83.9,0.701,100,25,rain,0,0,57.7,41.3,222.4,1006.2,90.3,21.7,56.8,4.8,2,,2024-03-15T06:53:43,2024-03-15T18:45:37,0.17,"Rain, Overcast",Cloudy skies throughout the day with rain clearing later.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-03-16,11,3.2,7.8,11,1.4,5.8,4.4,79.7,0.87,100,25,rain,0,0,41.1,27.2,296.5,1018.3,99.1,29.7,55.3,4.7,2,,2024-03-16T06:51:23,2024-03-16T18:47:23,0.2,"Rain, Overcast",Cloudy skies throughout the day with rain clearing later.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-03-17,12.8,4.6,9.3,12.8,2.9,8.1,6.6,83.4,2.806,100,25,rain,0,0,35.1,24.2,142.9,1019.3,99.7,26.2,64.8,5.4,3,,2024-03-17T06:49:03,2024-03-17T18:49:09,0.25,"Rain, Overcast",Cloudy skies throughout the day with late afternoon rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-03-18,13.3,7.2,10.7,13.3,6,10.4,8.4,86.6,0.944,100,12.5,rain,0,0,28.1,19.2,236.5,1015.6,87.9,15.5,129.7,11.2,6,,2024-03-18T06:46:43,2024-03-18T18:50:55,0.27,"Rain, Partially cloudy",Partly cloudy throughout the day with rain clearing later.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-03-19,15.2,7.9,11.4,15.2,6,11,8.2,81.6,0,0,0,,0,0,38.1,27.7,211.3,1017.5,94.3,28.8,91.5,8,6,,2024-03-19T06:44:22,2024-03-19T18:52:41,0.3,Overcast,Cloudy skies throughout the day.,cloudy,"06260099999,C0449,06257099999,EHAM,D3248,EHRD,06348099999,06249099999,06240099999,EHLE,EHKD,06269099999,06344099999" +"Amsterdam,Netherlands",2024-03-20,15,8,11.2,15,6.8,10.9,8.8,85.7,0.021,100,8.33,rain,0,0,17.8,9.9,236.3,1019,99.7,21.7,76.6,6.7,3,,2024-03-20T06:42:01,2024-03-20T18:54:26,0.34,"Rain, Overcast",Cloudy skies throughout the day with rain in the morning and afternoon.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-03-21,10.5,7.5,8.9,10.5,4.5,7.1,7.1,88.3,0.014,100,4.17,rain,0,0,29.2,20.9,267.2,1023.4,97.9,24.7,48,4,2,,2024-03-21T06:39:41,2024-03-21T18:56:12,0.37,"Rain, Overcast",Cloudy skies throughout the day with morning rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-03-22,10.8,7.6,9.7,10.8,6.2,8.7,7.9,88.8,0.634,100,37.5,rain,0,0,41.6,27.1,254.5,1016.2,100,10.5,27.6,2.4,1,,2024-03-22T06:37:20,2024-03-22T18:57:57,0.41,"Rain, Overcast",Cloudy skies throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-03-23,8.4,5,6.9,5.9,2.1,3.2,3.1,77.2,2.822,100,54.17,rain,0,0,56.3,40.9,259,1008.1,64,26.7,65.5,5.7,5,,2024-03-23T06:34:59,2024-03-23T18:59:42,0.44,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-03-24,8.1,5.3,6.9,3.9,1.1,2.6,4.5,84.8,7.443,100,91.67,rain,0,0,63,41.3,281.8,1004,93.2,15.7,55.7,4.9,3,,2024-03-24T06:32:38,2024-03-24T19:01:27,0.47,"Rain, Overcast",Cloudy skies throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-03-25,11.4,2.8,7.4,11.4,-0.1,5.8,2.5,73.1,0.855,100,8.33,rain,0,0,28.2,20.2,150.4,1004.7,69.7,26.1,132,11.4,6,,2024-03-25T06:30:18,2024-03-25T19:03:12,0.5,"Rain, Partially cloudy",Partly cloudy throughout the day with rain clearing later.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-03-26,11,4.8,8.3,11,1.1,6.2,3.2,70.2,0.1,100,4.17,rain,0,0,36,21.6,119.1,991.7,96.9,29.6,96.5,8.3,3,,2024-03-26T06:27:57,2024-03-26T19:04:56,0.54,"Rain, Overcast",Cloudy skies throughout the day with late afternoon rain.,rain,"06260099999,06356099999,C0449,06257099999,EHAM,D3248,EHRD,06348099999,06249099999,06240099999,EHLE,EHKD,06275099999,06269099999,06344099999" +"Amsterdam,Netherlands",2024-03-27,12.3,7.1,10,12.3,4.6,9,6,76.7,0.014,100,8.33,rain,0,0,37.4,23.2,175.6,985.5,86.8,24.3,84.4,7.2,4,,2024-03-27T06:25:36,2024-03-27T19:06:41,0.57,"Rain, Partially cloudy",Partly cloudy throughout the day with rain in the morning and afternoon.,rain,"06356099999,06260099999,C0449,EHAM,06257099999,D3248,EHRD,06348099999,06249099999,06240099999,EHLE,EHKD,06269099999,06344099999" +"Amsterdam,Netherlands",2024-03-28,11.8,7,8.8,11.8,3.4,6.8,4.4,74.5,1.6,100,33.33,rain,0,0,63,40.9,178.9,984.6,76.3,10,84.5,7.3,7,,2024-03-28T06:23:16,2024-03-28T19:08:26,0.61,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"D3248,EHRD,C0449,EHLE,EHKD,EHAM" +"Amsterdam,Netherlands",2024-03-29,13.9,8,10.7,13.9,4.5,9.1,5.5,70.6,2.4,100,41.67,rain,0,0,51.8,29.8,186,991.4,59.5,10,117.6,10,4,,2024-03-29T06:20:56,2024-03-29T19:10:10,0.64,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"D3248,EHRD,C0449,EHLE,EHKD,EHAM" +"Amsterdam,Netherlands",2024-03-30,11.8,5,9,11.8,3.3,7.8,7.9,93.3,4.8,100,50,rain,0,0,25.6,22.6,200.1,996.3,78,8.8,27.3,2.3,1,,2024-03-30T06:18:35,2024-03-30T19:11:55,0.68,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"D3248,EHRD,C0449,EHLE,EHKD,EHAM" +"Amsterdam,Netherlands",2024-03-31,14.9,3.8,9.3,14.9,1.4,8,7.9,91.9,5.8,100,54.17,rain,0,0,38.5,21.9,82.6,996.8,69.4,8.5,69.3,5.7,5,,2024-03-31T07:16:16,2024-03-31T20:13:39,0.71,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"D3248,EHRD,C0449,EHLE,EHKD,EHAM" +"Amsterdam,Netherlands",2024-04-01,12.8,7.1,10.1,12.8,5.6,9.3,7.7,86.1,0.1,100,4.17,rain,0,0,40.3,30.6,218.9,994.9,50.9,14.7,64,5.8,3,,2024-04-01T07:13:56,2024-04-01T20:15:24,0.75,"Rain, Partially cloudy",Partly cloudy throughout the day with afternoon rain.,rain,"D3248,06260099999,EHRD,06348099999,06249099999,C0449,06240099999,EHLE,EHKD,EHAM,06257099999" +"Amsterdam,Netherlands",2024-04-02,12,7.5,9.7,12,4.6,8.3,7.5,86.3,1.6,100,33.33,rain,0,0,55.4,29.3,218.6,1003.4,48.4,18.4,90.5,7.8,5,,2024-04-02T07:11:37,2024-04-02T20:17:08,0.75,"Rain, Partially cloudy",Partly cloudy throughout the day with rain in the morning and afternoon.,rain,"06260099999,06356099999,C0449,06257099999,EHAM,D3248,EHRD,06348099999,06249099999,06240099999,EHLE,EHKD,06269099999,06344099999" +"Amsterdam,Netherlands",2024-04-03,13.6,9.3,10.9,13.6,7.3,10.1,9.2,89.2,3.521,100,62.5,rain,0,0,62.8,37.6,202.4,1003.4,99.1,21.8,53.3,4.6,3,,2024-04-03T07:09:18,2024-04-03T20:18:52,0.82,"Rain, Overcast",Cloudy skies throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-04-04,13.9,9.1,11.5,13.9,6.2,11,9.1,85.4,4.072,100,45.83,rain,0,0,70.6,47.5,227.4,1004.1,98.1,15.7,111.5,9.5,7,,2024-04-04T07:06:59,2024-04-04T20:20:37,0.85,"Rain, Overcast",Cloudy skies throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-04-05,16.2,11.2,13.2,16.2,11.2,13.2,10.3,83.4,2.471,100,33.33,rain,0,0,66.6,46.8,213.1,1007.4,89.7,20.9,104.5,9.1,6,,2024-04-05T07:04:41,2024-04-05T20:22:21,0.88,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-04-06,23.2,11.3,16.8,23.2,11.3,16.8,10.9,70.7,0.495,100,4.17,rain,0,0,54.6,34.1,174.1,1008.4,97.1,34,143.6,12.5,6,,2024-04-06T07:02:23,2024-04-06T20:24:05,0.92,"Rain, Overcast",Cloudy skies throughout the day with morning rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-04-07,18.4,13.1,15.5,18.4,13.1,15.5,9.3,67.2,0.556,100,20.83,rain,0,0,53.1,37.6,221.2,1011.2,79.3,35.4,183.9,15.9,8,,2024-04-07T07:00:05,2024-04-07T20:25:50,0.95,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-04-08,18.5,10.8,14.6,18.5,10.8,14.6,10.9,79.2,0.068,100,20.83,rain,0,0,25,17.2,140.9,1008.3,89.5,30,113.5,9.6,5,,2024-04-08T06:57:48,2024-04-08T20:27:34,0,"Rain, Partially cloudy",Partly cloudy throughout the day with rain clearing later.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-04-09,15.8,10.3,11.8,15.8,10.3,11.8,8.1,78.2,4.532,100,41.67,rain,0,0,69.8,48,216.7,1007.4,99.9,30.2,54.2,4.8,2,,2024-04-09T06:55:32,2024-04-09T20:29:18,0.02,"Rain, Overcast",Cloudy skies throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06257099999" +"Amsterdam,Netherlands",2024-04-10,14.1,8.3,11.2,14.1,5.9,10.2,5,67.3,0.447,100,25,rain,0,0,61.9,36.1,250,1025.2,76.9,29.4,173.5,15,7,,2024-04-10T06:53:16,2024-04-10T20:31:02,0.05,"Rain, Partially cloudy",Partly cloudy throughout the day with rain clearing later.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-04-11,14.4,11.9,13,14.4,11.9,13,10.3,84.5,0.15,100,20.83,rain,0,0,48.9,33.3,221.4,1029.3,100,25.7,46.4,4,3,,2024-04-11T06:51:00,2024-04-11T20:32:47,0.09,"Rain, Overcast",Cloudy skies throughout the day with rain in the morning and afternoon.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-04-12,17.3,12.9,14.9,17.3,12.9,14.9,11,78.4,0.023,100,4.17,rain,0,0,55.2,35.3,230.2,1029.1,100,26.6,150.5,12.9,6,,2024-04-12T06:48:45,2024-04-12T20:34:31,0.12,"Rain, Overcast",Cloudy skies throughout the day with morning rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-04-13,20.9,11.5,16.3,20.9,11.5,16.3,11,71.6,0,0,0,,0,0,53.2,35,230.4,1022.7,99.9,38.9,181.3,15.7,6,,2024-04-13T06:46:31,2024-04-13T20:36:15,0.15,Overcast,Cloudy skies throughout the day.,cloudy,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-04-14,13.7,8.8,11.3,13.7,7.8,11.1,5,65.5,0,0,0,,0,0,53.1,27.5,279.6,1021.2,99.8,31.5,190.3,16.8,7,,2024-04-14T06:44:17,2024-04-14T20:37:59,0.19,Overcast,Cloudy skies throughout the day.,cloudy,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-04-15,10.2,5.1,8.3,10.2,0.4,5.2,4.9,79.2,7.703,100,50,rain,0,0,87,45.7,238,1006.1,93.5,26.6,65.5,5.7,4,,2024-04-15T06:42:05,2024-04-15T20:39:44,0.25,"Rain, Overcast",Cloudy skies throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-04-16,10.6,5.6,8.4,10.6,0,4.9,5.1,80.2,11.676,100,58.33,rain,0,0,66.1,44,303.1,1003.4,86.1,21.8,91.6,7.9,4,,2024-04-16T06:39:53,2024-04-16T20:41:28,0.25,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-04-17,8.6,4,6.1,7.8,0.3,4.2,3.2,82,10.732,100,62.5,rain,0,0,39.2,23,317.8,1011.9,70.5,32.3,116,10.1,5,,2024-04-17T06:37:41,2024-04-17T20:43:12,0.28,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,06240099999,C0449,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-04-18,10.3,1.8,6.9,10.3,-1,4.8,2.9,77.7,0.122,100,16.67,rain,0,0,34.7,23.7,295.4,1018.5,65,34.9,151.2,13.1,6,,2024-04-18T06:35:31,2024-04-18T20:44:56,0.32,"Rain, Partially cloudy",Partly cloudy throughout the day with rain clearing later.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-04-19,10.3,6.7,8.6,10.3,2.4,4.9,6.4,86,9.469,100,95.83,rain,0,0,66.4,44.9,302,1010.9,98.5,14.6,114.1,9.7,8,,2024-04-19T06:33:21,2024-04-19T20:46:40,0.35,"Rain, Overcast",Cloudy skies throughout the day with a chance of rain throughout the day.,rain,"D3248,06260099999,06348099999,06249099999,06240099999,C0449,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-04-20,9.7,5.9,7.4,6.1,1.8,3.8,3.5,76.1,2.218,100,54.17,rain,0,0,58.2,34.2,334.4,1021.1,91.6,24.4,172,14.9,8,,2024-04-20T06:31:12,2024-04-20T20:48:24,0.38,"Rain, Overcast",Cloudy skies throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-04-21,10.5,3.2,6.8,10.5,-1.6,3.6,2,72.1,1.675,100,20.83,rain,0,0,54.3,26.2,19.7,1025.1,48.6,34.8,234.3,20.3,8,,2024-04-21T06:29:04,2024-04-21T20:50:08,0.42,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-04-22,9.8,1.9,6,7.1,-0.9,3.5,0.2,68.6,0.269,100,12.5,rain,0,0,36.4,24.5,18.9,1025.9,75.8,38.5,195.3,16.8,8,,2024-04-22T06:26:57,2024-04-22T20:51:52,0.45,"Rain, Partially cloudy",Partly cloudy throughout the day with rain in the morning and afternoon.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-04-23,9.5,-0.3,5.4,9.3,-1.4,3.5,0.1,70.6,1.639,100,20.83,rain,0,0,39.1,29.7,304.8,1020.5,68.7,36.9,172.4,14.9,8,,2024-04-23T06:24:52,2024-04-23T20:53:35,0.48,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-04-24,9.4,4.6,6.4,5.5,0.8,2.9,2.9,78.6,2.51,100,70.83,rain,0,0,56.1,34.2,323.9,1010.6,86,31,117.7,10.2,7,,2024-04-24T06:22:47,2024-04-24T20:55:19,0.5,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-04-25,9.8,3.6,6.2,7,1.6,3.6,2.9,80.1,5.41,100,54.17,rain,0,0,41.3,29.3,225.5,1004.9,91.7,29.7,83.8,7.3,3,,2024-04-25T06:20:43,2024-04-25T20:57:02,0.55,"Rain, Overcast",Cloudy skies throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-04-26,11,4.3,7.8,11,1.3,6.1,3.6,75.5,1.107,100,33.33,rain,0,0,31,20.1,256.5,1003.2,87.7,34.3,176,15.2,7,,2024-04-26T06:18:40,2024-04-26T20:58:46,0.59,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-04-27,15,5.7,10.6,15,4.4,9.8,8,85,2.916,100,37.5,rain,0,0,31,19.2,109.8,1005,99.3,29.7,103,8.9,4,,2024-04-27T06:16:39,2024-04-27T21:00:29,0.62,"Rain, Overcast",Cloudy skies throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-04-28,15.3,7.3,12.8,15.3,4.9,12.6,7.8,72.1,0.788,100,12.5,rain,0,0,69.5,47.2,184.3,1006.3,90.7,41.7,88.7,7.7,4,,2024-04-28T06:14:38,2024-04-28T21:02:12,0.66,"Rain, Overcast",Cloudy skies throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-04-29,18.7,5.6,12.4,18.7,2.9,11.6,7.1,72.4,0.023,100,8.33,rain,0,0,39.4,26.4,172.5,1018.1,35.8,33.7,262.1,22.8,9,,2024-04-29T06:12:39,2024-04-29T21:03:54,0.69,"Rain, Partially cloudy",Becoming cloudy in the afternoon with rain clearing later.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-04-30,20.4,11.9,15.7,20.4,11.9,15.7,11.3,76,0,0,0,,0,0,25.9,19.9,138.3,1015.5,91.5,36.7,180.8,15.4,8,,2024-04-30T06:10:42,2024-04-30T21:05:36,0.73,Overcast,Cloudy skies throughout the day.,cloudy,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-05-01,22.3,13.9,18.1,22.3,13.9,18.1,14.6,80.4,0.7,100,8.33,rain,0,0,30.2,19,5.6,1007.3,53.9,10,237.8,20.6,7,,2024-05-01T06:08:45,2024-05-01T21:07:18,0.75,"Rain, Partially cloudy",Partly cloudy throughout the day with morning rain.,rain,"D3248,EHRD,C0449,EHLE,EHKD,EHAM" +"Amsterdam,Netherlands",2024-05-02,24.4,13.1,17.6,24.4,13.1,17.6,13.3,77.1,0.85,100,29.17,rain,0,0,36.3,24.6,37.4,1000.1,75.7,24.9,228.7,19.8,7,,2024-05-02T06:06:50,2024-05-02T21:09:00,0.8,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-05-03,13.6,9.3,12,13.6,7.7,11.9,10.7,91.5,3.878,100,58.33,rain,0,0,48,33.5,227.4,1007.2,86.7,15.2,33,2.7,1,,2024-05-03T06:04:56,2024-05-03T21:10:42,0.83,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"D3248,06260099999,06348099999,06249099999,06240099999,C0449,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-05-04,16.6,6.9,11.5,16.6,5.1,11,8.5,83,4.036,100,25,rain,0,0,47.5,22.2,148,1013.2,92.1,26.6,158.7,13.9,6,,2024-05-04T06:03:04,2024-05-04T21:12:22,0.87,"Rain, Overcast",Cloudy skies throughout the day with rain.,rain,"D3248,06260099999,06348099999,06249099999,06240099999,C0449,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-05-05,15.7,9.3,12.5,15.7,7.3,12.1,8.9,80.5,6.25,100,33.33,rain,0,0,31.2,18,295.4,1009,75.2,33,255.7,22.2,8,,2024-05-05T06:01:14,2024-05-05T21:14:03,0.9,"Rain, Partially cloudy",Partly cloudy throughout the day with rain clearing later.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-05-06,18.1,8.5,13.9,18.1,7.4,13.6,9.3,75.5,0,0,0,,0,0,25.6,17.6,69.7,1008.7,83.8,32.1,165,14.3,7,,2024-05-06T05:59:25,2024-05-06T21:15:43,0.94,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-05-07,18,10.6,14.1,18,10.6,14.1,9.5,74.8,0,0,0,,0,0,35.2,26.1,7.3,1018.8,71.5,28.5,194.7,16.7,8,,2024-05-07T05:57:37,2024-05-07T21:17:22,0.97,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"06260099999,C0449,06257099999,EHAM,D3248,EHRD,06348099999,06249099999,06240099999,EHLE,EHKD,06269099999,06344099999" +"Amsterdam,Netherlands",2024-05-08,15,10.2,12.5,15,10.2,12.5,9.9,84.7,0.058,100,8.33,rain,0,0,23,15.6,340.7,1027.2,89.6,25.1,101.7,8.7,4,,2024-05-08T05:55:52,2024-05-08T21:19:01,0,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"06260099999,06356099999,C0449,06257099999,EHAM,06267099999,D3248,06273099999,EHRD,06348099999,06249099999,06240099999,EHLE,EHKD,06269099999,06235099999,06344099999" +"Amsterdam,Netherlands",2024-05-09,17.2,6.3,12.3,17.2,6.3,12.3,9.2,82.3,0,0,0,,0,0,21.3,13.6,301.7,1027.6,50.8,15.5,215.2,18.6,8,,2024-05-09T05:54:08,2024-05-09T21:20:40,0.04,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-05-10,19.2,8.5,14.4,19.2,8.5,14.4,10.5,79,0,0,0,,0,0,21.7,17,38,1024.5,80,15.9,237.7,20.5,8,,2024-05-10T05:52:25,2024-05-10T21:22:17,0.07,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-05-11,21.7,11.1,16.1,21.7,11.1,16.1,11.4,75.9,0,0,0,,0,0,27.4,18.7,57.5,1022.5,93.5,23.2,247.8,21.3,8,,2024-05-11T05:50:45,2024-05-11T21:23:54,0.11,Overcast,Cloudy skies throughout the day.,cloudy,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-05-12,25,14,19.5,25,14,19.5,11,59.2,0.065,100,4.17,rain,0,0,38.3,24,95.7,1016.1,99.4,37.3,271.5,23.4,7,,2024-05-12T05:49:06,2024-05-12T21:25:30,0.14,"Rain, Overcast",Cloudy skies throughout the day with early morning rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-05-13,22.4,16.8,19.6,22.4,16.8,19.6,14.8,74.4,0.461,100,4.17,rain,0,0,34.6,20.8,147.4,1010,74.1,33.4,170,14.7,7,,2024-05-13T05:47:30,2024-05-13T21:27:05,0.17,"Rain, Partially cloudy",Partly cloudy throughout the day with late afternoon rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-05-14,26.6,15.6,20.2,26.6,15.6,20.2,14.4,72.2,0.962,100,12.5,rain,0,0,48.3,31.2,131.1,1004.9,41.3,31.2,249.6,21.7,8,,2024-05-14T05:45:55,2024-05-14T21:28:40,0.2,"Rain, Partially cloudy",Becoming cloudy in the afternoon with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-05-15,19.8,12.8,16,19.8,12.8,16,14.3,90.6,6.274,100,37.5,rain,0,0,32.8,19,302.6,1006.1,81.5,12.4,131,11.4,7,,2024-05-15T05:44:22,2024-05-15T21:30:13,0.25,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-05-16,20.2,13.7,16,20.2,13.7,16,14.1,88.9,3.508,100,20.83,rain,0,0,31.5,26.6,246.5,1004.8,99.7,13.7,155,13.3,8,,2024-05-16T05:42:52,2024-05-16T21:31:46,0.27,"Rain, Overcast",Cloudy skies throughout the day with rain clearing later.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-05-17,17.5,13.2,14.7,17.5,13.2,14.7,13.5,92.8,1.518,100,16.67,rain,0,0,30.7,22.7,318,1008.4,98.6,8.7,143.7,12.4,8,,2024-05-17T05:41:23,2024-05-17T21:33:17,0.3,"Rain, Overcast",Cloudy skies throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-05-18,22.3,13.3,17.5,22.3,13.3,17.5,12.9,77.2,0.214,100,8.33,rain,0,0,27.8,16.6,321.3,1010.3,67.6,18.3,273.8,23.6,8,,2024-05-18T05:39:57,2024-05-18T21:34:47,0.33,"Rain, Partially cloudy",Partly cloudy throughout the day with rain clearing later.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-05-19,21.5,11.3,17,21.5,11.3,17,11.8,74.2,0.717,100,4.17,rain,0,0,41.8,27.9,3.5,1011.2,22.3,22.8,268.6,23.2,8,,2024-05-19T05:38:33,2024-05-19T21:36:16,0.37,"Rain, Partially cloudy",Becoming cloudy in the afternoon with morning rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-05-20,19,13.1,15.7,19,13.1,15.7,13.7,88.2,2.89,100,20.83,rain,0,0,30.8,17.4,353.5,1011.5,89.7,18.8,109.6,9.4,6,,2024-05-20T05:37:11,2024-05-20T21:37:44,0.4,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-05-21,23.1,12.4,17.4,23.1,12.4,17.4,14.1,83.1,8.497,100,29.17,rain,0,0,53.1,25.9,52.3,1008.1,78.8,20.9,207.5,17.8,7,,2024-05-21T05:35:52,2024-05-21T21:39:11,0.43,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-05-22,18.1,14,15.5,18.1,14,15.5,13,85.7,23.183,100,37.5,rain,0,0,46.3,30,223.5,1006.8,95.3,23.1,85.3,7.3,7,,2024-05-22T05:34:35,2024-05-22T21:40:36,0.46,"Rain, Overcast",Cloudy skies throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-05-23,18.1,13.2,15.5,18.1,13.2,15.5,11.6,78.1,0,0,0,,0,0,41.7,26.9,225.1,1013.5,58.9,28.8,183.6,15.7,8,,2024-05-23T05:33:21,2024-05-23T21:41:59,0.5,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-05-24,18.7,11.4,14.9,18.7,11.4,14.9,12.8,87.9,5.086,100,37.5,rain,0,0,27.1,17.2,22.9,1018.3,96.7,14.4,113.4,10,4,,2024-05-24T05:32:09,2024-05-24T21:43:21,0.53,"Rain, Overcast",Cloudy skies throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-05-25,17.7,11.6,15.2,17.7,11.6,15.2,13.8,91.7,31.269,100,45.83,rain,0,0,32.5,21.4,261.1,1016.3,85.4,17.5,98.3,8.3,3,,2024-05-25T05:30:59,2024-05-25T21:44:42,0.57,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-05-26,20.4,11.1,15.6,20.4,11.1,15.6,12.5,82.5,3.309,100,20.83,rain,0,0,48.1,21.3,154.4,1014.7,74.6,24.1,174.7,15.1,7,,2024-05-26T05:29:52,2024-05-26T21:46:01,0.6,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"06260099999,D3248,06356099999,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-05-27,17.3,12.7,14.7,17.3,12.7,14.7,10.1,74.9,1.316,100,20.83,rain,0,0,34.5,23.9,232.8,1016,84.4,30.4,219.9,19.2,9,,2024-05-27T05:28:48,2024-05-27T21:47:18,0.64,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-05-28,18.7,10.8,14.2,18.7,10.8,14.2,11.1,82.4,6.213,100,37.5,rain,0,0,44.4,26,198.9,1015.5,84.6,23.1,189,16.4,7,,2024-05-28T05:27:46,2024-05-28T21:48:33,0.67,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-05-29,17,13.9,15.2,17,13.9,15.2,13,87.1,22.194,100,66.67,rain,0,0,45.2,29.9,242,1008.1,88.9,19.3,151.7,13.2,6,,2024-05-29T05:26:48,2024-05-29T21:49:46,0.71,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-05-30,16.6,12.9,14.3,16.6,12.9,14.3,12,86.3,0.621,100,8.33,rain,0,0,32.1,23.9,261.7,1006.6,97.5,24.4,121.6,10.5,5,,2024-05-30T05:25:52,2024-05-30T21:50:57,0.75,"Rain, Overcast",Cloudy skies throughout the day with rain in the morning and afternoon.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-05-31,18.5,12.9,15.4,18.5,12.9,15.4,12.5,83.7,1.511,100,33.33,rain,0,0,42.2,27.7,339.3,1011.5,90.3,24.8,216.6,18.7,9,,2024-05-31T05:24:59,2024-05-31T21:52:07,0.78,"Rain, Overcast",Cloudy skies throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06257099999" +"Amsterdam,Netherlands",2024-06-01,15.7,13.8,14.8,15.7,13.8,14.8,13.6,92.9,2.332,100,33.33,rain,0,0,42.1,26.4,350.1,1018,100,9.3,51,4.5,2,,2024-06-01T05:24:08,2024-06-01T21:53:14,0.82,"Rain, Overcast",Cloudy skies throughout the day with rain clearing later.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-06-02,16.4,12.8,14,16.4,12.8,14,9.5,74.5,0.559,100,16.67,rain,0,0,38.4,23.5,342.8,1023.5,94.8,20.9,198.6,17.4,9,,2024-06-02T05:23:21,2024-06-02T21:54:19,0.85,"Rain, Overcast",Cloudy skies throughout the day with rain in the morning and afternoon.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-06-03,16.3,9.7,13.7,16.3,8.8,13.7,10.6,81.9,0,0,0,,0,0,24.9,14.1,288.4,1020.7,92.4,27.4,141.3,12.2,5,,2024-06-03T05:22:37,2024-06-03T21:55:21,0.89,Overcast,Cloudy skies throughout the day.,cloudy,"06260099999,C0449,06257099999,EHAM,D3248,EHRD,06348099999,06249099999,06240099999,EHLE,EHKD,06269099999,06344099999" +"Amsterdam,Netherlands",2024-06-04,20.4,11.8,16,20.4,11.8,16,13,83.4,0,0,0,,0,0,46.4,27,214.9,1012.6,99.3,16.8,111.2,9.6,6,,2024-06-04T05:21:55,2024-06-04T21:56:22,0.92,Overcast,Cloudy skies throughout the day.,cloudy,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-06-05,14.8,10.8,13.1,14.8,10.8,13.1,7.4,69.6,3.409,100,12.5,rain,0,0,47.7,34.9,268.9,1012,54.7,26.5,302,26.2,9,,2024-06-05T05:21:17,2024-06-05T21:57:19,0.96,"Rain, Partially cloudy",Partly cloudy throughout the day with rain clearing later.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-06-06,16.8,9.7,13.2,16.8,8.3,13,7.3,68.9,0,0,0,,0,0,37.6,20.6,254.7,1016.8,79.7,25.1,153.3,13.1,7,,2024-06-06T05:20:42,2024-06-06T21:58:15,0,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-06-07,17.8,10,14.3,17.8,10,14.3,8.3,69.3,0,0,0,,0,0,41.2,26.3,241.7,1017.9,45.9,22.3,257.1,22.3,10,,2024-06-07T05:20:09,2024-06-07T21:59:08,0.03,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-06-08,17.6,10.2,13.7,17.6,10.2,13.7,10.3,80.3,0.373,100,8.33,rain,0,0,45.4,33.5,243.3,1012.5,65,24.7,189.6,16.4,9,,2024-06-08T05:19:40,2024-06-08T21:59:58,0.06,"Rain, Partially cloudy",Partly cloudy throughout the day with late afternoon rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-06-09,15.3,9,12.8,15.3,7.4,12.5,7.9,73.3,0.62,100,12.5,rain,0,0,46.6,33.8,261.7,1010.9,53.2,32.5,169.3,14.5,7,,2024-06-09T05:19:14,2024-06-09T22:00:45,0.09,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-06-10,14.1,9.4,11.6,14.1,5.7,11.2,10.1,90.4,24.004,100,62.5,rain,0,0,63.1,32.2,274,1005.8,99.9,20.3,72.5,6.4,6,,2024-06-10T05:18:51,2024-06-10T22:01:30,0.13,"Rain, Overcast",Cloudy skies throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-06-11,14.9,9.5,11.9,14.9,7.6,11.5,8.1,78.2,8.244,100,58.33,rain,0,0,41.9,30.4,289.6,1014.7,74.3,30.2,200,17.3,9,,2024-06-11T05:18:32,2024-06-11T22:02:12,0.16,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-06-12,14.5,9.5,11.5,14.5,8.5,11.4,7.8,78.4,2.397,100,37.5,rain,0,0,39.7,25,297.8,1019.4,98.2,33.7,150.4,13,4,,2024-06-12T05:18:15,2024-06-12T22:02:51,0.19,"Rain, Overcast",Cloudy skies throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-06-13,17.5,10.4,14,17.5,10.4,14,8.8,71.8,0.346,100,4.17,rain,0,0,38.6,24.6,228.5,1015.8,89.6,31.8,152.1,13.4,7,,2024-06-13T05:18:02,2024-06-13T22:03:28,0.22,"Rain, Partially cloudy",Partly cloudy throughout the day with morning rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-06-14,17.9,13,15,17.9,13,15,11.9,82.1,1.596,100,25,rain,0,0,39.7,27.3,175.7,1005.8,95.6,29.6,97.8,8.4,3,,2024-06-14T05:17:52,2024-06-14T22:04:01,0.25,"Rain, Overcast",Cloudy skies throughout the day with rain.,rain,"06260099999,D3248,06356099999,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-06-15,17.2,11.9,14.2,17.2,11.9,14.2,11.2,82.9,9.259,100,62.5,rain,0,0,65.6,41.1,208.8,1001.5,77.5,23.7,159.1,13.7,6,,2024-06-15T05:17:45,2024-06-15T22:04:31,0.29,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-06-16,17.2,12.6,14.9,17.2,12.6,14.9,11.3,79.7,2.728,100,37.5,rain,0,0,47.4,30.3,207,1004.5,85.4,34.7,180,15.5,9,,2024-06-16T05:17:42,2024-06-16T22:04:58,0.32,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"D3248,06260099999,06348099999,06249099999,06240099999,C0449,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-06-17,19.1,12.3,16,19.1,12.3,16,10.8,72.5,0.555,100,4.17,rain,0,0,38.5,25.3,230.9,1010.4,69.9,29.8,219.5,18.8,9,,2024-06-17T05:17:41,2024-06-17T22:05:23,0.35,"Rain, Partially cloudy",Partly cloudy throughout the day with morning rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-06-18,18.4,10.6,15.3,18.4,10.6,15.3,12.3,82.8,0.608,100,29.17,rain,0,0,22.6,10.5,77.9,1013.9,99.6,29,92.8,8.2,3,,2024-06-18T05:17:44,2024-06-18T22:05:44,0.38,"Rain, Overcast",Cloudy skies throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-06-19,18.8,12.3,15.7,18.8,12.3,15.7,11.6,78.2,0.178,100,4.17,rain,0,0,34,21,4.7,1019,50,26.7,207.2,17.8,9,,2024-06-19T05:17:50,2024-06-19T22:06:02,0.42,"Rain, Partially cloudy",Partly cloudy throughout the day with morning rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-06-20,20.4,9.7,15.3,20.4,8.9,15.3,11.7,79.9,0,0,0,,0,0,27.4,15.3,47.3,1020.4,78.4,34.7,182,15.8,7,,2024-06-20T05:17:59,2024-06-20T22:06:16,0.45,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999,EHAM" +"Amsterdam,Netherlands",2024-06-21,18.7,13.4,15.9,18.7,13.4,15.9,13.9,88.1,0.941,100,20.83,rain,0,0,27.3,15.9,328,1013.2,98,21.5,80.2,6.8,4,,2024-06-21T05:18:12,2024-06-21T22:06:28,0.48,"Rain, Overcast",Cloudy skies throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-06-22,19.3,13.2,16.3,19.3,13.2,16.3,13.1,81.8,0.335,100,4.17,rain,0,0,35.6,23.7,249.4,1012.6,67.1,23.2,268.5,23.1,10,,2024-06-22T05:18:27,2024-06-22T22:06:36,0.5,"Rain, Partially cloudy",Partly cloudy throughout the day with morning rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-06-23,22.5,12.8,17.9,22.5,12.8,17.9,13.4,76.7,0,0,0,,0,0,28.1,17.7,265.5,1018.9,26.8,27.8,279.5,24.3,9,,2024-06-23T05:18:46,2024-06-23T22:06:41,0.55,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"D3248,06260099999,06348099999,06249099999,06240099999,C0449,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-06-24,24.6,11.8,19.1,24.6,11.8,19.1,14,74.7,0,0,0,,0,0,21.3,13.7,76.8,1020.3,10.9,28.5,308.9,26.5,10,,2024-06-24T05:19:07,2024-06-24T22:06:43,0.59,Clear,Clear conditions throughout the day.,clear-day,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-06-25,27.1,15.9,22.1,27.6,15.9,22.2,16,70.2,0,0,0,,0,0,31.1,20.3,51.6,1016.2,4.1,34.8,288,25,8,,2024-06-25T05:19:32,2024-06-25T22:06:42,0.62,Clear,Clear conditions throughout the day.,clear-day,"06260099999,D3248,06348099999,06249099999,06240099999,C0449,06269099999,06257099999" +"Amsterdam,Netherlands",2024-06-26,28.8,18.6,24.1,28.8,18.6,24.2,16.8,66.1,0,0,0,,0,0,24.9,14.5,54.1,1011.5,0.6,32.5,293.2,25.4,8,,2024-06-26T05:20:00,2024-06-26T22:06:37,0.66,Clear,Clear conditions throughout the day.,clear-day,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-06-27,26.6,18.5,22.1,26.6,18.5,22.1,16.3,71.4,0,0,0,,0,0,41.5,32.1,248.3,1008.3,47.3,27,257,22.2,8,,2024-06-27T05:20:30,2024-06-27T22:06:29,0.69,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-06-28,19.3,15.4,17.5,19.3,15.4,17.5,11.7,69,0,0,0,,0,0,45,30.4,250,1014.5,66,30.1,212.2,18.4,10,,2024-06-28T05:21:04,2024-06-28T22:06:18,0.75,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"D3248,06260099999,06348099999,06249099999,06240099999,C0449,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-06-29,22.9,11.5,17.8,22.9,11.5,17.8,12.4,72.5,0.066,100,8.33,rain,0,0,35.4,19.2,340.9,1014.6,83.1,27.2,292.3,25.1,9,,2024-06-29T05:21:40,2024-06-29T22:06:04,0.76,"Rain, Partially cloudy",Partly cloudy throughout the day with late afternoon rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-06-30,19.9,14.7,17.6,19.9,14.7,17.6,13.6,78.3,0.507,100,12.5,rain,0,0,35,25,315.4,1009.9,99.6,36.1,187.2,16.2,8,,2024-06-30T05:22:19,2024-06-30T22:05:46,0.8,"Rain, Overcast",Cloudy skies throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-07-01,18,12.9,15.8,18,12.9,15.8,11.4,75.4,0.203,100,8.33,rain,0,0,40.8,24.6,285.5,1015.2,87.5,37.3,172.7,14.8,9,,2024-07-01T05:23:01,2024-07-01T22:05:25,0.84,"Rain, Partially cloudy",Partly cloudy throughout the day with rain clearing later.,rain,"D3248,06260099999,06348099999,06249099999,06240099999,C0449,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-07-02,17.3,12.8,15.3,17.3,12.8,15.3,11.6,79.8,3.898,100,37.5,rain,0,0,38,26.8,308.4,1014.9,96.4,25.7,147.6,12.8,6,,2024-07-02T05:23:46,2024-07-02T22:05:01,0.87,"Rain, Overcast",Cloudy skies throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-07-03,16.5,10.6,13.7,16.5,10.6,13.7,10.5,81.5,2.363,100,45.83,rain,0,0,31.1,23,234.1,1009.9,92.6,30,133.4,11.5,7,,2024-07-03T05:24:33,2024-07-03T22:04:34,0.91,"Rain, Overcast",Cloudy skies throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-07-04,16.9,14.2,15.8,16.9,14.2,15.8,10.3,71.2,14.628,100,33.33,rain,0,0,54.6,33.5,256.6,1006.5,72.6,27.1,228.9,19.9,10,,2024-07-04T05:25:23,2024-07-04T22:04:03,0.94,"Rain, Partially cloudy",Partly cloudy throughout the day with rain clearing later.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-07-05,17.2,15.2,16.1,17.2,15.2,16.1,13,82.2,1.173,100,33.33,rain,0,0,46.7,29.3,221.6,1007.9,98.9,21,76.2,6.8,4,,2024-07-05T05:26:15,2024-07-05T22:03:30,0.98,"Rain, Overcast",Cloudy skies throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-07-06,19.5,13.5,16.5,19.5,13.5,16.5,13.2,81.5,9.967,100,45.83,rain,0,0,79.6,54.8,219.8,1001.3,84.2,26.6,206.9,17.7,9,,2024-07-06T05:27:10,2024-07-06T22:02:53,0,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-07-07,17.3,12.4,14.8,17.3,12.4,14.8,11.4,81,4.117,100,50,rain,0,0,46.9,29.9,206.9,1011,84.7,34.9,145.3,12.5,4,,2024-07-07T05:28:07,2024-07-07T22:02:13,0.05,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-07-08,20,12.2,16.5,20,12.2,16.5,12.2,76.8,1.382,100,12.5,rain,0,0,37,17.7,215.1,1016.4,61.9,34.3,160.7,14,8,,2024-07-08T05:29:07,2024-07-08T22:01:30,0.08,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-07-09,26,16.6,20.1,26,16.6,20.1,15.1,74.4,2.33,100,20.83,rain,0,0,76,24.1,86.5,1013.7,99.7,32.7,176.1,15.3,7,,2024-07-09T05:30:08,2024-07-09T22:00:44,0.11,"Rain, Overcast",Cloudy skies throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-07-10,21.1,16.5,18.9,21.1,16.5,18.9,15.5,81.6,16.692,100,20.83,rain,0,0,51,30.5,222.7,1013.6,80.1,24,210.9,18.4,8,,2024-07-10T05:31:12,2024-07-10T21:59:56,0.14,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-07-11,19.5,15,17.1,19.5,15,17.1,13,77.2,0,0,0,,0,0,42,24.2,251.2,1016.9,92.8,36.2,199.9,17.5,10,,2024-07-11T05:32:18,2024-07-11T21:59:04,0.18,Overcast,Cloudy skies throughout the day.,cloudy,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-07-12,15.2,13.8,14.7,15.2,13.8,14.7,12.8,88.5,8.476,100,41.67,rain,0,0,32.1,20.7,342.7,1013.8,100,29.2,52,4.5,2,,2024-07-12T05:33:26,2024-07-12T21:58:09,0.21,"Rain, Overcast",Cloudy skies throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-07-13,16,14,15,16,14,15,12.8,87.1,5.368,100,54.17,rain,0,0,54,31,247.7,1010.8,93.8,25.8,106.4,9.4,6,,2024-07-13T05:34:36,2024-07-13T21:57:12,0.24,"Rain, Overcast",Cloudy skies throughout the day with a chance of rain throughout the day.,rain,"06260099999,06356099999,C0449,06257099999,EHAM,D3248,EHRD,06348099999,06249099999,06240099999,EHLE,06269099999,06235099999,06344099999" +"Amsterdam,Netherlands",2024-07-14,19.9,13.1,16.8,19.9,13.1,16.8,11.9,73.8,0.092,100,12.5,rain,0,0,46.2,34,227.6,1011.6,56.1,22.2,272,23.6,9,,2024-07-14T05:35:48,2024-07-14T21:56:12,0.25,"Rain, Partially cloudy",Partly cloudy throughout the day with rain clearing later.,rain,"06260099999,06356099999,C0449,06257099999,EHAM,D3248,EHRD,06348099999,06249099999,06240099999,EHLE,EHKD,06330099999,06275099999,06269099999,06235099999,06344099999" +"Amsterdam,Netherlands",2024-07-15,24.1,12.1,18.4,24.1,12.1,18.4,13.1,73,2.35,100,12.5,rain,0,0,26,14.1,153.3,1010.9,54.7,30.4,235.8,20.1,8,,2024-07-15T05:37:02,2024-07-15T21:55:09,0.31,"Rain, Partially cloudy",Partly cloudy throughout the day with late afternoon rain.,rain,"D3248,06260099999,EHRD,06348099999,06249099999,C0449,06240099999,EHLE,06269099999,06344099999,EHAM,06257099999" +"Amsterdam,Netherlands",2024-07-16,20.3,16.3,18.1,20.3,16.3,18.1,15,82.5,9.575,100,41.67,rain,0,0,43,30.3,223.4,1008.7,84.5,29,207.3,17.7,10,,2024-07-16T05:38:17,2024-07-16T21:54:03,0.34,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"D3248,06260099999,06348099999,06249099999,06240099999,C0449,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-07-17,20.6,15,17.7,20.6,15,17.7,14.2,80.8,0.545,100,8.33,rain,0,0,36.7,24.1,272.5,1019.1,72.7,33.2,235.4,20.2,9,,2024-07-17T05:39:34,2024-07-17T21:52:54,0.37,"Rain, Partially cloudy",Partly cloudy throughout the day with rain clearing later.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-07-18,25.3,12.5,19.3,25.3,12.5,19.3,14.6,76.9,0,0,0,,0,0,18,11.2,61.8,1022.3,73.1,19.7,287.3,24.9,10,,2024-07-18T05:40:53,2024-07-18T21:51:44,0.4,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-07-19,27.6,17.3,22.3,28.4,17.3,22.4,16.8,72.4,0.005,100,4.17,rain,0,0,18,13.5,100.9,1018.5,87.7,32.6,181.1,15.4,7,,2024-07-19T05:42:13,2024-07-19T21:50:30,0.43,"Rain, Partially cloudy",Partly cloudy throughout the day with morning rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-07-20,29.3,18.2,23.6,29.4,18.2,23.7,16.6,66.1,0.146,100,8.33,rain,0,0,24,19.2,128,1009.7,44.1,28.3,277.4,24,9,,2024-07-20T05:43:34,2024-07-20T21:49:14,0.47,"Rain, Partially cloudy",Becoming cloudy in the afternoon with rain.,rain,"06260099999,06356099999,C0449,06257099999,EHAM,06267099999,D3248,EHRD,06348099999,06249099999,06240099999,06330099999,EHLE,06275099999,06269099999,06235099999,06344099999" +"Amsterdam,Netherlands",2024-07-21,22.2,16.4,19.3,22.2,16.4,19.3,16.6,84.6,0.988,100,16.67,rain,0,0,29,20.8,306.6,1008.3,89.5,30.4,126.5,11.1,6,,2024-07-21T05:44:57,2024-07-21T21:47:56,0.5,"Rain, Partially cloudy",Partly cloudy throughout the day with rain in the morning and afternoon.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-07-22,21.9,13.2,18.1,21.9,13.2,18.1,14.2,79.1,1.494,100,12.5,rain,0,0,39.9,27.5,242.3,1015.7,61.5,31.7,195.3,16.9,9,,2024-07-22T05:46:21,2024-07-22T21:46:35,0.54,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-07-23,21.6,17,19.1,21.6,17,19.1,15.8,81.4,0.346,100,16.67,rain,0,0,38.4,25.1,272.1,1016,90,28.1,165.7,14.4,7,,2024-07-23T05:47:46,2024-07-23T21:45:12,0.57,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,EHLE,06269099999,06257099999,06344099999,EHAM" +"Amsterdam,Netherlands",2024-07-24,20.2,14.1,17.8,20.2,14.1,17.8,12.8,73.6,0,0,0,,0,0,33.7,16.3,321.1,1020.8,57.2,32.2,267.5,23.2,8,,2024-07-24T05:49:13,2024-07-24T21:43:46,0.61,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-07-25,22.3,13.1,18.3,22.3,13.1,18.3,14.4,78.3,0.363,100,20.83,rain,0,0,39.1,30,192.7,1013.8,94.5,33.4,128.6,11.1,7,,2024-07-25T05:50:41,2024-07-25T21:42:18,0.64,"Rain, Overcast",Cloudy skies throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-07-26,21,15.7,18.8,21,15.7,18.8,15.6,82.2,3.884,100,41.67,rain,0,0,34.6,23.2,250.1,1010.2,92.1,25.5,179.9,15.8,8,,2024-07-26T05:52:09,2024-07-26T21:40:49,0.68,"Rain, Overcast",Cloudy skies throughout the day with rain clearing later.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-07-27,22.3,12.8,17.8,22.3,12.8,17.8,13.5,77.9,0.014,100,4.17,rain,0,0,27.4,17,234.7,1013.9,57.1,23.2,194.4,16.8,8,,2024-07-27T05:53:39,2024-07-27T21:39:17,0.71,"Rain, Partially cloudy",Partly cloudy throughout the day with morning rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-07-28,22.1,14,18.4,22.1,14,18.4,13.1,73,0,0,0,,0,0,23,14.4,311.5,1023.9,35.4,30.5,217.8,18.6,9,,2024-07-28T05:55:09,2024-07-28T21:37:42,0.75,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"D3248,06260099999,06348099999,06249099999,06240099999,C0449,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-07-29,25.6,13.5,19.8,25.6,13.5,19.8,13.6,69.6,0,0,0,,0,0,24.8,14.4,99.4,1023.5,41.9,37.3,303,26.1,9,,2024-07-29T05:56:41,2024-07-29T21:36:06,0.78,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-07-30,27.9,16.4,22.6,28.6,16.4,22.7,15.4,65.8,0,0,0,,0,0,20.4,13.5,51.6,1017,56.1,35.8,294.4,25.5,9,,2024-07-30T05:58:13,2024-07-30T21:34:28,0.82,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-07-31,25.2,16.9,21.3,25.2,16.9,21.3,14.7,66.9,0,0,0,,0,0,36.4,17.5,47.1,1015.5,79,36.5,225,19.3,8,,2024-07-31T05:59:46,2024-07-31T21:32:48,0.86,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"06260099999,C0449,06257099999,EHAM,D3248,EHRD,06348099999,06249099999,06240099999,EHLE,EHKD,06269099999,06344099999" +"Amsterdam,Netherlands",2024-08-01,22.3,16.4,19.4,22.3,16.4,19.4,15.6,78.7,0,0,0,,0,0,31.1,19.9,52.1,1013,90.4,32.6,111.9,9.5,4,,2024-08-01T06:01:20,2024-08-01T21:31:07,0.89,Overcast,Cloudy skies throughout the day.,cloudy,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-08-02,25,14.9,19.8,25,14.9,19.8,14.7,74,0,0,0,,0,0,18.2,13.2,87.5,1012.6,52.2,29.2,211.1,18.2,7,,2024-08-02T06:02:54,2024-08-02T21:29:23,0.93,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"06260099999,C0449,06257099999,EHAM,D3248,EHRD,06348099999,06249099999,06240099999,EHLE,EHKD,06269099999,06344099999" +"Amsterdam,Netherlands",2024-08-03,22.9,18,20.3,22.9,18,20.3,16.7,80.1,0.029,100,8.33,rain,0,0,43.4,30.2,242.8,1011.1,94.8,24.2,167.9,14.4,6,,2024-08-03T06:04:29,2024-08-03T21:27:37,0.96,"Rain, Overcast",Cloudy skies throughout the day with late afternoon rain.,rain,"06260099999,C0449,06257099999,EHAM,D3248,EHRD,06348099999,06249099999,06240099999,EHLE,EHKD,06269099999,06344099999" +"Amsterdam,Netherlands",2024-08-04,19.8,14.4,17.6,19.8,14.4,17.6,12.4,72.7,0.049,100,12.5,rain,0,0,39.9,17,293.4,1014.5,99.5,28.5,142.7,12.1,5,,2024-08-04T06:06:04,2024-08-04T21:25:50,0,"Rain, Overcast",Cloudy skies throughout the day with rain clearing later.,rain,"D3248,06260099999,06348099999,06249099999,06240099999,C0449,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-08-05,23.1,14.8,19.1,23.1,14.8,19.1,13.5,70.3,0,0,0,,0,0,34,19.5,218.9,1014.1,52.6,29.9,233.5,20.2,8,,2024-08-05T06:07:40,2024-08-05T21:24:02,0.03,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-08-06,27.5,16.1,21.4,27.8,16.1,21.5,15.2,69,0,0,0,,0,0,31.3,20.4,196.4,1010.6,58,28.4,225.7,19.6,9,,2024-08-06T06:09:16,2024-08-06T21:22:11,0.06,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-08-07,22.5,17.6,20,22.5,17.6,20,13.9,69.8,0.34,100,16.67,rain,0,0,39.1,23.9,262,1011.8,62.4,33.7,249.9,21.6,9,,2024-08-07T06:10:53,2024-08-07T21:20:19,0.1,"Rain, Partially cloudy",Clearing in the afternoon with rain clearing later.,rain,"D3248,06260099999,06348099999,06249099999,06240099999,C0449,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-08-08,23.6,17.5,20.3,23.6,17.5,20.3,13.5,66.4,0,0,0,,0,0,37.9,23.9,221.4,1014.9,80.1,28,261.7,22.6,9,,2024-08-08T06:12:30,2024-08-08T21:18:26,0.13,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-08-09,22.8,17.1,20.3,22.8,17.1,20.3,16.6,79.8,0.448,100,29.17,rain,0,0,61.7,36.1,240.7,1013.1,85.2,27.6,132.9,11.5,5,,2024-08-09T06:14:08,2024-08-09T21:16:31,0.16,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-08-10,23.1,16.9,19.8,23.1,16.9,19.8,14.6,73.6,0.037,100,4.17,rain,0,0,45.2,30.5,237.9,1019,64.1,23.7,211.8,18.4,7,,2024-08-10T06:15:45,2024-08-10T21:14:35,0.19,"Rain, Partially cloudy",Partly cloudy throughout the day with morning rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-08-11,25.9,15.5,21.2,25.9,15.5,21.2,16.2,74.7,0.027,100,8.33,rain,0,0,29.9,18.1,52.3,1021.1,34,24.8,249.3,21.5,8,,2024-08-11T06:17:23,2024-08-11T21:12:37,0.23,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"06260099999,C0449,06257099999,EHAM,D3248,EHRD,06348099999,06249099999,06240099999,EHLE,EHKD,06269099999,06344099999" +"Amsterdam,Netherlands",2024-08-12,30.8,16.8,23.9,32.2,16.8,24.5,16.6,64.8,0,0,0,,0,0,34.6,21.8,103.2,1012.2,5.9,10,268.5,23.1,9,,2024-08-12T06:19:02,2024-08-12T21:10:38,0.25,Clear,Clear conditions throughout the day.,clear-day,"D3248,EHRD,C0449,EHLE,EHKD,EHAM" +"Amsterdam,Netherlands",2024-08-13,26.2,20.3,23.5,26.2,20.3,23.5,19.2,77.3,0.088,100,8.33,rain,0,0,27.1,19,249.7,1008.9,77.5,27.1,165.2,14.4,7,,2024-08-13T06:20:40,2024-08-13T21:08:37,0.29,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-08-14,22.5,18.2,20.7,22.5,18.2,20.7,17.7,83.8,4.624,100,41.67,rain,0,0,24.3,13.7,298.4,1011.6,100,19.9,79.6,7,5,,2024-08-14T06:22:19,2024-08-14T21:06:36,0.33,"Rain, Overcast",Cloudy skies throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-08-15,23.9,16.5,20.5,23.9,16.5,20.5,16.6,79.2,0.417,100,4.17,rain,0,0,42.8,30.5,230.5,1015.1,61.6,19.8,165,14.4,6,,2024-08-15T06:23:58,2024-08-15T21:04:33,0.36,"Rain, Partially cloudy",Partly cloudy throughout the day with morning rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-08-16,21.3,17.2,19.7,21.3,17.2,19.7,17.1,85.1,7.05,100,66.67,rain,0,0,43.8,25.8,257.9,1014.1,99.4,24.8,78.9,6.8,5,,2024-08-16T06:25:37,2024-08-16T21:02:29,0.39,"Rain, Overcast",Cloudy skies throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-08-17,22.7,12.4,18.2,22.7,12.4,18.2,12.5,71.9,0.636,100,8.33,rain,0,0,20.7,10.7,52.6,1013.4,57.3,36.3,237.7,20.5,8,,2024-08-17T06:27:16,2024-08-17T21:00:24,0.42,"Rain, Partially cloudy",Becoming cloudy in the afternoon with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-08-18,21.9,13.7,17.7,21.9,13.7,17.7,12.7,73.9,0,0,0,,0,0,34,20.6,315.2,1012.6,80.9,32.3,194.5,16.9,8,,2024-08-18T06:28:55,2024-08-18T20:58:18,0.46,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-08-19,22.8,11.7,18,22.8,11.7,18,11.9,69,0,0,0,,0,0,31.4,20.4,172.6,1017.3,49.1,28.7,204.4,17.7,6,,2024-08-19T06:30:34,2024-08-19T20:56:11,0.5,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-08-20,23,16,18.3,23,16,18.3,15,81.7,9.017,100,37.5,rain,0,0,47.3,29.7,189.3,1010.8,93.5,17,90.5,7.8,4,,2024-08-20T06:32:14,2024-08-20T20:54:03,0.52,"Rain, Overcast",Cloudy skies throughout the day with rain.,rain,"06260099999,06356099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,EHAM,06344099999" +"Amsterdam,Netherlands",2024-08-21,19,13.4,16.4,19,13.4,16.4,10.4,68.5,3.655,100,25,rain,0,0,48.8,31.3,257.1,1014.9,68.5,32.4,208.2,18.1,9,,2024-08-21T06:33:53,2024-08-21T20:51:53,0.56,"Rain, Partially cloudy",Partly cloudy throughout the day with rain clearing later.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-08-22,21.8,16,18.7,21.8,16,18.7,12.1,66.6,0,0,0,,0,0,53.9,37.2,211.5,1010,98.8,31.8,121.6,10.6,5,,2024-08-22T06:35:33,2024-08-22T20:49:43,0.59,Overcast,Cloudy skies throughout the day.,cloudy,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-08-23,21.9,15.8,19.6,21.9,15.8,19.6,14.3,72.1,0.848,100,8.33,rain,0,0,68.9,44.3,219.2,1005.2,91.6,33.4,117.1,10.2,6,,2024-08-23T06:37:12,2024-08-23T20:47:33,0.63,"Rain, Overcast",Cloudy skies throughout the day with afternoon rain.,rain,"D3248,06260099999,06348099999,06249099999,06240099999,C0449,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-08-24,25.5,15.2,18.3,25.5,15.2,18.3,15.7,85.6,10.326,100,41.67,rain,0,0,80.1,36.7,191,1006,97.5,26.5,89.4,8,6,,2024-08-24T06:38:51,2024-08-24T20:45:21,0.66,"Rain, Overcast",Cloudy skies throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-08-25,19.9,13.6,16.8,19.9,13.6,16.8,11.4,71.4,4.947,100,20.83,rain,0,0,42.6,30.2,238.1,1016.9,60.2,32.9,160,13.8,7,,2024-08-25T06:40:31,2024-08-25T20:43:08,0.7,"Rain, Partially cloudy",Partly cloudy throughout the day with rain clearing later.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-08-26,20.7,14.4,17.2,20.7,14.4,17.2,13,76.9,0.97,100,20.83,rain,0,0,42.2,27.8,211.8,1020.6,78.1,30.2,186.5,16.1,7,,2024-08-26T06:42:10,2024-08-26T20:40:55,0.75,"Rain, Partially cloudy",Partly cloudy throughout the day with rain in the morning and afternoon.,rain,"D3248,06260099999,06348099999,06249099999,06240099999,C0449,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-08-27,23.6,12.8,18.2,23.6,12.8,18.2,13,74.1,0.721,100,8.33,rain,0,0,27.7,16.6,161.8,1020.8,63.1,25.2,213,18.7,7,,2024-08-27T06:43:50,2024-08-27T20:38:41,0.77,"Rain, Partially cloudy",Partly cloudy throughout the day with rain in the morning and afternoon.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-08-28,28.2,13.9,21,29,13.9,21.2,15.4,72.2,0.009,100,4.17,rain,0,0,30.3,19.5,126.1,1015.3,16.9,26.7,220.1,19,8,,2024-08-28T06:45:29,2024-08-28T20:36:26,0.8,Rain,Clear conditions throughout the day with afternoon rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-08-29,22.8,15.1,19.7,22.8,15.1,19.7,16.2,81.1,0.009,100,4.17,rain,0,0,30,20.8,275.1,1016.1,81.3,23.4,174.9,15,8,,2024-08-29T06:47:09,2024-08-29T20:34:11,0.84,"Rain, Partially cloudy",Partly cloudy throughout the day with afternoon rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-08-30,21.6,12.5,17,21.6,12.5,17,12.7,78,0.009,100,4.17,rain,0,0,29.4,20.2,33,1022.6,97.2,24.3,196.9,16.9,7,,2024-08-30T06:48:48,2024-08-30T20:31:55,0.87,"Rain, Overcast",Cloudy skies throughout the day with afternoon rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-08-31,21.6,15.4,18.2,21.6,15.4,18.2,12.7,71,0.009,100,4.17,rain,0,0,45.4,30.8,60,1023.5,59.2,27.2,195.5,16.8,6,,2024-08-31T06:50:28,2024-08-31T20:29:38,0.91,"Rain, Partially cloudy",Clearing in the afternoon with afternoon rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-09-01,27.8,15.7,21,29,15.7,21.2,15.8,72.9,0,0,0,,0,0,33.6,22.5,74.3,1016.2,67.3,28.9,173,15,7,,2024-09-01T06:52:07,2024-09-01T20:27:21,0.94,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-09-02,27,18.3,22.2,28.1,18.3,22.3,18.3,79.5,0.656,100,12.5,rain,0,0,27.1,15.8,240.5,1011.9,84.8,24.7,178.2,15.3,7,,2024-09-02T06:53:46,2024-09-02T20:25:03,0.98,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-09-03,23.1,17.3,19.8,23.1,17.3,19.8,17.5,86.6,8.288,100,29.17,rain,0,0,28.1,20.4,218.2,1013.5,98.3,16.8,72,6.2,3,,2024-09-03T06:55:26,2024-09-03T20:22:45,0,"Rain, Overcast",Cloudy skies throughout the day with rain.,rain,"D3248,06260099999,06348099999,06249099999,06240099999,C0449,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-09-04,20.4,16.5,18.3,20.4,16.5,18.3,16.6,90.5,1.983,100,25,rain,0,0,20.2,12.8,188,1015.7,99.8,17.9,64.5,5.6,5,,2024-09-04T06:57:05,2024-09-04T20:20:26,0.04,"Rain, Overcast",Cloudy skies throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-09-05,27.6,15.7,21.1,28.8,15.7,21.3,17.4,81,0.009,100,4.17,rain,0,0,45,28.1,49.3,1011.7,71.9,18.7,138.6,12.1,7,,2024-09-05T06:58:44,2024-09-05T20:18:07,0.08,"Rain, Partially cloudy",Partly cloudy throughout the day with afternoon rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-09-06,26.3,18.6,22,26.3,18.6,22,15.8,69.1,5.645,100,20.83,rain,0,0,32.5,21.3,89.4,1010.1,31.2,22.2,165.3,14.4,7,,2024-09-06T07:00:23,2024-09-06T20:15:47,0.11,"Rain, Partially cloudy",Becoming cloudy in the afternoon with late afternoon rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-09-07,24.6,16.8,20.6,24.6,16.8,20.6,17.5,83.6,1.292,100,12.5,rain,0,0,21.7,13.5,119.9,1011.1,94,20.6,156.4,13.4,7,,2024-09-07T07:02:03,2024-09-07T20:13:27,0.14,"Rain, Overcast",Cloudy skies throughout the day with rain clearing later.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-09-08,22.3,15.5,19.7,22.3,15.5,19.7,15,75.4,0.777,100,20.83,rain,0,0,39.5,27.3,172.2,1007.5,80.5,33.8,142,12.3,7,,2024-09-08T07:03:42,2024-09-08T20:11:07,0.18,"Rain, Partially cloudy",Partly cloudy throughout the day with rain clearing later.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-09-09,20.5,14.4,16.7,20.5,14.4,16.7,13.1,79.6,0.836,100,25,rain,0,0,45.5,29.2,305,1005,88.2,34.9,109.1,9.6,6,,2024-09-09T07:05:21,2024-09-09T20:08:46,0.21,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999,EHAM" +"Amsterdam,Netherlands",2024-09-10,17.2,13.1,15.1,17.2,13.1,15.1,11.9,81.7,12.151,100,50,rain,0,0,55.1,40.4,231.1,1007.3,98.8,20.5,40,3.3,2,,2024-09-10T07:07:01,2024-09-10T20:06:25,0.24,"Rain, Overcast",Cloudy skies throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-09-11,13.8,9,11.8,13.8,7,11.6,8.2,79.6,18.236,100,79.17,rain,0,0,54.3,35.6,266.8,1004.6,74.5,27.6,101,8.7,5,,2024-09-11T07:08:40,2024-09-11T20:04:04,0.25,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-09-12,13.9,8,10.5,13.9,6.1,9.7,7.6,83.1,8.291,100,41.67,rain,0,0,27.5,19,272.2,1012.1,73.9,33.4,112.2,9.6,6,,2024-09-12T07:10:19,2024-09-12T20:01:43,0.31,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-09-13,16.8,6.9,11.7,16.8,5.7,11.5,7.8,78.6,0.388,100,16.67,rain,0,0,36.7,23.7,341.8,1023.8,42.7,25,128.5,11,7,,2024-09-13T07:11:59,2024-09-13T19:59:21,0.34,"Rain, Partially cloudy",Partly cloudy throughout the day with rain in the morning and afternoon.,rain,"06260099999,D3248,EHRD,06348099999,06249099999,C0449,06240099999,EHLE,EHKD,06257099999,EHAM" +"Amsterdam,Netherlands",2024-09-14,17.2,5.7,11.8,17.2,5.7,11.8,7.2,75.3,0.055,100,4.17,rain,0,0,26.8,17,270.6,1030.4,58.4,35.2,106.6,9.1,5,,2024-09-14T07:13:38,2024-09-14T19:56:59,0.38,"Rain, Partially cloudy",Partly cloudy throughout the day with morning rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-09-15,18.7,8.9,13.9,18.7,7.8,13.6,10.3,79.5,0.018,100,4.17,rain,0,0,25,17,227.3,1027,57.9,28.1,96.5,8.2,4,,2024-09-15T07:15:17,2024-09-15T19:54:37,0.41,"Rain, Partially cloudy",Partly cloudy throughout the day with early morning rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-09-16,19,12.2,15.5,19,12.2,15.5,12.6,83.9,0.343,100,20.83,rain,0,0,32.9,21.1,357.5,1026,74.8,24.9,91.4,8,4,,2024-09-16T07:16:57,2024-09-16T19:52:15,0.44,"Rain, Partially cloudy",Partly cloudy throughout the day with rain in the morning and afternoon.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-09-17,19,12.5,16.3,19,12.5,16.3,13.3,82.7,0.105,100,4.17,rain,0,0,42.9,27.7,35.4,1029.6,85.7,31.8,131.6,11.2,6,,2024-09-17T07:18:37,2024-09-17T19:49:53,0.48,"Rain, Partially cloudy",Partly cloudy throughout the day with morning rain.,rain,"06260099999,D3248,E5029,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-09-18,21,14.3,17,21,14.3,17,14.4,85.4,0,0,0,,0,0,37.6,23.5,50.4,1028.3,56.1,15.7,144.6,12.5,6,,2024-09-18T07:20:16,2024-09-18T19:47:31,0.5,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"06260099999,D3248,06348099999,06249099999,06240099999,C0449,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-09-19,21,15.1,17.2,21,15.1,17.2,15.1,87.8,0.014,100,4.17,rain,0,0,31.9,22.3,52.7,1025.5,21.1,7.3,104.3,9,6,,2024-09-19T07:21:56,2024-09-19T19:45:08,0.54,"Rain, Partially cloudy",Partly cloudy throughout the day with morning rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-09-20,22.4,15.1,17.8,22.4,15.1,17.8,13.4,77,0.009,100,4.17,rain,0,0,39.6,24.8,73.4,1022.2,11.6,20.4,149.9,13,6,,2024-09-20T07:23:36,2024-09-20T19:42:46,0.58,Rain,Clear conditions throughout the day with late afternoon rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999" +"Amsterdam,Netherlands",2024-09-21,23.2,12.7,17.5,23.2,12.7,17.5,13.4,77.8,0,0,0,,0,0,21.3,16.6,80.1,1019.9,2.2,24.8,157.3,13.5,6,,2024-09-21T07:25:16,2024-09-21T19:40:24,0.61,Clear,Clear conditions throughout the day.,clear-day,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-09-22,23.2,13.6,17.9,23.2,13.6,17.9,14.5,81.4,0.13,100,8.33,rain,0,0,23.2,17,97.5,1014.9,88,23.1,151.1,13.1,8,,2024-09-22T07:26:56,2024-09-22T19:38:02,0.65,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"D3248,06260099999,06348099999,06249099999,06240099999,C0449,06269099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-09-23,20.6,14.8,17.4,20.6,14.8,17.4,14.4,83,0.369,100,20.83,rain,0,0,29.6,23.4,176.5,1006.8,50.4,12.6,77.8,6.5,6,,2024-09-23T07:28:36,2024-09-23T19:35:40,0.68,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"D3248,06260099999,E5029,06348099999,06249099999,06240099999,C0449,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-09-24,17.3,11.9,14.3,17.3,11.9,14.3,12.4,88.8,1.494,100,20.83,rain,0,0,32,23.4,209.8,1002,49.3,10.7,54.9,4.8,3,,2024-09-24T07:30:17,2024-09-24T19:33:18,0.75,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"06260099999,E5029,C0449,06257099999,EHAM,D3248,EHRD,06348099999,06249099999,06240099999,EHLE,EHKD,06269099999" +"Amsterdam,Netherlands",2024-09-25,16.8,14.1,14.9,16.8,14.1,14.9,11.9,82,2.543,100,12.5,rain,0,0,37.1,18.9,199.1,1000.9,70.9,13.3,48.1,4.2,2,,2024-09-25T07:31:57,2024-09-25T19:30:56,0.75,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-09-26,18.1,14,15.9,18.1,14,15.9,13.7,87.4,12.356,100,50,rain,0,0,46.9,31.2,194.9,988.8,55.6,13.8,86.1,7.4,5,,2024-09-26T07:33:38,2024-09-26T19:28:34,0.78,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-09-27,15.1,11.4,12.8,15.1,11.4,12.8,10,83,25.278,100,54.17,rain,0,0,66.4,45.7,228.5,994.4,78.1,12.2,26.5,2.4,2,,2024-09-27T07:35:19,2024-09-27T19:26:13,0.82,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-09-28,14,8.3,10.8,14,7.1,10.4,6.9,77.1,10.955,100,41.67,rain,0,0,42.4,26.5,315.8,1018.2,37.8,14.9,101.3,8.9,5,,2024-09-28T07:37:00,2024-09-28T19:23:52,0.85,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,C0449,06257099999,EHAM,D3248,EHRD,06348099999,06249099999,06240099999,06330099999,EHLE,EHKD,06275099999,06269099999,06235099999,06344099999" +"Amsterdam,Netherlands",2024-09-29,14.1,6.1,10.3,14.1,4.5,9.7,6.7,79.8,0.572,100,4.17,rain,0,0,28.6,22,151.7,1025.1,30.5,14.2,119.3,10.3,5,,2024-09-29T07:38:42,2024-09-29T19:21:31,0.89,"Rain, Partially cloudy",Partly cloudy throughout the day with morning rain.,rain,"06260099999,06356099999,C0449,06257099999,EHAM,D3248,EHRD,06348099999,06249099999,06240099999,EHLE,EHKD,06269099999,06235099999" +"Amsterdam,Netherlands",2024-09-30,14.9,9,11.5,14.9,5.8,10.5,9.1,85.7,8.691,100,37.5,rain,0,0,48.2,30.1,135.9,1009.1,74.5,12.9,35.1,3.1,2,,2024-09-30T07:40:23,2024-09-30T19:19:10,0.92,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"06260099999,06356099999,C0449,06257099999,EHAM,D3248,EHRD,06348099999,06249099999,06240099999,EHLE,06269099999,06235099999,06344099999" +"Amsterdam,Netherlands",2024-10-01,14.2,12.1,13.4,14.2,12.1,13.4,12.4,93.9,13.099,100,37.5,rain,0,0,42.8,26.8,187.5,1003.6,81.2,10.1,21.2,1.9,1,,2024-10-01T07:42:05,2024-10-01T19:16:50,0.95,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-10-02,13.6,11,12.1,13.6,11,12.1,9.8,86.2,3.896,100,25,rain,0,0,39.6,26.2,49.6,1011.4,72.9,12.2,35.4,2.9,2,,2024-10-02T07:43:47,2024-10-02T19:14:30,0,"Rain, Partially cloudy",Partly cloudy throughout the day with rain clearing later.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-10-03,15,5.8,10.5,15,3.9,9.7,6.6,77.9,0.022,100,8.33,rain,0,0,37.3,25.8,43.1,1020,14.5,16.8,121.7,10.6,6,,2024-10-03T07:45:29,2024-10-03T19:12:10,0.02,Rain,Clear conditions throughout the day with rain.,rain,"D3248,06260099999,06348099999,06249099999,06240099999,C0449,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-10-04,15,4.5,9.3,15,4.5,9.2,6.3,83.5,0,0,0,,0,0,16.4,10.9,69.5,1021.7,9.7,12.8,110.1,9.7,5,,2024-10-04T07:47:12,2024-10-04T19:09:51,0.06,Clear,Clear conditions throughout the day.,clear-day,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-10-05,15.2,4.3,9.4,15.2,3.4,9.1,6.2,82.3,0.014,100,4.17,rain,0,0,22.2,13.3,114.8,1018.4,8,12.4,129.2,11.2,6,,2024-10-05T07:48:55,2024-10-05T19:07:32,0.09,Rain,Clear conditions throughout the day with morning rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-10-06,15.1,7.1,10.8,15.1,4.7,9.8,7.7,81.5,0,0,0,,0,0,36.4,27.8,125.5,1006.8,50.5,15.5,107.7,9.2,6,,2024-10-06T07:50:38,2024-10-06T19:05:14,0.12,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-10-07,18,12.8,15.2,18,12.8,15.2,12.2,82.8,0.202,100,12.5,rain,0,0,36.8,26.6,189.1,1000.1,62,14.6,101.9,8.9,4,,2024-10-07T07:52:21,2024-10-07T19:02:56,0.16,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-10-08,17.2,13.9,15.6,17.2,13.9,15.6,13.1,85.4,0.143,100,16.67,rain,0,0,35.3,25.7,180.7,996.6,60,15.2,69.5,5.8,3,,2024-10-08T07:54:05,2024-10-08T19:00:39,0.19,"Rain, Partially cloudy",Partly cloudy throughout the day with rain clearing later.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-10-09,16.1,12.1,14.3,16.1,12.1,14.3,12.3,87.8,0.334,100,20.83,rain,0,0,39.7,26.7,168.1,989.2,55.4,15.7,41.5,3.7,2,,2024-10-09T07:55:49,2024-10-09T18:58:23,0.23,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-10-10,13.7,9.3,12,13.7,7.9,11.9,9,82.4,0.347,100,20.83,rain,0,0,44,26.3,323,997.3,59.3,12.6,63,5.3,3,,2024-10-10T07:57:33,2024-10-10T18:56:07,0.25,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-10-11,13,5.4,8.9,13,3.7,8.3,5.2,78.6,2.067,100,16.67,rain,0,0,26.2,16.2,227.6,1014.8,33.2,15.4,100.2,8.5,6,,2024-10-11T07:59:18,2024-10-11T18:53:51,0.29,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-10-12,12.1,4.1,8.7,12.1,3.3,7.8,6.7,87.6,0.104,100,4.17,rain,0,0,42,32.7,164.6,1010.4,51.7,13.3,49.4,4.2,3,,2024-10-12T08:01:02,2024-10-12T18:51:37,0.33,"Rain, Partially cloudy",Partly cloudy throughout the day with morning rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-10-13,13.5,8.3,10.8,13.5,6,10.4,6.1,72.6,7.684,100,41.67,rain,0,0,55.6,38.6,282.2,1012.7,46.9,14.4,52.3,4.5,2,,2024-10-13T08:02:48,2024-10-13T18:49:23,0.36,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-10-14,12,5.2,8.5,12,4.2,7.8,6.3,86.4,0.116,100,8.33,rain,0,0,16.8,11.9,108.4,1017.9,40.3,12.6,44.6,3.7,3,,2024-10-14T08:04:33,2024-10-14T18:47:10,0.4,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-10-15,14,4.7,9.4,14,2.3,8.2,6.7,84,0,0,0,,0,0,34.8,22.5,100.7,1018.8,44,13.6,87.4,7.5,4,,2024-10-15T08:06:19,2024-10-15T18:44:57,0.43,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999" +"Amsterdam,Netherlands",2024-10-16,19.7,10.9,15,19.7,10.9,15,12,82.6,0,0,0,,0,0,40,26.2,125.9,1009.4,46.9,14.9,75.2,6.4,3,,2024-10-16T08:08:05,2024-10-16T18:42:46,0.46,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-10-17,18.6,14.4,16.4,18.6,14.4,16.4,14.4,88.2,0.25,100,25,rain,0,0,36.1,25.7,182.9,1009.4,67.9,15.1,25.5,2.3,2,,2024-10-17T08:09:51,2024-10-17T18:40:35,0.5,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-10-18,17.2,13.9,15.2,17.2,13.9,15.2,14.4,95.4,0.293,100,25,rain,0,0,16.2,11.1,359.3,1013.7,75.4,6.7,37.2,3.1,3,,2024-10-18T08:11:38,2024-10-18T18:38:26,0.53,"Rain, Partially cloudy",Partly cloudy throughout the day with rain clearing later.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-10-19,15.9,13.9,14.7,15.9,13.9,14.7,12.9,89.5,0.986,100,16.67,rain,0,0,31.6,22,187.3,1013.5,76.3,11.4,36.3,3.1,2,,2024-10-19T08:13:25,2024-10-19T18:36:17,0.56,"Rain, Partially cloudy",Partly cloudy throughout the day with rain in the morning and afternoon.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-10-20,17.2,13.4,15.3,17.2,13.4,15.3,13,86.5,0.808,100,20.83,rain,0,0,51.2,35.3,185.3,1015,82.7,14.1,37,3.2,2,,2024-10-20T08:15:13,2024-10-20T18:34:09,0.6,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-10-21,15.6,11.3,14.2,15.6,11.3,14.2,12.4,88.8,0.973,100,25,rain,0,0,44,26.3,218,1020.2,59.6,11.1,17.9,1.4,1,,2024-10-21T08:17:00,2024-10-21T18:32:02,0.63,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-10-22,15.9,9.1,12.4,15.9,7.3,12.3,10,86.1,6.662,100,25,rain,0,0,36.4,24.7,224.6,1026.7,38.6,12.1,62,5.4,3,,2024-10-22T08:18:48,2024-10-22T18:29:57,0.66,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-10-23,14.6,9.3,11.3,14.6,8.1,11.2,10.8,96.4,0,0,0,,0,0,20.9,12.1,157.3,1032.8,53.6,4.9,29.5,2.6,1,,2024-10-23T08:20:36,2024-10-23T18:27:52,0.7,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-10-24,13.9,7,10.4,13.9,5.2,9.6,8.9,90.7,0.128,100,4.17,rain,0,0,31.2,17.4,121.1,1024.7,22,7.9,70.8,6.2,4,,2024-10-24T08:22:25,2024-10-24T18:25:49,0.75,"Rain, Partially cloudy",Partly cloudy throughout the day with morning rain.,rain,"06260099999,06356099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-10-25,18.7,10.3,13.7,18.7,10.3,13.7,12.1,90.5,0.009,100,4.17,rain,0,0,22,14.7,145.3,1016.8,38.7,10,84.1,7.3,4,,2024-10-25T08:24:13,2024-10-25T18:23:47,0.76,"Rain, Partially cloudy",Clearing in the afternoon with morning rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-10-26,19.7,10.4,14.4,19.7,10.4,14.4,13,91.8,0,0,0,,0,0,19.6,14.2,129.5,1016,31.1,8,81,7.1,4,,2024-10-26T08:26:02,2024-10-26T18:21:46,0.8,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"06260099999,C0449,06257099999,EHAM,D3248,EHRD,06348099999,06249099999,06240099999,EHLE,EHKD,06269099999,06235099999,06344099999" +"Amsterdam,Netherlands",2024-10-27,15.9,10.1,13.4,15.9,10.1,13.4,11.2,86.8,0.071,100,12.5,rain,0,0,27.2,19,257.2,1019.5,57.5,11.4,78.8,7,5,,2024-10-27T07:27:51,2024-10-27T17:19:46,0.83,"Rain, Partially cloudy",Partly cloudy throughout the day with morning rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-10-28,15.5,9.9,12.7,15.5,8.3,12.5,11.5,92.9,1.034,100,33.33,rain,0,0,38.4,28.3,212.4,1021.8,65.2,9.2,29,2.5,2,,2024-10-28T07:29:41,2024-10-28T17:17:48,0.86,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-10-29,15.1,13.1,13.9,15.1,13.1,13.9,13.3,96,3.475,100,58.33,rain,0,0,29.2,19,218.6,1023.2,90,7.5,27,2.3,1,,2024-10-29T07:31:30,2024-10-29T17:15:51,0.9,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06356099999,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-10-30,14.8,10.6,12.9,14.8,10.6,12.9,11.9,93.9,1.222,100,29.17,rain,0,0,11.9,8.3,298.8,1027.5,91.5,11.3,21.8,1.7,1,,2024-10-30T07:33:20,2024-10-30T17:13:56,0.93,"Rain, Overcast",Cloudy skies throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-10-31,14.2,9.7,12.6,14.2,8.4,12.6,10.6,87.6,0.077,100,8.33,rain,0,0,21.6,16.2,230.2,1027,83.4,10.8,30.2,2.6,2,,2024-10-31T07:35:10,2024-10-31T17:12:02,0.96,"Rain, Partially cloudy",Partly cloudy throughout the day with rain in the morning and afternoon.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-11-01,12.8,11.2,12,12.8,11.2,12,10.5,90.6,0,0,0,,0,0,29.6,19.8,225,1023.7,91.8,6.7,11.7,0.9,1,,2024-11-01T07:36:59,2024-11-01T17:10:10,0,Overcast,Cloudy skies throughout the day.,cloudy,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-11-02,12.1,5.7,10.2,12.1,3.9,9.7,7.4,83.7,0.207,100,25,rain,0,0,23.6,16.7,58.8,1030.7,50.9,12,60.4,5.3,3,,2024-11-02T07:38:49,2024-11-02T17:08:19,0.03,"Rain, Partially cloudy",Clearing in the afternoon with rain clearing later.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-11-03,12.5,2.9,6.6,12.5,1,5.4,4.9,89.5,0,0,0,,0,0,14.8,10.1,76.5,1030.1,4.9,10.9,76.5,6.6,3,,2024-11-03T07:40:39,2024-11-03T17:06:30,0.07,Clear,Clear conditions throughout the day.,clear-day,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-11-04,12.6,3.4,7.5,12.6,1,5.9,6,90.6,0.014,100,4.17,rain,0,0,26,19.7,81.3,1027.2,31.3,10.2,71.5,6.2,3,,2024-11-04T07:42:29,2024-11-04T17:04:43,0.1,"Rain, Partially cloudy",Partly cloudy throughout the day with morning rain.,rain,"06260099999,C0449,06257099999,EHAM,D3248,EHRD,06348099999,06249099999,06240099999,EHLE,EHKD,06269099999,06235099999,06344099999" +"Amsterdam,Netherlands",2024-11-05,11.6,4,7.1,11.6,1.4,5.8,5.6,90.9,0.062,100,4.17,rain,0,0,22.8,13.1,131.9,1023.6,23,8.2,58.7,5.1,3,,2024-11-05T07:44:19,2024-11-05T17:02:57,0.14,"Rain, Partially cloudy",Partly cloudy throughout the day with morning rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-11-06,11.1,7.2,9,11.1,5.7,8.4,8,93.7,0,0,0,,0,0,11.5,8.8,161.3,1029.9,92.8,4.2,22.6,1.8,1,,2024-11-06T07:46:09,2024-11-06T17:01:13,0.17,Overcast,Cloudy skies throughout the day.,cloudy,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-11-07,9.1,4.1,7,8.6,0.5,5,6,93.4,0,0,0,,0,0,23.6,16.3,105.2,1032.6,95.8,4.1,15.5,1.3,1,,2024-11-07T07:47:58,2024-11-07T16:59:31,0.21,Overcast,Cloudy skies throughout the day.,cloudy,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-11-08,5,4.1,4.6,3.2,0.3,1.5,3.5,93,0,0,0,,0,0,25.9,17.8,107.5,1028.1,93.4,6.2,8.8,0.8,0,,2024-11-08T07:49:48,2024-11-08T16:57:51,0.24,Overcast,Cloudy skies throughout the day.,cloudy,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-11-09,7.9,5.1,6.3,6.9,3.1,5,5.2,92.8,0.22,100,25,rain,0,0,17.2,12,131.9,1024.9,93.3,5.6,9.8,0.8,1,,2024-11-09T07:51:37,2024-11-09T16:56:13,0.25,"Rain, Overcast",Cloudy skies throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-11-10,10.1,7.9,8.9,10.1,6.1,7.8,8.2,95.2,0.404,100,8.33,rain,0,0,17,11.9,181.3,1026.8,89,5.9,6.4,0.6,0,,2024-11-10T07:53:26,2024-11-10T16:54:37,0.31,"Rain, Partially cloudy",Partly cloudy throughout the day with rain clearing later.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-11-11,11.8,9.1,10.5,11.8,7.4,10.1,8.2,85.8,2.623,100,29.17,rain,0,0,42.4,29.4,323.1,1028.5,52.6,10.3,39.2,3.4,2,,2024-11-11T07:55:14,2024-11-11T16:53:03,0.35,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-11-12,10.3,7.2,8.9,10.3,5.5,7,6.6,85.9,2.552,100,8.33,rain,0,0,39.2,24.4,36,1032.6,58.8,12.5,50.7,4.4,3,,2024-11-12T07:57:02,2024-11-12T16:51:31,0.38,"Rain, Partially cloudy",Partly cloudy throughout the day with rain clearing later.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-11-13,12,5,9,12,5,9,7.6,90.8,0,0,0,,0,0,20.4,14,315.3,1032.1,55.6,10.3,22.3,2,2,,2024-11-13T07:58:50,2024-11-13T16:50:02,0.42,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"06260099999,D3248,EHRD,06348099999,06249099999,C0449,06240099999,EHLE,06269099999,06257099999,06344099999,EHAM" +"Amsterdam,Netherlands",2024-11-14,11.3,7.9,10.1,11.3,7.3,9.9,8.5,90.2,0.642,100,25,rain,0,0,26.7,14.7,329.1,1028.3,90,11.5,25.6,2.3,2,,2024-11-14T08:00:37,2024-11-14T16:48:34,0.45,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"06260099999,D3248,06356099999,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-11-15,10.9,7.1,9.3,10.9,6.4,8.6,8.2,92.9,0.271,100,12.5,rain,0,0,23.9,18.2,214.5,1026.2,91.2,9.2,15.1,1.4,1,,2024-11-15T08:02:23,2024-11-15T16:47:09,0.5,"Rain, Overcast",Cloudy skies throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06356099999,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-11-16,10.1,8.5,9.5,10.1,5.9,7.3,7.7,88.7,0.795,100,29.17,rain,0,0,37.2,25.7,219.7,1016.5,91.5,9.4,14.3,1.3,1,,2024-11-16T08:04:09,2024-11-16T16:45:46,0.52,"Rain, Overcast",Cloudy skies throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-11-17,9.9,5.9,8.2,8.4,3.2,5.2,4.3,76.8,3.553,100,54.17,rain,0,0,44.4,32.3,276.7,1009.7,58.6,14.6,30,2.7,3,,2024-11-17T08:05:54,2024-11-17T16:44:26,0.55,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,06356099999,C0449,06257099999,EHAM,D3248,EHRD,06348099999,06249099999,06240099999,EHLE,EHKD,06269099999,06235099999,06344099999" +"Amsterdam,Netherlands",2024-11-18,8.9,3.9,5.8,5.9,1.1,3.6,2.2,78.4,2.5,100,29.17,rain,0,0,57.6,21.4,281.1,1008.6,39.6,9.8,29.1,2.6,2,,2024-11-18T08:07:39,2024-11-18T16:43:08,0.58,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"D3248,EHRD,C0449,EHLE,EHKD,EHAM" +"Amsterdam,Netherlands",2024-11-19,5,2,4,2.4,-2.1,0.7,2.9,92.5,13.5,100,62.5,rain,0.1,0,47.9,32.6,39.9,994.6,68.7,8.8,12,1.1,1,,2024-11-19T08:09:22,2024-11-19T16:41:53,0.62,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain or snow throughout the day.,snow,"D3248,EHRD,C0449,EHLE,EHKD,EHAM" +"Amsterdam,Netherlands",2024-11-20,5.7,1.1,3.6,2.5,-2.7,-0.1,1,83.4,1,100,33.33,rain,0,0,55.4,38.7,281.9,999.2,59.2,9.3,16.3,1.5,1,,2024-11-20T08:11:05,2024-11-20T16:40:40,0.65,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"D3248,EHRD,C0449,06240099999,EHLE,EHKD,06269099999,EHAM" +"Amsterdam,Netherlands",2024-11-21,4.7,0,2.1,1.3,-3.7,-1.5,-1.1,80,1.7,100,41.67,"rain,snow",0.1,0,55.4,24.5,272.8,997.8,45,9.9,31.5,2.8,4,,2024-11-21T08:12:46,2024-11-21T16:39:30,0.68,"Snow, Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain or snow throughout the day.,snow,"D3248,06260099999,EHRD,06348099999,06249099999,C0449,06240099999,EHLE,EHKD,06269099999,EHAM,06257099999" +"Amsterdam,Netherlands",2024-11-22,6.5,1.4,4,2.7,-2.5,0.1,0.9,80.3,11.495,100,75,"rain,snow",0,0,59.7,40.2,265.1,1000.9,64.2,13.7,10.7,0.9,1,,2024-11-22T08:14:27,2024-11-22T16:38:23,0.71,"Snow, Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain or snow throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-11-23,8.9,2,4.3,4.9,-2.7,-0.2,2.2,86.2,3.886,100,37.5,rain,0,0,54.8,39.2,181.5,1010.2,58.4,14.1,12.9,1.2,1,,2024-11-23T08:16:06,2024-11-23T16:37:18,0.75,"Rain, Partially cloudy",Becoming cloudy in the afternoon with a chance of rain throughout the day.,rain,"06260099999,D3248,06356099999,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-11-24,16.4,9.9,14,16.4,6.2,13.8,8.4,69.5,9.312,100,12.5,rain,0,0,61.2,42.3,192.7,1002,64.9,17.1,27.9,2.6,2,,2024-11-24T08:17:44,2024-11-24T16:36:17,0.78,"Rain, Partially cloudy",Partly cloudy throughout the day with rain clearing later.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-11-25,16.3,7.3,12.2,16.3,4.5,11.7,8.7,80.2,2.626,100,37.5,rain,0,0,58.3,37.5,203,1001.9,53.3,14.4,8.9,0.8,1,,2024-11-25T08:19:20,2024-11-25T16:35:18,0.81,"Rain, Partially cloudy",Partly cloudy throughout the day with rain in the morning and afternoon.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-11-26,9.8,3.9,7.4,6.6,1,4.4,5.3,86.7,2.521,100,20.83,rain,0,0,41.6,29.7,214.8,1014.8,40.9,12.2,26.7,2.3,2,,2024-11-26T08:20:56,2024-11-26T16:34:22,0.84,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"06260099999,C0449,06257099999,EHAM,D3248,EHRD,06348099999,06249099999,06240099999,EHLE,EHKD,06269099999,06235099999,06344099999" +"Amsterdam,Netherlands",2024-11-27,11,3.7,7.5,11,1.9,4.5,6.2,91.6,9.14,100,54.17,rain,0,0,84.1,56.1,218.9,1011.2,76.1,8.3,3.5,0.2,0,,2024-11-27T08:22:29,2024-11-27T16:33:29,0.88,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-11-28,9.7,5.4,7.3,7,3.4,5,3.8,78.2,5.758,100,25,rain,0,0,72.9,27.7,308.4,1024.3,53.3,15.9,31.2,2.6,2,,2024-11-28T08:24:01,2024-11-28T16:32:39,0.91,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-11-29,7.9,0.6,3.8,5.2,-1.8,1.8,1.6,85.9,0.163,100,4.17,"rain,snow",0,0,25.6,20.3,149.5,1030.7,44.3,13,42.6,3.8,2,,2024-11-29T08:25:31,2024-11-29T16:31:53,0.94,"Snow, Rain, Partially cloudy",Partly cloudy throughout the day with morning rain or snow.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-11-30,6.2,1.3,4,3.7,-3.3,0.6,1.6,84.2,0,0,0,,0,0,27.6,18,164.9,1026.7,56.6,13.5,30.8,2.8,2,,2024-11-30T08:27:00,2024-11-30T16:31:09,0.97,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"06260099999,C0449,06257099999,EHAM,D3248,EHRD,06348099999,06249099999,06240099999,EHLE,EHKD,06269099999,06235099999,06344099999" +"Amsterdam,Netherlands",2024-12-01,8.1,3.1,6.1,5,-0.8,2.9,3.1,81.4,1,100,12.5,rain,0,0,41,25.6,170.8,1018.7,82.2,9.4,35.3,3.1,2,,2024-12-01T08:28:26,2024-12-01T16:30:28,0,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"D3248,06260099999,EHRD,06348099999,06249099999,C0449,06240099999,EHLE,EHKD,06269099999,EHAM,06257099999" +"Amsterdam,Netherlands",2024-12-02,11,7.5,9.6,11,5.1,8.3,8.6,93.7,1.266,100,50,rain,0,0,36.4,24.5,212.1,1010.9,86,10.8,13,1.2,1,,2024-12-02T08:29:51,2024-12-02T16:29:51,0.04,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-12-03,8.7,3,5.9,6.1,-0.2,3.1,2.9,81.1,1.775,100,25,rain,0,0,41.2,25.5,319,1018.2,51.2,13.2,18.2,1.8,1,,2024-12-03T08:31:13,2024-12-03T16:29:17,0.08,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,C0449,06257099999,EHAM,D3248,EHRD,06348099999,06249099999,06240099999,EHLE,EHKD,06269099999,06235099999,06344099999" +"Amsterdam,Netherlands",2024-12-04,6.1,1.2,4.2,4.4,-0.3,2.1,2.8,91,0.971,100,25,rain,0,0,20,14.3,187.4,1024.3,58,13.2,10.3,0.8,1,,2024-12-04T08:32:34,2024-12-04T16:28:47,0.11,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,C0449,EHAM,06257099999,D3248,EHRD,06348099999,06249099999,06240099999,EHLE,EHKD,06269099999,06235099999,06344099999" +"Amsterdam,Netherlands",2024-12-05,10.8,4.2,6.9,10.8,0,3.2,6.1,94.5,5.233,100,58.33,rain,0,0,50.4,36.9,182.3,1011.1,91.9,6.3,4.8,0.4,0,,2024-12-05T08:33:52,2024-12-05T16:28:20,0.15,"Rain, Overcast",Cloudy skies throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-12-06,11.1,5.5,8.8,11.1,2.4,5.2,5.2,78.3,14.869,100,37.5,rain,0,0,79.6,51,283.5,1007.2,75.6,11,13.9,1.1,1,,2024-12-06T08:35:08,2024-12-06T16:27:56,0.18,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-12-07,10,6,7.5,10,1.6,3.6,5.9,89.8,2.271,100,54.17,rain,0,0,54.7,37.1,180.5,993.5,80.7,11.2,17.3,1.5,2,,2024-12-07T08:36:21,2024-12-07T16:27:35,0.22,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-12-08,7.3,6.1,6.9,4.3,1.7,3.3,5.5,90.9,7.263,100,25,rain,0,0,42,26.5,43,1008.2,84.3,12.1,16.5,1.5,1,,2024-12-08T08:37:32,2024-12-08T16:27:18,0.25,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-12-09,7,5.9,6.4,3.1,1.3,2.2,3.5,81.7,0.153,100,8.33,rain,0,0,52.8,34.4,39.5,1027.2,74.2,15.4,8.8,0.8,1,,2024-12-09T08:38:40,2024-12-09T16:27:05,0.29,"Rain, Partially cloudy",Partly cloudy throughout the day with rain clearing later.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-12-10,5.8,4.8,5,1.6,0.2,0.7,2.4,83.1,0.014,100,4.17,rain,0,0,40.8,27.2,51.9,1031.3,92.3,14.4,6.8,0.6,0,,2024-12-10T08:39:45,2024-12-10T16:26:55,0.33,"Rain, Overcast",Cloudy skies throughout the day with morning rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-12-11,4.8,3.1,3.9,2.5,0,1,1.6,85,0,0,0,,0,0,26,18,59.8,1032,96.1,11.4,6.9,0.6,0,,2024-12-11T08:40:48,2024-12-11T16:26:49,0.36,Overcast,Cloudy skies throughout the day.,cloudy,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-12-12,5,3.1,4.1,5,0.8,2,3.2,94,0.23,100,25,rain,0,0,20.4,12.3,129.6,1031.4,94,6.5,7.9,0.6,0,,2024-12-12T08:41:48,2024-12-12T16:26:46,0.4,"Rain, Overcast",Cloudy skies throughout the day with rain in the morning and afternoon.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-12-13,3.9,0.6,2.1,1,-3.1,-1.4,0.5,89.2,0.049,100,4.17,rain,0,0,25.6,14.9,147.1,1027.1,92,8,5.5,0.4,0,,2024-12-13T08:42:45,2024-12-13T16:26:46,0.43,"Rain, Overcast",Cloudy skies throughout the day with morning rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-12-14,6.9,1.9,4.5,4.1,-2.4,0.9,3,90.1,0.716,100,50,rain,0,0,31.1,23.5,227.9,1018.6,82,7.9,8.3,0.7,0,,2024-12-14T08:43:39,2024-12-14T16:26:51,0.47,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-12-15,10.8,5.1,8.6,10.8,2.1,6.4,7.2,90.8,1.237,100,25,rain,0,0,44,32.2,244.8,1023.3,82.5,11.2,7.3,0.6,0,,2024-12-15T08:44:30,2024-12-15T16:26:58,0.5,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-12-16,10.9,9.7,10.4,10.9,6.4,9.1,8.2,86.5,0.056,100,16.67,rain,0,0,48.2,33.1,249.2,1027.7,78.8,13.5,6.8,0.5,0,,2024-12-16T08:45:18,2024-12-16T16:27:10,0.53,"Rain, Partially cloudy",Partly cloudy throughout the day with rain in the morning and afternoon.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-12-17,9.8,7.3,8.6,6.9,4.5,5.8,7.1,90.6,0,0,0,,0,0,34.4,24,212.9,1025,79.3,11.5,12.2,1.1,1,,2024-12-17T08:46:03,2024-12-17T16:27:25,0.57,Partially cloudy,Partly cloudy throughout the day.,partly-cloudy-day,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-12-18,12.8,6.4,10.2,12.8,2,8.4,8.4,88.4,0.254,100,25,rain,0,0,71.4,48.4,210.9,1009.3,80.5,11.8,5.7,0.5,0,,2024-12-18T08:46:45,2024-12-18T16:27:43,0.6,"Rain, Partially cloudy",Partly cloudy throughout the day with rain.,rain,"06260099999,D3248,EHRD,06348099999,06249099999,C0449,06240099999,EHLE,EHKD,06269099999,06257099999,EHAM" +"Amsterdam,Netherlands",2024-12-19,12.8,5.3,8.5,12.8,1.1,5.7,6,84.8,15.598,100,70.83,rain,0,0,67.2,43.4,267.6,999.9,71.3,11.6,7.5,0.6,0,,2024-12-19T08:47:23,2024-12-19T16:28:05,0.63,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-12-20,7.8,5,6.4,4.2,1.6,2.8,3.1,79.4,2.074,100,25,rain,0,0,48.1,27.6,248.1,1015.9,53.5,12.9,18.7,1.6,1,,2024-12-20T08:47:58,2024-12-20T16:28:30,0.66,"Rain, Partially cloudy",Becoming cloudy in the afternoon with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-12-21,10.7,7.1,8.5,10.7,3.1,5.5,7.1,91,3.895,100,54.17,rain,0,0,54.8,41.3,231,1008.6,75.3,10.2,9.5,0.9,1,,2024-12-21T08:48:30,2024-12-21T16:28:59,0.7,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-12-22,9.1,3.5,6.1,5.2,-1.1,1.7,2.6,78,2.824,100,37.5,rain,0,0,57.2,40.4,273.1,998.3,54.3,12.7,17.9,1.4,2,,2024-12-22T08:48:58,2024-12-22T16:29:32,0.75,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-12-23,8,3.2,6.4,4.4,1,2.8,3,79.1,10.719,100,37.5,rain,0,0,58.8,39,320.4,1013.9,44.9,12,18.8,1.6,1,,2024-12-23T08:49:23,2024-12-23T16:30:08,0.76,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-12-24,8.9,3.3,6.1,7,0.3,3.3,5.4,95.2,1.101,100,62.5,rain,0,0,27.6,19.6,207.6,1024.4,82.7,6.2,6.8,0.6,1,,2024-12-24T08:49:44,2024-12-24T16:30:47,0.79,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-12-25,9.8,8.9,9.3,8.5,6.6,7.5,8.9,97.6,0.606,100,29.17,rain,0,0,22.4,15.5,214.6,1032.4,95,3.6,4.4,0.5,0,,2024-12-25T08:50:02,2024-12-25T16:31:29,0.82,"Rain, Overcast",Cloudy skies throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-12-26,8.7,5.2,7.4,7.1,3.5,5.7,7,97.2,0.022,100,4.17,rain,0,0,20.4,14.3,176,1035.8,99,3.5,6.7,0.5,0,,2024-12-26T08:50:16,2024-12-26T16:32:15,0.86,"Rain, Overcast",Cloudy skies throughout the day with morning rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-12-27,5.1,2,3.3,4,0.1,2.1,3,98.3,0.083,100,8.33,rain,0,0,16.7,10.9,143.4,1033.4,92.6,2.1,18.4,1.6,1,,2024-12-27T08:50:27,2024-12-27T16:33:04,0.89,"Rain, Overcast",Cloudy skies throughout the day with rain clearing later.,rain,"D3248,06260099999,06348099999,06249099999,06240099999,C0449,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-12-28,3.1,0.9,1.8,1.3,-1.9,-0.7,1.4,97.8,0,0,0,,0,0,20.2,14,196,1029.5,91.4,2.4,11.8,0.9,1,,2024-12-28T08:50:35,2024-12-28T16:33:56,0.92,Overcast,Cloudy skies throughout the day.,cloudy,"D3248,06260099999,06348099999,06249099999,06240099999,C0449,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-12-29,7,3.3,5.4,3.9,-0.2,2.2,4.8,96,0.316,100,33.33,rain,0,0,34.8,22.9,211.2,1027.7,91.6,4.3,4.6,0.5,0,,2024-12-29T08:50:39,2024-12-29T16:34:51,0.95,"Rain, Overcast",Cloudy skies throughout the day with rain.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-12-30,8.6,6.3,7.7,4.8,2.5,4.1,5.8,88.2,0.117,100,20.83,rain,0,0,46,31.1,229.1,1026.5,86.6,10.8,8.3,0.7,0,,2024-12-30T08:50:39,2024-12-30T16:35:50,0,"Rain, Partially cloudy",Partly cloudy throughout the day with rain clearing later.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" +"Amsterdam,Netherlands",2024-12-31,8,3.8,5.6,4.5,-1.5,0.9,4,89.9,0,0,0,,0,0,54.8,39.5,204.2,1021.9,94.4,9.6,7,0.6,0,,2024-12-31T08:50:36,2024-12-31T16:36:51,0.02,Overcast,Cloudy skies throughout the day.,cloudy,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06235099999,06257099999,06344099999" diff --git a/example/assets/data/bitcoin_2023-01-01_2023-12-31.csv b/example/assets/data/bitcoin_2023-01-01_2023-12-31.csv new file mode 100644 index 0000000..8f1db7b --- /dev/null +++ b/example/assets/data/bitcoin_2023-01-01_2023-12-31.csv @@ -0,0 +1,366 @@ +Start,End,Open,High,Low,Close,Volume,Market Cap +2023-12-31,2024-01-01,37100.9041846051,37694.17078259951,36977.382178671105,37174.6564186542,22825350707.305206,731910439496.2489 +2023-12-30,2023-12-31,37039.794687585294,37479.209036563574,36572.16308778603,37152.43478337427,30594016263.905033,726109862996.9537 +2023-12-29,2023-12-30,37473.67123601244,37887.51837863061,36698.41437539069,37027.90910611624,31014082955.47395,731231275147.4174 +2023-12-28,2023-12-29,38241.00437565481,38500.50623772924,37260.03891427415,37498.622152963035,31118209903.51097,740038242927.5472 +2023-12-27,2023-12-28,37425.46860005459,38407.71946506079,37112.99226116585,38196.07687770177,29646539917.820133,738226351479.7245 +2023-12-26,2023-12-27,38364.96658830988,38364.96658830988,36733.70134615216,37435.0034776331,28343694802.52816,736382893906.3896 +2023-12-25,2023-12-26,37833.46979741687,38496.614810314924,37650.00044020673,38398.246216423235,24222883364.049984,746958065848.8574 +2023-12-24,2023-12-25,38475.94270269319,38674.6960372591,37725.89207892026,37873.53741316923,18441594555.91457,751912373357.4268 +2023-12-23,2023-12-24,38700.12237746846,38716.8678411382,38166.85595555673,38498.12031730101,23689451646.33951,752865941432.9445 +2023-12-22,2023-12-23,38616.4919045984,39053.898910928576,38236.311772008135,38693.193523678725,31635908335.28064,754925466509.3234 +2023-12-21,2023-12-22,38440.60290712519,38919.56542792496,38118.477236910454,38605.6099944534,36900311517.69093,753656559272.9637 +2023-12-20,2023-12-21,37215.8861801502,38984.67200197213,37175.78334786016,38426.48107551306,32216573879.423225,744105485376.4031 +2023-12-19,2023-12-20,37556.46531611245,38134.870535203336,36842.32675664493,37241.62946919874,35321133560.19876,736112600653.7316 +2023-12-18,2023-12-19,36426.79802435224,37606.83376913799,35797.63697032126,37563.77274768231,29230001499.750675,713473537007.7267 +2023-12-17,2023-12-18,37222.12831145507,37345.72075046442,36359.728128329065,36492.03666041573,21485318327.465748,722965909668.2908 +2023-12-16,2023-12-17,36919.785531285495,37591.80511168045,36736.07846244597,37192.72250248717,26013472004.2771,728624719756.068 +2023-12-15,2023-12-16,37877.340799239326,37927.057746317674,36750.499634628424,36936.354912266805,33576481436.623196,731848194136.2754 +2023-12-14,2023-12-15,37740.55976686652,38172.61385946841,36923.113494096826,37861.44933660847,37815134507.75901,738173146604.9332 +2023-12-13,2023-12-14,36530.431490628005,38169.70849510931,35827.42135706928,37775.78510868704,33444797308.24787,717054467420.6472 +2023-12-12,2023-12-13,36324.81973534772,37017.82837220359,35833.34653953496,36548.25986283159,43182665596.03501,715787192578.017 +2023-12-11,2023-12-12,38552.8468168652,38566.00019369096,35546.86000545857,36291.62814857858,38616502126.022385,722335599913.6361 +2023-12-10,2023-12-11,38486.04104487468,38767.23629416374,38394.847820536525,38529.251736615515,22433123426.065327,754829380952.9645 +2023-12-09,2023-12-10,38888.77736985289,39041.37062764675,38412.420872841896,38475.16793886409,30769814401.870342,758255680218.3878 +2023-12-08,2023-12-09,38100.0061628941,39278.77411232315,37972.22295590009,38913.68426613138,34997013565.24524,751988730200.5594 +2023-12-07,2023-12-08,38532.67654490549,38778.646452374036,37867.55060176259,38077.87256895839,36189011173.544044,750162704788.3457 +2023-12-06,2023-12-07,38801.83654244033,38975.89427995387,38257.48571529191,38486.85982937588,51559637064.33903,755648237085.3834 +2023-12-05,2023-12-06,36969.9338809505,39102.67381562382,36482.48417456838,38813.62527843076,43605395400.09655,730195971787.5426 +2023-12-04,2023-12-05,35192.0798006744,37317.47708724017,35192.0798006744,36973.66683394523,39795773948.88418,713444181714.2103 +2023-12-03,2023-12-04,34735.594235052784,35312.09776110862,34589.89461451097,35164.654921951354,23114925704.107597,681002113706.5303 +2023-12-02,2023-12-03,34056.645800868086,34910.770097637855,34020.61047868079,34741.11442733508,24968442438.03418,669739373202.578 +2023-12-01,2023-12-02,33202.4686792918,34300.124138295345,33125.24761628061,34035.07567153536,25657764864.129047,661256575066.8969 +2023-11-30,2023-12-01,33327.751512110095,33582.437512655946,33042.7000519444,33204.810579047924,24769328951.891644,650494593059.8873 +2023-11-29,2023-11-30,33308.39122051716,33760.31624450842,33123.80373823548,33286.680225033684,60203429986.63117,653269996178.6052 +2023-11-28,2023-11-29,32787.53862813978,33696.67996090964,32483.8840319414,33259.28175871389,66111478083.01223,644450541405.1779 +2023-11-27,2023-11-28,32965.285297975934,33057.74631767078,32358.75086940828,32811.97890529393,54203204738.04888,639679531300.7247 +2023-11-26,2023-11-27,33262.61852565965,33292.52617028957,32732.662458290415,32996.231830467594,33237734083.827305,647139105881.2972 +2023-11-25,2023-11-26,33201.33294595142,33339.540248100515,33102.4184957256,33267.161459021154,61553322629.37676,649919275526.9122 +2023-11-24,2023-11-25,32834.35021085902,33789.24663021755,32805.97448561845,33201.09523432204,56329227390.823845,647902555273.643 +2023-11-23,2023-11-24,32932.128927744474,33134.38630781015,32498.657369500717,32834.49107700977,71524721313.82204,642293254021.1082 +2023-11-22,2023-11-23,31491.64927850119,33279.55768028666,31491.64927850119,32970.40050007484,93379003831.60822,630926624123.9825 +2023-11-21,2023-11-22,32970.43571661252,33105.50874690755,31643.987216396825,31643.987216396825,74216217634.24104,639043650485.0344 +2023-11-20,2023-11-21,32883.43326025902,33205.18915682805,32494.220085752277,32985.47317820449,53095880190.50191,641665094312.1991 +2023-11-19,2023-11-20,32193.955081306183,32998.195152443586,32047.058098483052,32917.34678605074,35501561032.696236,630488133929.0431 +2023-11-18,2023-11-19,32242.42184129668,32405.58006039637,31895.767852583576,32187.396001162153,57432245152.28768,627914596432.3691 +2023-11-17,2023-11-18,31841.147002632435,32285.799811591525,31643.652659288804,32147.42523088843,78771004869.51784,625600863201.4542 +2023-11-16,2023-11-17,33330.61285579709,33341.11618816196,31296.10064886471,31891.69153834641,89245282004.02216,635629883084.7542 +2023-11-15,2023-11-16,31299.771972918486,33335.67523308947,31158.166274882686,33335.67523308947,76584212183.93465,623679337045.7006 +2023-11-14,2023-11-15,32105.860912284414,32340.526311155718,30946.55890406135,31289.92895063522,64335580554.127945,623189647313.1923 +2023-11-13,2023-11-14,32631.71425301322,32872.6658038615,32059.58638176488,32162.98213641126,46106918187.29965,634269491678.048 +2023-11-12,2023-11-13,32692.233873026777,32756.706549395603,32358.988581037655,32606.13824251869,45702663616.12886,637760357716.0565 +2023-11-11,2023-11-12,32831.60332091951,32924.19640263068,32389.19556623791,32647.640932181755,67547247019.99921,637684718060.7434 +2023-11-10,2023-11-11,32302.58049179895,32967.09894966676,32037.743324265077,32858.03333245292,107693014611.56467,635362285488.1392 +2023-11-09,2023-11-10,31361.110377433244,33364.218236884044,31330.965021173946,32377.40683024749,95049049236.81912,629424558285.6077 +2023-11-08,2023-11-09,31169.708495109306,31606.28791280386,30956.366709806927,31413.4069358971,65338994513.963486,608786880752.1049 +2023-11-07,2023-11-08,30851.148499335293,31565.55118283546,30433.902960830408,31187.096660591815,53116956739.91035,601374828234.8928 +2023-11-06,2023-11-07,30824.956199431257,31057.78153420847,30583.969432045295,30831.576908516243,55593478416.07753,601906249554.1104 +2023-11-05,2023-11-06,30869.1089335552,31088.30546824789,30466.196525888558,30834.226952977122,37939776675.62549,602564889724.8782 +2023-11-04,2023-11-05,30565.137388517654,30998.036678024004,30456.529586293727,30831.79701187678,49263838985.33085,597828761317.721 +2023-11-03,2023-11-04,30752.154811899672,30752.154811899672,30049.162286609793,30576.785258357326,69933412493.12483,593762736096.3765 +2023-11-02,2023-11-03,31185.494308127098,31591.629028992018,30334.944489932474,30742.320593750832,87193998967.29948,604424394739.1691 +2023-11-01,2023-11-02,30500.136464083538,31270.013998573733,30102.73544456477,31191.516336071425,61577149209.30313,594761359227.5613 +2023-10-31,2023-11-01,30352.02451071023,30519.505559810892,30012.93327346522,30506.35218298513,56096644493.466354,591237341407.3635 +2023-10-30,2023-10-31,30393.940994691107,30661.577876971027,30006.206914767175,30362.615884419323,46228472890.55691,592151523212.1005 +2023-10-29,2023-10-30,30006.250935439286,30565.181409189758,29897.651937349783,30405.58886453079,32373537300.913456,589718265254.667 +2023-10-28,2023-10-29,29838.998793833587,30277.383059084546,29813.167463440837,30011.88558146906,48381101793.12706,585989312580.1788 +2023-10-27,2023-10-28,30067.554123416357,30149.75832651013,29440.303566554856,29854.837431657907,60334747804.30696,583852275406.8123 +2023-10-26,2023-10-27,30369.315830714102,30638.95125150771,29731.59715802541,30065.115378181596,75784058731.1894,590172065644.0789 +2023-10-25,2023-10-26,29859.40677742268,30899.22787741123,29745.648556562166,30342.771365433215,106577812777.00316,590122722188.2163 +2023-10-24,2023-10-25,29109.048008945003,30802.98988404955,28947.289647218335,29807.277497512834,152605324417.8083,586042854374.1233 +2023-10-23,2023-10-24,26402.753933247055,29989.70796686124,26317.468283105747,29002.104188126745,52209234683.26393,531074280079.6341 +2023-10-22,2023-10-23,26332.972363822053,26555.179912486903,26190.451035806418,26381.597598232132,37718159919.575455,514055713328.8206 +2023-10-21,2023-10-22,26121.056848295964,26592.90562848314,25946.4972751204,26319.220305855633,47749099551.780914,512008396415.1675 +2023-10-20,2023-10-21,25283.545953179615,26432.925701909622,25175.255099794867,26117.482369720823,57020497039.10846,505686274857.8469 +2023-10-19,2023-10-20,24933.326290025798,25413.362915224992,24812.753669123023,25264.317723603006,38390510017.338036,489396525224.86316 +2023-10-18,2023-10-19,25000.149670285165,25350.659869874897,24819.295140998216,24935.712210454032,45873609114.43736,488019880360.7977 +2023-10-17,2023-10-18,25092.50504036696,25176.707781974415,24755.236258947207,25010.27442486992,66287448095.436455,487757459872.5621 +2023-10-16,2023-10-17,23906.83464955143,25788.841640034163,23880.37822561475,25093.63196957291,40863666312.60596,479477459574.1294 +2023-10-15,2023-10-16,23641.275542994994,24010.080733912648,23615.514645677613,23900.24915700413,18395967060.974087,462817239100.3731 +2023-10-14,2023-10-15,23649.665883098707,23743.03372863897,23601.718567039083,23646.760518739604,25956344664.72817,461824260830.513 +2023-10-13,2023-10-14,23559.564371428824,23820.42207020417,23504.441685815662,23631.811098491857,25524542061.776657,460709148210.67285 +2023-10-12,2023-10-13,23662.264599455906,23708.415872093537,23399.100217462124,23540.890802320773,23928365175.032307,459794710703.0548 +2023-10-11,2023-10-12,24113.75822086052,24182.932305010436,23373.330516010323,23634.22343132335,23076947516.552383,464052681627.1808 +2023-10-10,2023-10-11,24289.5943935272,24394.275551799128,24037.68169532413,24114.823521125523,22640158012.774193,472752475967.7028 +2023-10-09,2023-10-10,24578.510868703943,24629.522023542257,24035.251754223784,24285.412429676977,20386854633.518124,475333242579.8633 +2023-10-08,2023-10-09,24613.392849282027,24735.206853138236,24420.670346794857,24592.095648116356,14807557012.392187,479473397980.7412 +2023-10-07,2023-10-08,24593.275402128842,24663.734889904303,24532.720565577598,24615.144872031906,22966215201.340775,479653811431.45325 +2023-10-06,2023-10-07,24131.991583247494,24849.64299234921,23989.18852293037,24592.588679643963,21332203552.015804,475783478783.8519 +2023-10-05,2023-10-06,24462.745305195323,24714.481920709968,24100.76331845435,24143.22565876936,17334073687.856873,474547071141.5178 +2023-10-04,2023-10-05,24147.34599367863,24492.952290395573,23995.976510569366,24456.652844175627,19514701948.7279,472505565004.0212 +2023-10-03,2023-10-04,24209.15982145216,24355.343669387144,23995.43065423523,24140.258665469308,27502514213.382385,471926706796.8163 +2023-10-02,2023-10-03,24643.714288229756,25073.26800665593,24100.64006057245,24307.449178134055,27752533373.83541,481516356300.7094 +2023-10-01,2023-10-02,23738.173846438287,24646.20585827105,23738.173846438287,24632.295325885036,16582476253.818794,466422622641.34186 +2023-09-30,2023-10-01,23689.275683861146,23842.3179525105,23671.561765405037,23742.20614000335,19192072934.68763,462960269129.40625 +2023-09-29,2023-09-30,23789.99498164338,23973.032936266874,23509.63612512436,23692.533213597108,29559383346.88192,462775165650.66345 +2023-09-28,2023-09-29,23216.79300599562,23978.024880483877,23181.91982955196,23789.04413512586,25508406408.449966,457996126198.6249 +2023-09-27,2023-09-28,23079.941540547443,23602.598980481238,23042.6208147346,23214.77685921309,25079537938.08517,451939455383.10156 +2023-09-26,2023-09-27,23159.05549245926,23231.038095489643,22999.98239173116,23082.133770018405,27117977046.606644,450494273120.35095 +2023-09-25,2023-09-26,23112.296734546544,23260.505533398486,22916.34311472668,23162.313022195227,26174674174.282246,449741019393.3438 +2023-09-24,2023-09-25,23397.841226239845,23514.724914820003,23101.00983421815,23114.47135574866,18100283816.081142,455797753127.80634 +2023-09-23,2023-09-24,23401.47733375593,23447.83110148526,23352.87851174912,23401.970365283538,24198618520.692043,456094622917.11304 +2023-09-22,2023-09-23,23391.282146095808,23535.766796087446,23328.711162762036,23399.285104284976,28851926657.84297,456670057967.75085 +2023-09-21,2023-09-22,23881.188205981533,23892.92411716542,23258.647861035544,23381.12217497337,30564553490.435776,459535430591.6311 +2023-09-20,2023-09-21,23953.927964572165,24075.601102277633,23631.74066541648,23878.432511907595,31388357323.28207,465402214345.0409 +2023-09-19,2023-09-20,23567.954711532537,24177.103968023388,23486.22593169753,23957.106257098338,31555589119.658092,464048509925.20953 +2023-09-18,2023-09-19,23355.581381016527,24119.55134130988,23255.900971096027,23541.146122218997,24155391190.807037,460755193940.43945 +2023-09-17,2023-09-18,23391.079651004115,23427.89854115493,23269.37129676096,23358.60119912311,18621474753.383358,455117398247.3353 +2023-09-16,2023-09-17,23418.988757120347,23526.311155718726,23298.627435443686,23377.732583221084,24112777794.138798,455641189245.16595 +2023-09-15,2023-09-16,23348.53807347931,23603.752322090455,23101.212329309845,23443.376209467966,31174206698.82744,454658876788.8168 +2023-09-14,2023-09-15,23086.22769252441,23534.182051891574,23038.20994338942,23355.801484377065,31858953598.34757,453509735027.5423 +2023-09-13,2023-09-14,22749.927365891024,23220.367484570757,22713.68074447761,23086.21888838999,33302428243.819138,447496303003.1055 +2023-09-12,2023-09-13,22154.160393720893,23255.548805719165,22129.033394081864,22762.464453307275,37717367105.9563,443820194977.5929 +2023-09-11,2023-09-12,22751.29200672636,22796.32515429246,22013.452717396092,22136.789836507225,26237940449.99949,437426260713.0176 +2023-09-10,2023-09-11,22804.08159671782,22866.64377591717,22595.097857954097,22751.33602739847,18796303396.46387,442848670825.67865 +2023-09-09,2023-09-10,22811.952492890665,22814.787424174396,22729.792310468998,22796.659711400476,21618150728.811142,443753149079.4911 +2023-09-08,2023-09-09,23111.57479552398,23227.08503913438,22649.0495936892,22811.961297025082,29796500435.95445,446321289814.0323 +2023-09-07,2023-09-08,22682.804645061326,23247.11444494335,22559.705237579572,23067.677381298257,28494914658.3305,443059951530.9198 +2023-09-06,2023-09-07,22703.344690666738,22804.583432379848,22443.182518510694,22689.689478178956,27257283002.285652,441197002289.6528 +2023-09-05,2023-09-06,22737.566361163204,22749.522375707635,22515.10349260013,22704.718135636496,27041739259.51238,441364803457.4081 +2023-09-04,2023-09-05,22865.402392963737,22935.967530352256,22607.001047691996,22735.9287921608,22869495931.358814,444115648353.52606 +2023-09-03,2023-09-04,22775.846737627995,22941.170773795377,22741.140839738342,22873.669475185547,21893229754.66974,444319628243.0855 +2023-09-02,2023-09-03,22720.85611403115,22843.198365952656,22679.467878115567,22764.788744794558,31390071976.39734,442763597712.851 +2023-09-01,2023-09-02,22837.299595890232,23006.840812445527,22363.293802769786,22729.255258269284,39071061119.61872,444017520548.75806 +2023-08-31,2023-09-01,24035.28697076147,24160.772298671458,22738.834156519904,22834.79922171452,32844170485.05034,460528503233.1136 +2023-08-30,2023-08-31,24403.114902758338,24423.065071357512,23845.24972927287,24049.576080927607,45220944141.72896,468768582595.0272 +2023-08-29,2023-08-30,22987.260417492056,24656.34822112464,22822.975269186412,24403.502284672883,35520389861.394196,457624962436.01416 +2023-08-28,2023-08-29,22980.393192643267,23053.546745551714,22800.982541401445,23003.34557108018,24304377968.014336,446133914377.1852 +2023-08-27,2023-08-28,22906.051081587917,23040.446193532483,22871.12508033773,22968.56043598074,18955229866.543385,446700125105.1025 +2023-08-26,2023-08-27,22943.583106626873,22976.792301664864,22890.94318692058,22909.25578651735,24865956559.17797,446225808983.5095 +2023-08-25,2023-08-26,23049.276740357276,23100.217462120214,22744.222286785876,22942.00716656542,30037425915.061382,446408112637.7328 +2023-08-24,2023-08-25,23275.43734537739,23380.822834403036,22859.917417219127,23015.35441043114,42977907165.9204,450521201963.97314 +2023-08-23,2023-08-24,22939.16343114727,23575.719958092322,22719.121699550113,23258.234066717734,43383092390.47352,448500598651.46344 +2023-08-22,2023-08-23,23002.482765906872,23008.76891788384,22533.116751626567,22933.08857839642,37643078943.280106,445245386861.9693 +2023-08-21,2023-08-22,23058.045658241113,23068.4697533962,22784.642067915094,22999.436535397024,34314443969.299377,446683015899.0043 +2023-08-20,2023-08-21,22978.755623640864,23126.11042145392,22892.536735250877,23049.223915550745,31910613041.00184,447634312271.3188 +2023-08-19,2023-08-20,22932.07610293794,23113.72300432283,22727.21269908349,22970.54136622558,42014144416.708374,445536766622.16 +2023-08-18,2023-08-19,23448.306524744025,23581.196129702512,22645.580764727118,22942.042383103108,61897504430.57366,450366064187.7128 +2023-08-17,2023-08-18,25294.700791491687,25328.12128575579,22413.891163290282,23427.836912213977,42505575972.25768,482804368735.17365 +2023-08-16,2023-08-17,25708.06370671668,25758.634654833913,25299.992076279024,25299.992076279024,34846631160.32317,498614353877.3042 +2023-08-15,2023-08-16,25911.377582912937,25931.94404092162,25638.66951920622,25708.77684160482,38178497714.62096,502344658978.5694 +2023-08-14,2023-08-15,25799.45942614652,26135.979856140446,25674.678428990257,25911.403995316203,34236979779.630074,503449625186.10657 +2023-08-13,2023-08-14,25910.75248936901,25942.711497319142,25780.019897343795,25783.00449891269,26565685412.909626,503182685149.8617 +2023-08-12,2023-08-13,25907.05475291197,25950.44152734124,25843.612160270466,25910.62923148711,26805714951.75579,503615627774.48846 +2023-08-11,2023-08-12,25936.997614079573,26009.18271220165,25788.225350624656,25907.05475291197,30873053717.781155,503522268480.82275 +2023-08-10,2023-08-11,26044.39924988775,26157.04814981115,25866.177156792834,25933.229444547163,37613754767.60387,505248823426.1632 +2023-08-09,2023-08-10,26210.304358926955,26475.467279434426,25897.343792645028,26057.5086060414,42095444658.87709,508685549050.27856 +2023-08-08,2023-08-09,25717.92433726878,26524.259792398512,25645.395877904266,26207.020416787727,40795121449.79394,504350378860.2248 +2023-08-07,2023-08-08,25564.54751151141,25775.66185080514,25339.88360934295,25713.363795638434,34950138428.58551,497968031827.2486 +2023-08-06,2023-08-07,25595.52045640633,25708.01968604457,25514.48720319062,25608.27764718312,34098405541.814384,497626207740.0998 +2023-08-05,2023-08-06,25632.3217382883,25657.061356012786,25520.887808915068,25597.624644533076,33386496620.91493,497473741689.09814 +2023-08-04,2023-08-05,25702.508297896697,25804.618648917534,25466.874444239016,25631.811098491857,37964263392.4641,499314188394.6163 +2023-08-03,2023-08-04,25696.292578995097,25882.605671623398,25546.25252018348,25706.602220402707,43407606729.66942,499739998044.42444 +2023-08-02,2023-08-03,26173.731984539943,26403.335006118876,25542.54597959202,25665.548541595137,50899463132.586235,504061977491.2613 +2023-08-01,2023-08-02,25736.43062782283,26085.98117676061,25336.72292508562,26085.98117676061,36516604718.71647,496783552059.5256 +2023-07-31,2023-08-01,25779.48284514408,25969.238354331195,25650.766399901397,25730.901631406112,30351262373.366047,502105893083.8211 +2023-07-30,2023-07-31,25842.705334425045,25921.176584524095,25624.97028604633,25773.135064226164,23355416427.081104,501554450991.99365 +2023-07-29,2023-07-30,25808.562901138375,25881.65482510587,25764.823961332244,25845.610698784152,29498960461.96211,501812172397.0363 +2023-07-28,2023-07-29,25728.436473768084,25981.062306859305,25642.965936803925,25812.50715335922,35771604624.151505,500788595760.8338 +2023-07-27,2023-07-28,25841.84252925174,26021.279592896826,25620.673868448626,25716.56850056787,35532742662.362305,502287175040.47455 +2023-07-26,2023-07-27,25733.534067598146,26114.365706135603,25647.341591611425,25839.711928721732,32297947948.80176,501006976765.9994 +2023-07-25,2023-07-26,25687.356382557253,25822.552670734178,25578.88944648407,25745.076287824762,40594580383.055664,499251495431.4797 +2023-07-24,2023-07-25,26486.164302756577,26489.862039213618,25484.09533116752,25695.81715573634,40955566127.43178,503589397904.38983 +2023-07-23,2023-07-24,26226.6448324133,26671.22720829702,26190.08126216071,26465.888381183806,32229056560.05227,512323826565.91626 +2023-07-22,2023-07-23,26349.885106045804,26398.16697921344,26118.68853613657,26190.52146888179,33817442477.28289,511249455898.51337 +2023-07-21,2023-07-22,26236.355792680246,26441.65940325577,26182.879480203905,26331.33479481965,41874611317.05755,510833813658.66077 +2023-07-20,2023-07-21,26335.499150401032,26766.725654367296,26118.353979028554,26233.547273799777,42179106918.17573,513145794249.1731 +2023-07-19,2023-07-20,26289.127774402863,26576.389072308357,26257.776251727813,26353.028182034286,43252280905.35482,512774770728.7629 +2023-07-18,2023-07-19,26533.88271132124,26608.321667855227,26139.48390164021,26276.352975357233,46104018937.32936,512146126948.31836 +2023-07-17,2023-07-18,26616.623966614723,26699.708583150652,26138.577075794794,26551.51739256755,36628645506.25057,516091318383.5585 +2023-07-16,2023-07-17,26662.731218580248,26789.273042620818,26518.04407349692,26607.036264229686,32412986794.828575,518073855435.81934 +2023-07-15,2023-07-16,26687.127475062294,26735.849554951008,26637.833126436177,26655.11564230563,49654035987.908264,518380422776.2224 +2023-07-14,2023-07-15,27688.985147425232,27754.87528943592,26343.722211950735,26681.50163316694,62283509099.12184,530455935923.90405 +2023-07-13,2023-07-14,26748.747611878538,27914.476638229313,26647.165508922993,27685.74522595811,48546316947.304565,525281242901.7217 +2023-07-12,2023-07-13,26961.666798728686,27164.320364843334,26629.328332584984,26750.349964343255,43310881653.048996,523021579491.683 +2023-07-11,2023-07-12,26774.763829094147,27067.466082072144,26736.28095753766,26959.633043677313,48495518289.476524,521714274089.6678 +2023-07-10,2023-07-11,26553.850488189255,27269.969977901623,26427.5199633748,26762.89585589393,32610150128.635128,517517291839.2705 +2023-07-09,2023-07-10,26663.021755016158,26754.047700800296,26487.352860903484,26557.2224716727,27276650771.651814,517438580915.8342 +2023-07-08,2023-07-09,26710.960266941358,26740.339663505983,26462.173036457923,26649.252088780893,32717958377.623024,516689607691.4478 +2023-07-07,2023-07-08,26328.341389116333,26774.129931415795,26259.36980005811,26694.17078259951,49395883681.209755,516073268227.3799 +2023-07-06,2023-07-07,26858.570384652634,27630.869056108753,26367.90716920666,26386.03488198058,44910171427.06215,521637224913.72565 +2023-07-05,2023-07-06,27100.77212258877,27179.56912566141,26609.017194474527,26855.180792900348,35737309871.83749,523045056093.0909 +2023-07-04,2023-07-05,27423.892659993136,27574.522595810995,27011.057992833437,27092.04722537704,41641136506.26084,529702129016.83746 +2023-07-03,2023-07-04,26955.600750112255,27585.316464611784,26924.372485319105,27428.435593354643,42857454084.87382,526946977576.47797 +2023-07-02,2023-07-03,26933.352702429063,27084.431649102422,26644.37459831137,26961.569953250048,35570091143.789734,522051066177.18134 +2023-07-01,2023-07-02,26829.71043201888,26970.54136622558,26709.261068998003,26931.010802672936,53518354331.487404,521587171492.8792 +2023-06-30,2023-07-01,26805.032443235345,27475.907486155502,26296.813783752852,26845.593090515307,55126480988.349075,522564188379.5851 +2023-06-29,2023-06-30,26480.58248153333,27094.670857434652,26480.58248153333,26816.601075865226,44883592535.44826,519632891275.9771 +2023-06-28,2023-06-29,27022.0455525915,27022.0455525915,26405.870596832276,26486.36679784827,47164030716.05982,517755567512.30383 +2023-06-27,2023-06-28,26645.721630877862,27264.000774763834,26621.959272074168,27037.303117544,48542551794.5241,521638973102.6143 +2023-06-26,2023-06-27,26819.752955988133,26973.437926450264,26421.753255328706,26653.460465034383,44496579093.19189,517419138350.3755 +2023-06-25,2023-06-26,26876.72450982982,27327.80433691662,26686.42314430857,26815.368497046216,55561262148.15256,523179986358.9628 +2023-06-24,2023-06-25,27012.246550980344,27105.288643547014,26688.84428127449,26888.310750728542,54609260634.920555,523198274002.565 +2023-06-23,2023-06-24,26313.453597809534,27638.792777088125,26283.766056540153,26945.57284100614,52496533222.419586,518408884606.56714 +2023-06-22,2023-06-23,26407.076763248024,26830.027380858053,26124.085470536967,26327.496192211867,60531026529.403496,514382842515.4831 +2023-06-21,2023-06-22,24916.307898188992,26991.776938450297,24881.293855594588,26464.972751203968,64278258015.63859,500629817856.7686 +2023-06-20,2023-06-21,23632.277717616194,24978.227375575574,23479.77250116655,24978.227375575574,50594213344.92178,464711674368.22394 +2023-06-19,2023-06-20,23191.243407904356,23743.02492450455,23179.1553313436,23579.074333306922,44447324741.22877,452879415149.83966 +2023-06-18,2023-06-19,23345.914441421693,23463.616914503054,23189.588230633108,23189.588230633108,38945227506.417854,452952080399.3464 +2023-06-17,2023-06-18,23175.140646047388,23561.087486683748,23062.747066022206,23348.573290016993,43060911228.82056,452350250365.1913 +2023-06-16,2023-06-17,22518.4050430082,23284.10061364817,22315.58419833954,23192.37033711031,41150698275.80504,440377580751.24585 +2023-06-15,2023-06-16,22124.67534754321,22634.822112464015,21901.75466399021,22525.75649525017,48147823426.45051,429190737719.0579 +2023-06-14,2023-06-15,22835.829305441835,22931.4862259317,21903.61233635315,22115.90642965937,36573505506.738464,441055649405.183 +2023-06-13,2023-06-14,22807.3039099161,23154.94396168441,22679.48548638441,22811.53869857285,29737395144.74303,443900918505.70056 +2023-06-12,2023-06-13,22825.176302791795,22962.60003697737,22594.006145285828,22791.782220930952,26575916499.03045,441822877976.3294 +2023-06-11,2023-06-12,22750.966253752766,23020.909819251123,22606.01498463679,22821.073576151364,29058090007.970802,441201905564.83356 +2023-06-10,2023-06-11,23311.437451027006,23331.3964237606,22443.420230140076,22752.38371939463,31534410313.951447,441069101603.6961 +2023-06-09,2023-06-10,23329.76765889262,23565.392708415875,23177.685040895205,23321.676659359237,25364752644.315662,452815229728.3705 +2023-06-08,2023-06-09,23189.55301409542,23562.284848965075,23097.769912751028,23325.198313127847,33484448097.007748,451898591102.03735 +2023-06-07,2023-06-08,23972.2669765722,24050.121937261738,23014.89659544122,23207.3373656269,47332110826.606125,455564585988.486 +2023-06-06,2023-06-07,22643.476576600373,24015.970699840647,22396.54701847988,23947.280843083914,57502402648.35089,445662272961.2254 +2023-06-05,2023-06-06,23875.729642640188,23879.79715274293,22404.47073945925,22653.49568157207,28937055550.049915,451455658641.9825 +2023-06-04,2023-06-05,23832.14037311922,24109.620277682403,23752.234049109466,23863.08690561088,17631113605.249027,463706874853.13605 +2023-06-03,2023-06-04,23984.777651585187,24045.878344470566,23730.250125458915,23824.69207539861,22317979380.7875,463346970144.49854 +2023-06-02,2023-06-03,23598.372995958907,24019.633219760002,23430.55738975023,23976.167208120936,30891652595.5322,461875384495.83984 +2023-06-01,2023-06-02,23956.401926344613,24051.292887139803,23485.25747691116,23601.22553551148,37465818215.37505,459156162639.7504 +2023-05-31,2023-06-01,24383.279187906643,24477.192889781043,23672.38935404066,23969.933880950495,38290511439.00398,464330556574.12054 +2023-05-30,2023-05-31,24419.517005185637,24677.610205752622,24305.51226856132,24372.309236417423,36514741080.708725,474154528118.54065 +2023-05-29,2023-05-30,24708.768037470396,25016.824700879537,24257.69701451802,24424.755465166443,39483580785.70725,475563101322.8274 +2023-05-28,2023-05-29,23642.81626651876,24785.83943019642,23585.624609316535,24739.890652650487,28037199018.213676,466073175607.2744 +2023-05-27,2023-05-28,23514.821760298637,23644.673938881704,23441.668207390194,23639.796448412177,29372761816.39259,456002991338.49567 +2023-05-26,2023-05-27,23307.845364183024,23688.861889543332,23195.77753713144,23509.86503261932,34035357071.908665,453435882138.5539 +2023-05-25,2023-05-26,23179.287393359922,23370.759708759233,22858.640817728006,23308.083075812403,39237469901.41995,448788329748.1786 +2023-05-24,2023-05-25,23966.975691784864,23966.975691784864,22991.125432503104,23187.12307299508,35865786338.89975,454098014047.25024 +2023-05-23,2023-05-24,23638.440611711263,24148.798675858186,23610.989320584948,23967.275032355195,35257470492.67753,464554384604.61676 +2023-05-22,2023-05-23,23549.06984319837,23804.46897863237,23375.505137212436,23640.324696477466,30359248672.39156,457363028373.6618 +2023-05-21,2023-05-22,23861.334882860992,23985.349920322584,23500.418196385024,23552.353785337596,24404393253.413197,460190935452.12476 +2023-05-20,2023-05-21,23671.99316799169,23895.177975577335,23621.686343907102,23868.47503587685,26678521743.73892,459327953680.1832 +2023-05-19,2023-05-20,23613.806643599837,23837.90708116532,23504.77624292368,23653.96230069641,36795414760.22919,458277494104.8494 +2023-05-18,2023-05-19,24125.309245221557,24170.37760932534,23318.63923298381,23628.483135680515,36602278490.10868,463194551590.3905 +2023-05-17,2023-05-18,23800.929716594917,24171.53095093456,23415.000484227396,24125.687823001685,33129300727.090054,460685185369.69604 +2023-05-16,2023-05-17,23910.849334847648,24025.74328904854,23675.55884243241,23793.85119252001,35654896563.96033,461659473214.2678 +2023-05-15,2023-05-16,23698.60806634796,24311.208543532048,23554.86296364773,23940.351989294173,32882424550.297947,466019909154.6379 +2023-05-14,2023-05-15,23582.402296118256,23891.471434985873,23486.322777176167,23688.553744838577,26094609827.80153,458036640530.36786 +2023-05-13,2023-05-14,23590.704594877756,23772.342692128226,23511.749117385527,23585.448526628108,35786708846.81613,457228306838.3669 +2023-05-12,2023-05-13,23743.517956032152,23806.546754355848,22850.919591840327,23588.00172561035,38513179211.37517,451709643920.6535 +2023-05-11,2023-05-12,24298.354507276617,24298.354507276617,23566.915823670795,23773.170280763847,39326646801.15533,465016213492.2535 +2023-05-10,2023-05-11,24325.788190134088,24903.8764603858,23737.32864953382,24321.016349277623,30550598742.67178,472714106389.53033 +2023-05-09,2023-05-10,24358.627611526375,24469.999911958657,24102.841094177827,24331.68696019651,30787400154.366848,470513739840.65625 +2023-05-08,2023-05-09,25019.57159081905,25195.768732997018,24037.443983694746,24381.28064939296,29594475642.356377,475945103468.40405 +2023-05-07,2023-05-08,25398.316649498607,25605.68923166319,25041.64355581381,25041.64355581381,35407051858.390755,492300594389.95355 +2023-05-06,2023-05-07,25977.40859107437,26218.756327971616,25046.195293309742,25412.500110051682,35520055736.3827,496215685481.4963 +2023-05-05,2023-05-06,25402.340138929245,26099.812471936824,25392.488312511556,25998.31841032549,37873008418.54697,498841695328.4866 +2023-05-04,2023-05-05,25551.473371895445,25812.68323604765,25267.152654886737,25374.09647570499,41606425999.33651,493912718844.6995 +2023-05-03,2023-05-04,25241.312520359563,25558.38461741634,24810.226882544044,25530.93332629003,36211733788.98642,485942118832.9174 +2023-05-02,2023-05-03,24710.669730505448,25370.363522710264,24583.05380206545,25214.671209600034,36572169771.8268,481581861626.9448 +2023-05-01,2023-05-02,25699.963903048876,25820.54532808607,24377.292376500005,24725.93609959237,37300089162.987885,484505246346.859 +2023-04-30,2023-05-01,25734.91631670233,26311.560708908906,25617.35470977171,25805.393412746627,24077378967.772713,499911122239.7572 +2023-04-29,2023-04-30,25803.60617345906,25906.183143604238,25622.55795321483,25722.837044275995,31315994131.142597,499106452661.96985 +2023-04-28,2023-04-29,25948.249297870283,26020.240705035085,25488.717501738818,25819.981863483095,47109603848.89244,499793483132.2353 +2023-04-27,2023-04-28,25017.168062121975,26299.701539843114,25000.651505947197,25943.38061153518,71677652429.86902,496624934284.0143 +2023-04-26,2023-04-27,24913.772307475592,26382.020196684367,24145.928528036766,24983.131278448363,49564702241.22388,491297450411.9357 +2023-04-25,2023-04-26,24230.413001945715,24964.88030779254,23971.994048405133,24939.32190556686,34981156157.043015,469038219867.1805 +2023-04-24,2023-04-25,24290.369157356297,24622.566757349254,23878.39729536991,24237.07773170281,31603141629.209362,468323041124.57355 +2023-04-23,2023-04-24,24492.07187695342,24492.07187695342,24123.47798526188,24289.3478777634,26597454317.443584,469692277334.8871 +2023-04-22,2023-04-23,24002.59721965435,24522.366903497885,23927.22502487168,24493.67422941814,42140226710.00441,467105424449.3534 +2023-04-21,2023-04-22,24863.183751089513,24959.976404919755,23964.149564635554,24012.836427986585,43332995025.95141,476105456911.35254 +2023-04-20,2023-04-21,25364.042154195613,25575.52626713505,24691.247809971566,24886.796439608042,46854819417.56738,488226638110.0043 +2023-04-19,2023-04-20,26747.00439326308,26754.831268763814,25257.961138550665,25360.291592932044,42726117135.300354,503624905542.0253 +2023-04-18,2023-04-19,25927.51556130759,26794.036079342863,25707.253726349896,26735.136420062863,35078485033.86719,510080335021.3397 +2023-04-17,2023-04-18,26683.05562499164,26683.41037119355,25796.527022721933,25896.44577093403,30413874493.467194,505882380384.1647 +2023-04-16,2023-04-17,26672.22201082216,26876.53202743304,26560.51093972515,26680.81894367368,20664570044.89507,516202091989.9173 +2023-04-15,2023-04-16,26824.267980243527,26867.397145699622,26630.81870949189,26674.617807485145,32420176954.47589,517265526531.9535 +2023-04-14,2023-04-15,26747.587755209846,27253.962828944477,26440.27944322654,26833.498322812393,42322583004.10012,520818807710.5643 +2023-04-13,2023-04-14,26309.596946726186,26856.00098606306,26289.006013223814,26728.98338659835,37768163431.34292,514437324705.1563 +2023-04-12,2023-04-13,26591.815500559067,26791.727195090818,26205.726297069108,26304.480191497943,39101589391.99627,510641018981.0643 +2023-04-11,2023-04-12,26114.324590827855,26801.963528439006,26055.363654772285,26614.96280253207,46442807113.34933,512418853734.39136 +2023-04-10,2023-04-11,24936.629753728204,26177.367387725277,24829.101890247664,26068.89094318692,28812160880.883,486876167745.64746 +2023-04-09,2023-04-10,24597.322398598386,25072.078392012892,24490.12008165725,24934.947659420868,20244184194.198223,476860612926.24866 +2023-04-08,2023-04-09,24571.994928818574,24778.837358671717,24528.2885643098,24593.501844466162,20594665247.04891,476485800333.49255 +2023-04-07,2023-04-08,24681.353811750003,24737.82344188831,24488.67529759025,24565.522745481278,25000710267.47939,475537911883.74646 +2023-04-06,2023-04-07,24804.86208323429,24804.86208323429,24443.688139950522,24668.955565533575,31057253793.26132,476783895433.3782 +2023-04-05,2023-04-06,24793.972425450997,25251.186092989272,24557.763661815592,24806.871714957346,32202519639.969597,482591072217.77655 +2023-04-04,2023-04-05,24482.33303883514,25008.495021261988,24383.295299472637,24783.357896868372,36119664292.48509,477862765643.04565 +2023-04-03,2023-04-04,24801.93092276133,25028.159671781872,24128.594249363887,24441.561386107987,31711839050.36091,476301168595.3388 +2023-04-02,2023-04-03,25050.174944281793,25105.820061100694,24545.094600424363,24817.252669853766,22992322073.236702,481185912965.14294 +2023-04-01,2023-04-02,25063.50673082909,25346.581618728156,24920.31950203816,25060.80488962448,32185370169.049168,484116903561.2133 +2023-03-31,2023-04-01,24676.654189167974,25192.183513377884,24354.92141185431,25062.90257280921,39686593813.041275,479880975275.21277 +2023-03-30,2023-03-31,24958.43893892572,25669.66949279382,24453.261491596455,24679.39321857439,40726322677.00468,482931724053.19696 +2023-03-29,2023-03-30,24005.944375478728,25167.49962582429,23993.645968146644,24978.00656788428,37401859519.2747,477884759284.1433 +2023-03-28,2023-03-29,23886.679520702924,24163.970312458732,23486.74563836429,23999.384326879906,37911689041.10259,459866060072.7386 +2023-03-27,2023-03-28,24625.603157924976,24666.20488981626,23505.700412913906,23909.889772237046,30499596068.66051,468692435550.3266 +2023-03-26,2023-03-27,24205.735893575624,24774.136954090114,24150.545152003382,24637.127945199052,23869208108.37213,472465442703.39746 +2023-03-25,2023-03-26,24168.147541156774,24429.17698951428,23932.864601216734,24180.214116549134,37255562615.297485,467733647943.31494 +2023-03-24,2023-03-25,24912.67945026985,24965.252564988234,23868.40777228987,24137.35690586082,42593077676.13894,475579690293.32983 +2023-03-23,2023-03-24,24001.613629382642,25196.91670408424,23909.203489958887,24931.740313251106,52679587701.99375,472183352777.1401 +2023-03-22,2023-03-23,24760.58244099669,25273.712879568247,23523.074755905374,24012.22726992596,54921393105.188866,476633909972.72534 +2023-03-21,2023-03-22,24408.416400341604,25003.537237086537,24074.382698115038,24756.829112366184,66694066147.22274,474968609964.99976 +2023-03-20,2023-03-21,24616.81457612495,25031.52469999912,23919.06796802361,24376.952272787305,71453761862.25052,473605528903.2075 +2023-03-19,2023-03-20,23683.888170048165,24942.14638899651,23638.275710273545,24573.93447963164,52781997781.74943,466373590862.51794 +2023-03-18,2023-03-19,24113.437750367575,24330.80927603603,23612.80914953115,23675.750745374982,76757484842.70436,464814597562.4822 +2023-03-17,2023-03-18,22009.16114207232,24396.27109690711,21921.64038632542,24044.246586196878,74054539983.38954,447028314304.4 +2023-03-16,2023-03-17,21382.337337619712,22104.59954394584,21290.45614220438,22020.18083692102,81518843434.22475,419195822718.1983 +2023-03-15,2023-03-16,21741.366313620878,22034.09585941558,21103.771871958463,21373.50298233769,85937568532.06711,418272434426.27576 +2023-03-14,2023-03-15,21226.953153200746,23123.150119296024,21141.476298983023,21741.541515895868,83169799179.77708,421258633424.11163 +2023-03-13,2023-03-14,19365.885582965453,21431.706014885873,19217.835078509528,21185.883844353382,71967897149.91635,390465377832.16754 +2023-03-12,2023-03-13,18006.41187501651,19327.538282987847,17862.282295766094,19327.538282987847,38976688115.4946,350299882390.5809 +2023-03-11,2023-03-12,17742.91483740217,18165.802276749164,17437.48914890433,18044.017854784608,57714238691.82302,343777181524.8877 +2023-03-10,2023-03-11,17934.027891497848,17934.027891497848,17253.347596031097,17726.015181430874,63255284328.53951,339446115666.1145 +2023-03-09,2023-03-10,19108.494602295654,19195.45303441425,17798.921317450677,17921.573122738442,39828650391.85301,364244100270.77386 +2023-03-08,2023-03-09,19543.117191833288,19613.21773504838,19070.178812167353,19098.76004968101,41529480864.405365,374935481318.8341 +2023-03-07,2023-03-08,19723.392056909925,19810.32994510888,19358.991104612676,19540.789026526858,30980249574.907043,378346551834.6299 +2023-03-06,2023-03-07,19746.492168722434,19883.717369676804,19662.481797452085,19729.620277682403,23981191044.434467,379263124190.24915 +2023-03-05,2023-03-06,19674.626235953507,19905.608029399256,19602.661491476218,19752.069940043846,21816197668.078262,379305646753.42096 +2023-03-04,2023-03-05,19681.54829507937,19719.21696028455,19535.346574751504,19667.589348354864,27400161130.212814,377800267343.8355 +2023-03-03,2023-03-04,20660.376552829213,20660.376552829213,19537.092522648636,19662.948418215325,43532712356.21137,379281359917.18665 +2023-03-02,2023-03-03,20812.077863764825,20900.32997895812,20456.388279936262,20659.138163281477,36596145293.260826,396377333913.7873 +2023-03-01,2023-03-02,20361.18354717729,21013.699233159896,20290.34089844126,20830.05388130266,38371463209.96785,399147273439.63617 +2023-02-28,2023-03-01,20685.525403620784,20759.553366260796,20307.389643474464,20371.954248539718,37526166083.9308,395490695061.9871 +2023-02-27,2023-02-28,20739.24381289454,21021.47724571459,20403.52004032026,20696.992242819484,33311851214.041225,396733506349.3921 +2023-02-26,2023-02-27,20394.67200197213,20828.01142885958,20309.530035304582,20717.18804750711,27084924813.67028,393515612741.2198 +2023-02-25,2023-02-26,20417.171055527677,20426.039372089137,20098.4201861194,20399.191076129355,38602331483.17056,389930595803.2417 +2023-02-24,2023-02-25,21075.622056117554,21222.564732398336,20213.91599915546,20423.61665037902,46433799486.816795,399820216124.87146 +2023-02-23,2023-02-24,21296.865904228627,21642.93908820758,20843.348212320507,21085.111768486484,51893778176.11595,408195251571.0755 +2023-02-22,2023-02-23,21528.973261843767,21540.35559898929,20787.659596946727,21299.825237931735,60469343557.085526,406328143309.47797 +2023-02-21,2023-02-22,21869.814676779886,22097.959025558404,21284.3322504248,21505.13008108608,50113887831.805824,417804781543.56604 +2023-02-20,2023-02-21,21390.036008909785,22055.91287663794,21048.470424220603,21831.80748879674,50744906105.86287,417182688253.78845 +2023-02-19,2023-02-20,21686.841604817622,22112.27701433833,21414.805824815336,21422.467710837012,34497650437.90787,416902687188.9453 +2023-02-18,2023-02-19,21629.08516239226,21848.346253694923,21533.984134949773,21696.94135566062,67931837505.41699,416390804377.7645 +2023-02-17,2023-02-18,20733.699937490648,21991.646901384895,20725.206060766137,21621.471082820495,80096829238.33476,406305898099.023 +2023-02-16,2023-02-17,21421.38135948529,22176.11482352113,20717.651761267094,20717.651761267094,76028607792.83286,416386315233.5043 +2023-02-15,2023-02-16,19547.517058010442,21415.585101895053,19420.21362144631,21415.585101895053,65557271357.12236,384112862044.59546 +2023-02-14,2023-02-15,19169.706886091208,19601.097865601445,19027.445063106745,19545.20535643538,60489824293.34594,370458563684.5219 +2023-02-13,2023-02-14,19179.759647130293,19267.053256209118,18871.8729827273,19202.68921047974,54211198801.07254,366771115465.38574 +2023-02-12,2023-02-13,19249.787644277756,19445.857087529603,19084.509712380233,19152.296758077904,42385051816.24402,369848619178.80035 +2023-02-11,2023-02-12,19048.20580544624,19271.590731007283,19024.96324273879,19250.92249720469,60979405414.15138,367128366356.34106 +2023-02-10,2023-02-11,19182.913387990153,19310.193867039965,18918.734757842285,19059.440761381546,86418304110.06342,368347999514.72815 +2023-02-09,2023-02-10,20215.911095850613,20235.447646214663,19153.053367001015,19192.29295567756,75239854715.34766,381273862168.0319 +2023-02-08,2023-02-09,20471.25837493287,20578.362078832222,20002.239771796838,20204.795788102092,68866132663.43654,390800414291.87976 +2023-02-07,2023-02-08,20035.015803421287,20533.54058265762,20035.015803421287,20479.48953628624,64439684977.896736,388848193972.0379 +2023-02-06,2023-02-07,20189.076548133846,20377.285159658397,19959.57977866406,20027.173960892036,56812938067.60989,387518883384.2577 +2023-02-05,2023-02-06,20535.292869531535,20625.534616174336,20107.726156202956,20195.08614057671,44688909948.378426,392404730197.3052 +2023-02-04,2023-02-05,20629.57203102577,20747.236734370465,20498.24619893725,20527.5504256799,60453208973.5651,395693601026.3599 +2023-02-03,2023-02-04,20662.40414498649,20853.325585695045,20489.449477474624,20627.875650405433,79991879999.25655,397186013933.75146 +2023-02-02,2023-02-03,20882.86019914952,21345.065194615392,20625.882570455087,20676.464611781692,84042423281.26178,402882985494.90594 +2023-02-01,2023-02-02,20364.769463740173,20925.160015143116,20096.36847063381,20871.13767024995,61999404164.35559,391906844887.4464 +2023-01-31,2023-02-01,20102.75657448738,20446.259563491018,20024.94211281618,20384.388861009134,65773835979.758316,388506867617.8045 +2023-01-30,2023-01-31,20909.344708275006,20942.085347279084,19961.602289804767,20086.17416338713,70285286870.9813,393845503290.1666 +2023-01-29,2023-01-30,20271.4816477818,21064.912002676458,20231.939991019783,20932.69494554643,53466700859.576996,396460139964.7565 +2023-01-28,2023-01-29,20312.422457586086,20408.14951120594,20158.02769780689,20256.05909335024,55273990459.05736,389495729002.0418 +2023-01-27,2023-01-28,20265.520456406328,20608.942201697937,19923.781992023454,20294.648671015908,65669458294.13049,389025826611.68445 +2023-01-26,2023-01-27,20316.4170123795,20462.32903435862,20152.91513694831,20256.407384907954,83623868150.84808,390267643746.4165 +2023-01-25,2023-01-26,19927.148233673994,20881.809249623624,19691.83046758758,20360.197300652388,72744799269.8438,384117711752.10767 +2023-01-24,2023-01-25,20177.172904395906,20382.177702649165,19870.16181999067,19910.249280914748,70055927452.3672,388494417589.56934 +2023-01-23,2023-01-24,20000.820545328086,20372.467900497788,19957.181092240917,20201.01608515359,64572625748.57153,386241464988.3065 +2023-01-22,2023-01-23,20053.926115703936,20295.418328447042,19693.658910224243,20006.473856122837,70183655674.60703,385821640844.49615 +2023-01-21,2023-01-22,19956.647825819007,20517.28530282823,19814.95151308384,20030.82098553481,88951565611.05096,387101592619.6832 +2023-01-20,2023-01-21,18554.66533840029,19974.252749090978,18418.205893487582,19955.23159275596,56740666707.75263,360407127935.07745 +2023-01-19,2023-01-20,18197.789997164433,18627.0127829207,18195.328614317285,18560.927431309003,68657352762.68848,352744511092.5284 +2023-01-18,2023-01-19,18609.041737128147,18972.631995985317,18088.947929453374,18212.681844792252,51485572813.6155,357360764717.6194 +2023-01-17,2023-01-18,18649.814184700208,18879.324129834364,18453.027691924246,18627.07413339452,45354634154.75908,358576535224.41675 +2023-01-16,2023-01-17,18378.4434999978,18849.938018893674,18249.268024110967,18639.275494171172,53385570771.838905,355906067766.5796 +2023-01-15,2023-01-16,18445.73052305363,18484.51549081525,18116.915471505417,18361.954158067874,55653036628.52521,351541131138.0992 +2023-01-14,2023-01-15,17539.56767986684,18620.389584401066,17539.56767986684,18474.574188038707,92814939124.49731,352179568955.9312 +2023-01-13,2023-01-14,16596.680753281744,17587.092173191177,16492.441298433747,17532.31511276283,72025203421.09099,322693049337.92865 +2023-01-12,2023-01-13,15797.813867928571,16753.083383957106,15780.971348986224,16603.42181488427,64064702965.91724,310299235733.6799 +2023-01-11,2023-01-12,15354.186058439327,15794.055888434514,15260.793308529328,15794.055888434514,38573027395.53161,295312732061.302 +2023-01-10,2023-01-11,15124.748275600083,15396.541295792507,15107.287005978007,15350.670123097383,39702602455.96221,292482790358.4664 +2023-01-09,2023-01-10,15067.467042614933,15310.735701899952,15067.467042614933,15138.514332748231,35543033249.69406,291618974132.7453 +2023-01-08,2023-01-09,14917.51554626762,15029.163265168514,14895.871061998501,15029.163265168514,19171962320.652447,286693207483.90546 +2023-01-07,2023-01-08,14924.027648667123,14944.491253092454,14887.568363568911,14918.200434924242,30203802038.450592,286541536956.2488 +2023-01-06,2023-01-07,14818.343590149936,14971.868222519943,14709.816636882206,14926.50490529431,30777557239.29736,284726799678.17206 +2023-01-05,2023-01-06,14836.844234058593,14858.84912354842,14774.627188927923,14816.342586478611,34300957648.00484,284767408236.67706 +2023-01-04,2023-01-05,14680.461600767721,14943.592959247419,14666.309170060815,14841.378295556884,34674346927.11253,284713591023.36285 +2023-01-03,2023-01-04,14679.792527689058,14758.764332738443,14629.506511623187,14681.694476510456,26102786070.294167,282363884113.50854 +2023-01-02,2023-01-03,14630.054142251183,14754.430951681778,14579.726483621926,14683.051111112043,21886784819.715786,282353839676.1603 +2023-01-01,2023-01-02,14559.796976660242,14633.497178274918,14533.328579100746,14621.087186672692,20697449792.858284,280118415694.3652 \ No newline at end of file diff --git a/example/assets/data/btc_last_year_price.json b/example/assets/data/btc_last_year_price.json new file mode 100644 index 0000000..3c42a29 --- /dev/null +++ b/example/assets/data/btc_last_year_price.json @@ -0,0 +1,4 @@ +{ + "url": "https://api.coingecko.com/api/v3/coins/bitcoin/market_chart?vs_currency=eur&days=365", + "prices":[[1702944000000,39078.47891755909],[1703030400000,38477.47402170458],[1703116800000,39864.87762044923],[1703203200000,39838.679877640185],[1703289600000,39908.58005878162],[1703376000000,39678.979321595696],[1703462400000,39062.32681431535],[1703548800000,39597.85927945271],[1703635200000,38502.06842500796],[1703721600000,39086.65066359461],[1703808000000,38491.64747698571],[1703894400000,38057.70863986569],[1703980800000,38189.68296890573],[1704067200000,38240.20908960317],[1704153600000,40022.56708386281],[1704240000000,41121.44236936086],[1704326400000,39193.97344095043],[1704412800000,40376.019880448854],[1704499200000,40269.190802082194],[1704585600000,40125.3447966974],[1704672000000,40118.34311925359],[1704758400000,42857.43103594771],[1704844800000,42178.411061608065],[1704931200000,42492.38965139214],[1705017600000,42171.814324539526],[1705104000000,39122.52373887039],[1705190400000,39081.06524302686],[1705276800000,38190.210045708205],[1705363200000,38908.89747537756],[1705449600000,39668.76253090919],[1705536000000,39242.29028152058],[1705622400000,37938.77972050555],[1705708800000,38150.14288219012],[1705795200000,38173.221525259156],[1705881600000,38141.44573510522],[1705968000000,36313.41944934261],[1706054400000,36689.35973905385],[1706140800000,36871.20615880076],[1706227200000,36811.15855147119],[1706313600000,38535.125270419456],[1706400000000,38771.39629759249],[1706486400000,38759.18018541519],[1706572800000,39930.07569963493],[1706659200000,39555.50605607913],[1706745600000,39390.4556124346],[1706832000000,39601.98542555142],[1706918400000,39978.18836096943],[1707004800000,39798.71036813004],[1707091200000,39517.8500683694],[1707177600000,39701.74708708336],[1707264000000,40053.71777001113],[1707350400000,41066.29173159883],[1707436800000,42076.0465628706],[1707523200000,43713.572063470674],[1707609600000,44293.80084671968],[1707696000000,44634.239554822176],[1707782400000,46463.69349243575],[1707868800000,46442.363953357555],[1707955200000,48260.30672871491],[1708041600000,48219.73170515296],[1708128000000,48405.75606037122],[1708214400000,47958.58062660045],[1708300800000,48348.85208766836],[1708387200000,48043.59788234124],[1708473600000,48367.898535816996],[1708560000000,47916.29005429376],[1708646400000,47410.49380112791],[1708732800000,46933.94401263771],[1708819200000,47591.24123045009],[1708905600000,47833.38664994704],[1708992000000,50208.13598783667],[1709078400000,52573.782674551876],[1709164800000,57717.736388579004],[1709251200000,56734.564615792624],[1709337600000,57560.48389994141],[1709424000000,57229.6765848964],[1709510400000,58157.09485482637],[1709596800000,62819.675032831015],[1709683200000,59216.16978710529],[1709769600000,60688.021041080945],[1709856000000,61130.91471064688],[1709942400000,62425.38416118097],[1710028800000,62601.51789297492],[1710115200000,63133.50171410394],[1710201600000,65996.21012712058],[1710288000000,65396.96618741416],[1710374400000,66750.17623162274],[1710460800000,65624.65332089699],[1710547200000,63816.32105192292],[1710633600000,59954.666681584524],[1710720000000,62871.803645507665],[1710806400000,62274.42066839416],[1710892800000,57178.75390064521],[1710979200000,62011.64214760789],[1711065600000,60327.113345413214],[1711152000000,58488.654960729786],[1711238400000,59204.01008039168],[1711324800000,62277.19869537941],[1711411200000,64530.77647863455],[1711497600000,64695.8250958296],[1711584000000,64211.47397847694],[1711670400000,65526.713768406174],[1711756800000,64739.24207573283],[1711843200000,64538.55260842681],[1711929600000,66012.15293045473],[1712016000000,64987.58440425927],[1712102400000,60773.862930874704],[1712188800000,61012.09522243375],[1712275200000,63241.87913574653],[1712361600000,62689.87435787321],[1712448000000,63632.39935297565],[1712534400000,64086.823460256426],[1712620800000,65931.06490439967],[1712707200000,63687.50350010323],[1712793600000,65668.16571700791],[1712880000000,65354.13336928861],[1712966400000,63099.287555906376],[1713052800000,60427.82751512378],[1713139200000,61736.684829580605],[1713225600000,59686.89912995451],[1713312000000,59995.209903171795],[1713398400000,57478.36312839251],[1713484800000,59623.56249209243],[1713571200000,60012.93722485975],[1713657600000,60862.26888013952],[1713744000000,60924.51880957888],[1713830400000,62738.59113235908],[1713916800000,62051.58606093932],[1714003200000,60081.61563815237],[1714089600000,60110.989276790104],[1714176000000,59659.006735859955],[1714262400000,59381.3230914825],[1714348800000,58876.98016307695],[1714435200000,59532.80475223963],[1714521600000,56955.30306317563],[1714608000000,54392.86086133425],[1714694400000,55109.42816389191],[1714780800000,58355.75377420205],[1714867200000,59266.86265965134],[1714953600000,59488.78971098448],[1715040000000,58653.24832859499],[1715126400000,57997.6431882983],[1715212800000,56957.965704980525],[1715299200000,58566.91642282563],[1715385600000,56489.04308941497],[1715472000000,56385.835846451875],[1715558400000,57100.99619053609],[1715644800000,58257.94727352527],[1715731200000,56927.17129981614],[1715817600000,60811.29527582236],[1715904000000,60054.301551755365],[1715990400000,61631.8782491127],[1716076800000,61502.73339909207],[1716163200000,60924.86820709035],[1716249600000,65770.16026770095],[1716336000000,64653.893276922405],[1716422400000,63912.083874721946],[1716508800000,62795.077789946045],[1716595200000,63150.07451451353],[1716681600000,63850.68338638202],[1716768000000,63150.48139241663],[1716854400000,63871.133659487794],[1716940800000,62950.705715262724],[1717027200000,62561.005165216935],[1717113600000,63123.741723088686],[1717200000000,62145.91785386879],[1717286400000,62362.92899401087],[1717372800000,62419.71597472404],[1717459200000,63083.78769270654],[1717545600000,64881.83386232028],[1717632000000,65462.71014376164],[1717718400000,64964.377920006424],[1717804800000,64146.75781805342],[1717891200000,64137.2658453789],[1717977600000,64623.80690329454],[1718064000000,64559.64845115045],[1718150400000,62689.904415286815],[1718236800000,63085.64417953837],[1718323200000,62107.46654002352],[1718409600000,61562.52612996296],[1718496000000,61759.23292317989],[1718582400000,62231.3694306833],[1718668800000,61877.28055510076],[1718755200000,60621.91572601943],[1718841600000,60398.36957706064],[1718928000000,60572.188509925974],[1719014400000,59924.844689378995],[1719100800000,60079.40613872267],[1719187200000,59147.18345045349],[1719273600000,56243.927094112085],[1719360000000,57677.50624706287],[1719446400000,56873.38456798354],[1719532800000,57491.13821425305],[1719619200000,56265.05472146868],[1719705600000,56775.63073475873],[1719792000000,58430.814422068135],[1719878400000,58509.4401642161],[1719964800000,57718.07416888454],[1720051200000,55833.80621748973],[1720137600000,52904.90433375453],[1720224000000,52307.68520432331],[1720310400000,53715.700026296974],[1720396800000,51613.90871758563],[1720483200000,52316.60116848915],[1720569600000,53621.436327802985],[1720656000000,53272.288732207024],[1720742400000,52781.06838693916],[1720828800000,53014.0364517072],[1720915200000,54166.67728993813],[1721001600000,55984.24278202276],[1721088000000,59500.95530894624],[1721174400000,59763.54497883689],[1721260800000,58625.721758164145],[1721347200000,58665.955279138856],[1721433600000,61224.60704078251],[1721520000000,61705.0466917566],[1721606400000,62467.87253811925],[1721692800000,62073.819234324954],[1721779200000,60765.80580937294],[1721865600000,60341.49772175119],[1721952000000,60594.574722199846],[1722038400000,62469.76061302131],[1722124800000,62599.81463153654],[1722211200000,62873.08389964636],[1722297600000,61707.72841612238],[1722384000000,61216.26793898385],[1722470400000,59750.92079501405],[1722556800000,60586.10315866606],[1722643200000,56231.21313117775],[1722729600000,55639.42359487421],[1722816000000,53169.53306907562],[1722902400000,49271.40149467704],[1722988800000,51222.280928669235],[1723075200000,50411.00101226333],[1723161600000,56671.842504303975],[1723248000000,55761.51548402803],[1723334400000,55729.910819518554],[1723420800000,53884.90745860033],[1723507200000,54270.12342034756],[1723593600000,55114.14602647766],[1723680000000,53343.88139134332],[1723766400000,52521.96236743676],[1723852800000,53376.171320376874],[1723939200000,53873.71546267861],[1724025600000,53006.74653077159],[1724112000000,53742.26074312489],[1724198400000,53072.96487740486],[1724284800000,54811.25973940124],[1724371200000,54317.08310206506],[1724457600000,57173.9518647258],[1724544000000,57319.03380665118],[1724630400000,57448.04865756021],[1724716800000,56355.54477751003],[1724803200000,53250.67556605576],[1724889600000,53049.55806257478],[1724976000000,53564.80049060293],[1725062400000,53481.26904169355],[1725148800000,53307.011182245114],[1725235200000,51929.8407759559],[1725321600000,53412.30560377036],[1725408000000,52046.1502790778],[1725494400000,52329.681939274866],[1725580800000,50518.54680242768],[1725667200000,48622.690205035346],[1725753600000,48827.90111822669],[1725840000000,49416.66965979366],[1725926400000,51694.7737107547],[1726012800000,52293.63869759159],[1726099200000,52115.49563129617],[1726185600000,52451.75030832475],[1726272000000,54674.77446852742],[1726358400000,54142.357125255534],[1726444800000,53407.547394791305],[1726531200000,52308.16606889451],[1726617600000,54228.992673647364],[1726704000000,55288.44506329167],[1726790400000,56427.20338319878],[1726876800000,56488.082242429715],[1726963200000,56759.67683409224],[1727049600000,56960.916902308025],[1727136000000,56989.44770900782],[1727222400000,57493.81502099898],[1727308800000,56737.43484845022],[1727395200000,58275.038981340615],[1727481600000,58891.82868753552],[1727568000000,59019.92695395789],[1727654400000,58791.91905839133],[1727740800000,56785.567724519715],[1727827200000,55018.60488254399],[1727913600000,54905.83731260847],[1728000000000,55029.88993283354],[1728086400000,56557.5221526609],[1728172800000,56539.73406535857],[1728259200000,57252.32733417741],[1728345600000,56754.33895468004],[1728432000000,56657.33655044081],[1728518400000,55382.099090773205],[1728604800000,55055.71620633765],[1728691200000,57015.18140309107],[1728777600000,57760.33532582211],[1728864000000,57417.523313436555],[1728950400000,60551.20074941091],[1729036800000,61522.6779321783],[1729123200000,62295.13515961688],[1729209600000,62168.14661024002],[1729296000000,62971.25326535072],[1729382400000,62900.664368658116],[1729468800000,63461.0436312693],[1729555200000,62314.63337960675],[1729641600000,62379.86708703343],[1729728000000,61857.31577287963],[1729814400000,62996.90492256556],[1729900800000,61649.93632490859],[1729987200000,62050.376516974386],[1730073600000,62938.34448352703],[1730160000000,64580.714702132216],[1730246400000,67268.45323344307],[1730332800000,66618.65645361826],[1730419200000,64550.8369757725],[1730505600000,63941.79763295739],[1730592000000,63726.32978776158],[1730678400000,63270.25948561604],[1730764800000,62346.71648632486],[1730851200000,63437.06088138662],[1730937600000,70456.7357599368],[1731024000000,70367.30136816247],[1731110400000,71385.26799176612],[1731196800000,71493.3957234108],[1731283200000,75100.39122172692],[1731369600000,83136.67035524877],[1731456000000,83116.39043645399],[1731542400000,85646.80574476697],[1731628800000,83012.8378117053],[1731715200000,86243.41823875198],[1731801600000,85927.71877600063],[1731888000000,85275.3689714038],[1731974400000,85439.42645883636],[1732060800000,86966.37073765934],[1732147200000,89326.4051029836],[1732233600000,94069.11559788969],[1732320000000,94955.55602347082],[1732406400000,93760.27068972368],[1732492800000,93546.11482101932],[1732579200000,88960.11249694412],[1732665600000,87635.2127980843],[1732752000000,90869.12691365478],[1732838400000,90578.80838013266],[1732924800000,92112.0297646138],[1733011200000,91239.47122285848],[1733097600000,92304.33867392405],[1733184000000,91254.9956460552],[1733270400000,91374.28893844674],[1733356800000,94065.64524925785],[1733443200000,91830.04825245812],[1733529600000,94532.37474294563],[1733616000000,94396.5048454845],[1733702400000,95806.82736572677],[1733788800000,92230.50083900787],[1733875200000,91771.99667795865],[1733961600000,96256.94286515891],[1734048000000,95478.4718086457],[1734134400000,96474.24824906969],[1734220800000,96521.66753667717],[1734307200000,99631.30349379999],[1734393600000,100856.74619693069],[1734396005000,100857.82408798973]],"market_caps":[[1702944000000,764045559429.9309],[1703030400000,753661901693.3868],[1703116800000,780530401149.5747],[1703203200000,779398921861.9735],[1703289600000,781618872485.1848],[1703376000000,777082690166.5908],[1703462400000,767022708939.5092],[1703548800000,775395628458.022],[1703635200000,753811358997.4457],[1703721600000,764431847914.952],[1703808000000,755499387426.541],[1703894400000,744365987728.8765],[1703980800000,747690300646.6624],[1704067200000,749793913990.623],[1704153600000,782654769238.5906],[1704240000000,802781005990.2638],[1704326400000,767255728861.6259],[1704412800000,791548580238.1727],[1704499200000,788420193391.969],[1704585600000,786156615466.8936],[1704672000000,783483447081.5107],[1704758400000,839801871053.8757],[1704844800000,825360034647.3444],[1704931200000,834081331690.4401],[1705017600000,828583454756.0026],[1705104000000,764669480252.108],[1705190400000,765413030638.2758],[1705276800000,751728074198.5833],[1705363200000,763009829957.6416],[1705449600000,777614406833.8657],[1705536000000,769011178459.9075],[1705622400000,743690861843.4908],[1705708800000,747617250694.5536],[1705795200000,749429420625.5665],[1705881600000,748057990952.723],[1705968000000,712368205942.2236],[1706054400000,717595846224.838],[1706140800000,723122348942.4839],[1706227200000,722256139684.4453],[1706313600000,755726914732.5784],[1706400000000,760573087967.9829],[1706486400000,760145533102.0594],[1706572800000,783768195098.995],[1706659200000,773940797300.2744],[1706745600000,772787325782.8993],[1706832000000,776719396542.2999],[1706918400000,783958733249.4813],[1707004800000,780353189423.5363],[1707091200000,775166129480.905],[1707177600000,778777836396.1644],[1707264000000,785970965415.3887],[1707350400000,806224953741.7788],[1707436800000,826409667638.9438],[1707523200000,858208751753.8303],[1707609600000,869955939250.7516],[1707696000000,875978364782.8907],[1707782400000,911118983788.3319],[1707868800000,909849453848.8177],[1707955200000,948242282042.772],[1708041600000,947310107637.3593],[1708128000000,950197730601.678],[1708214400000,942550827895.1658],[1708300800000,949322898904.7262],[1708387200000,942991011917.1135],[1708473600000,949540001949.272],[1708560000000,941627317726.0989],[1708646400000,930501637905.6324],[1708732800000,922355633704.3813],[1708819200000,934676374407.968],[1708905600000,939479759011.2206],[1708992000000,986997076569.4227],[1709078400000,1031955351342.0142],[1709164800000,1130411922559.5562],[1709251200000,1117043914029.6543],[1709337600000,1132012429589.901],[1709424000000,1124332376821.0593],[1709510400000,1140239593736.3315],[1709596800000,1228771711791.6702],[1709683200000,1165166845467.8345],[1709769600000,1190756605244.6602],[1709856000000,1203016564461.5864],[1709942400000,1225358206016.94],[1710028800000,1229978177507.9448],[1710115200000,1240532622108.6042],[1710201600000,1297198119272.7556],[1710288000000,1284621855311.5063],[1710374400000,1311710453331.1807],[1710460800000,1289935627507.947],[1710547200000,1255862277359.9707],[1710633600000,1177567732739.3704],[1710720000000,1235543252546.1501],[1710806400000,1224902554138.9043],[1710892800000,1126404736225.0005],[1710979200000,1218425490228.851],[1711065600000,1185130051154.7644],[1711152000000,1148762056120.8308],[1711238400000,1164746053086.3816],[1711324800000,1224549270763.7375],[1711411200000,1265735595022.6558],[1711497600000,1270001087597.4917],[1711584000000,1257948244675.7322],[1711670400000,1289062232208.1372],[1711756800000,1272808920844.3113],[1711843200000,1268735443309.547],[1711929600000,1298379310622.5427],[1712016000000,1279434155078.9397],[1712102400000,1196304599517.0796],[1712188800000,1201042014718.3618],[1712275200000,1244181203942.2173],[1712361600000,1233398772850.8083],[1712448000000,1252853847882.3098],[1712534400000,1262195994485.1626],[1712620800000,1298271561930.8398],[1712707200000,1254048562016.6165],[1712793600000,1291829474485.008],[1712880000000,1286022438247.104],[1712966400000,1240036468179.4954],[1713052800000,1191586427540.0698],[1713139200000,1214419196128.5972],[1713225600000,1174217480691.2903],[1713312000000,1180219579685.6067],[1713398400000,1131391929592.0496],[1713484800000,1174027153438.296],[1713571200000,1184813761326.875],[1713657600000,1198537261371.2727],[1713744000000,1200575636208.7861],[1713830400000,1236171486303.3884],[1713916800000,1221220335945.559],[1714003200000,1182986317158.927],[1714089600000,1183322986546.24],[1714176000000,1174004613361.7327],[1714262400000,1168134415693.9995],[1714348800000,1159037915185.4558],[1714435200000,1173144443638.821],[1714521600000,1122733421392.7615],[1714608000000,1074539282758.3136],[1714694400000,1085640313340.4697],[1714780800000,1151356095639.7876],[1714867200000,1167823420330.3193],[1714953600000,1170693158689.583],[1715040000000,1155394777129.147],[1715126400000,1143529004292.6711],[1715212800000,1120237757734.764],[1715299200000,1150877656960.4106],[1715385600000,1112737515566.075],[1715472000000,1110612255834.1973],[1715558400000,1124453150567.785],[1715644800000,1148051981903.983],[1715731200000,1121416977137.8594],[1715817600000,1200367522106.6362],[1715904000000,1183686599329.002],[1715990400000,1212577969927.9417],[1716076800000,1213212301806.6638],[1716163200000,1199871409788.1362],[1716249600000,1295016060059.1472],[1716336000000,1273106118664.916],[1716422400000,1258222473590.343],[1716508800000,1235720348593.309],[1716595200000,1244542923268.9268],[1716681600000,1257314123353.6438],[1716768000000,1243813340333.789],[1716854400000,1258601999436.9578],[1716940800000,1241636599638.2234],[1717027200000,1232802172226.9258],[1717113600000,1242748884197.98],[1717200000000,1226438800629.8735],[1717286400000,1228433941518.9446],[1717372800000,1231006583667.743],[1717459200000,1242762702466.981],[1717545600000,1278263906372.3005],[1717632000000,1287689992253.43],[1717718400000,1280663158196.4136],[1717804800000,1264299316495.0254],[1717891200000,1264020591899.3923],[1717977600000,1273609983761.1584],[1718064000000,1272210501370.2603],[1718150400000,1236256618655.0908],[1718236800000,1244029473920.9504],[1718323200000,1226474851982.018],[1718409600000,1214209737400.5469],[1718496000000,1217046635445.1626],[1718582400000,1227595179188.131],[1718668800000,1219051116802.9226],[1718755200000,1193916188008.191],[1718841600000,1189652390521.7627],[1718928000000,1195025470628.2522],[1719014400000,1181499377294.3064],[1719100800000,1184321835834.7974],[1719187200000,1167675702293.7664],[1719273600000,1107858256415.485],[1719360000000,1137194446940.2493],[1719446400000,1121746795196.9453],[1719532800000,1133540553897.4954],[1719619200000,1109655746534.5195],[1719705600000,1119192024599.859],[1719792000000,1151840688521.5325],[1719878400000,1153842325231.202],[1719964800000,1137636248797.9766],[1720051200000,1099959601411.4194],[1720137600000,1045455213793.5796],[1720224000000,1031120886833.7273],[1720310400000,1058232307989.9891],[1720396800000,1016418261442.7322],[1720483200000,1031886535204.6488],[1720569600000,1058511512538.3088],[1720656000000,1051818926340.8833],[1720742400000,1040185593841.387],[1720828800000,1045247628106.2981],[1720915200000,1069888561366.5227],[1721001600000,1106207687377.5999],[1721088000000,1172591088151.519],[1721174400000,1178656334961.6272],[1721260800000,1157075741795.815],[1721347200000,1157133857736.086],[1721433600000,1208294383639.0444],[1721520000000,1217149815021.6057],[1721606400000,1232461528971.5544],[1721692800000,1224219293321.9321],[1721779200000,1198835347285.7869],[1721865600000,1190570334458.9207],[1721952000000,1195542086259.1108],[1722038400000,1232400087482.9558],[1722124800000,1235238043388.1118],[1722211200000,1240304376596.7764],[1722297600000,1217681073144.4795],[1722384000000,1208037973353.6438],[1722470400000,1178510499183.3403],[1722556800000,1194838474557.0352],[1722643200000,1109705678810.4106],[1722729600000,1097387256676.079],[1722816000000,1052498007467.1093],[1722902400000,972427918423.3748],[1722988800000,1010553135277.3352],[1723075200000,995829528973.5793],[1723161600000,1116222608474.5273],[1723248000000,1096966222757.3721],[1723334400000,1100012902964.6958],[1723420800000,1062177229408.719],[1723507200000,1071240180223.895],[1723593600000,1087488608146.0857],[1723680000000,1053042558016.0778],[1723766400000,1036457024309.6311],[1723852800000,1053996298299.8085],[1723939200000,1063563839568.8687],[1724025600000,1048736410107.2246],[1724112000000,1061067922878.8774],[1724198400000,1047855615204.1729],[1724284800000,1082245173425.6625],[1724371200000,1072480197942.716],[1724457600000,1126449704877.065],[1724544000000,1130137190128.4998],[1724630400000,1134445401094.1704],[1724716800000,1112777064777.5403],[1724803200000,1052696200890.6747],[1724889600000,1048104543360.7959],[1724976000000,1057603318248.8383],[1725062400000,1056174914763.6614],[1725148800000,1052644976421.7173],[1725235200000,1025971504101.7284],[1725321600000,1055774373172.189],[1725408000000,1029402103522.1177],[1725494400000,1033215644314.5056],[1725580800000,998279096382.0908],[1725667200000,960727122442.3049],[1725753600000,963876478138.5984],[1725840000000,978789756851.9156],[1725926400000,1021080494896.519],[1726012800000,1032514469645.9021],[1726099200000,1030116857690.4011],[1726185600000,1036768569791.1576],[1726272000000,1080345335187.4402],[1726358400000,1069483699270.5634],[1726444800000,1054918116527.0073],[1726531200000,1033801760122.5457],[1726617600000,1071488975861.3236],[1726704000000,1090617713058.4265],[1726790400000,1114446751303.7446],[1726876800000,1115365405107.757],[1726963200000,1122693086016.0098],[1727049600000,1125222965632.3362],[1727136000000,1125923016568.3755],[1727222400000,1136025045157.7083],[1727308800000,1119165941254.2256],[1727395200000,1151723722886.6248],[1727481600000,1163414323043.4895],[1727568000000,1166555764895.319],[1727654400000,1162004270981.7502],[1727740800000,1121955923185.1003],[1727827200000,1087242811870.4403],[1727913600000,1085088202725.9215],[1728000000000,1087741100651.1527],[1728086400000,1117128576184.053],[1728172800000,1117066123002.787],[1728259200000,1131919568773.6216],[1728345600000,1121655031340.7485],[1728432000000,1120296945863.3242],[1728518400000,1094687997044.0862],[1728604800000,1088058691272.9097],[1728691200000,1127062755021.9717],[1728777600000,1141359509847.64],[1728864000000,1134323870057.25],[1728950400000,1197356598803.713],[1729036800000,1215867091585.8062],[1729123200000,1231494200521.79],[1729209600000,1228557191099.4949],[1729296000000,1244920069909.3826],[1729382400000,1243448974215.4006],[1729468800000,1253692236055.3801],[1729555200000,1231860087149.921],[1729641600000,1233160899216.1892],[1729728000000,1222780930737.8003],[1729814400000,1244798930657.2415],[1729900800000,1216575374276.9106],[1729987200000,1226960927651.153],[1730073600000,1244481292778.0234],[1730160000000,1276885375262.7925],[1730246400000,1330292730940.5366],[1730332800000,1317853770698.0745],[1730419200000,1277141006442.827],[1730505600000,1264603721992.7305],[1730592000000,1260304002327.4312],[1730678400000,1252514111441.5698],[1730764800000,1232424950327.2417],[1730851200000,1254425017928.7395],[1730937600000,1394022982578.445],[1731024000000,1391245051949.168],[1731110400000,1411897389187.6711],[1731196800000,1417614104649.186],[1731283200000,1484434748459.1074],[1731369600000,1647828731487.2427],[1731456000000,1640041552819.5366],[1731542400000,1694929711862.1062],[1731628800000,1639737492315.8826],[1731715200000,1706203252031.6326],[1731801600000,1697962280123.0564],[1731888000000,1686578526653.0322],[1731974400000,1689949263959.0898],[1732060800000,1720695713494.1255],[1732147200000,1766101580550.202],[1732233600000,1861205978707.735],[1732320000000,1879473647269.3767],[1732406400000,1853767867711.6104],[1732492800000,1850862266537.5037],[1732579200000,1757981270636.0205],[1732665600000,1734807678369.8247],[1732752000000,1798047653070.2747],[1732838400000,1791871712985.0552],[1732924800000,1821220595631.0247],[1733011200000,1805655347652.551],[1733097600000,1826712092790.8245],[1733184000000,1805973641926.7527],[1733270400000,1808870011532.4983],[1733356800000,1861950970195.8547],[1733443200000,1814513963950.151],[1733529600000,1870708184255.423],[1733616000000,1868160448106.8904],[1733702400000,1895819284477.133],[1733788800000,1824685578367.7026],[1733875200000,1816541694218.0803],[1733961600000,1905410387343.5999],[1734048000000,1889662319163.5203],[1734134400000,1908887834839.7786],[1734220800000,1911680889414.6633],[1734307200000,1974756090871.2612],[1734393600000,1994542016925.7861],[1734396005000,1997412290393.3455]],"total_volumes":[[1702944000000,24236756809.610874],[1703030400000,21312795316.66426],[1703116800000,25887016911.17018],[1703203200000,19940952368.916634],[1703289600000,18895067898.906437],[1703376000000,8953022262.130066],[1703462400000,16623374495.489248],[1703548800000,17009181022.074778],[1703635200000,18544256378.162125],[1703721600000,20925648865.060066],[1703808000000,18375029769.578327],[1703894400000,22461574030.714294],[1703980800000,13321172326.845724],[1704067200000,12850316555.324738],[1704153600000,15367058214.745464],[1704240000000,35725011405.88339],[1704326400000,39491078579.2771],[1704412800000,23866156229.01105],[1704499200000,26805544307.690857],[1704585600000,10809150081.41469],[1704672000000,13845181930.24744],[1704758400000,37274113826.374435],[1704844800000,36490066076.38239],[1704931200000,47410932764.19392],[1705017600000,44797651373.354866],[1705104000000,41876420338.82902],[1705190400000,17716846311.352158],[1705276800000,15503197379.230145],[1705363200000,20679492660.517693],[1705449600000,20247282080.206425],[1705536000000,19567983377.45631],[1705622400000,23134362082.989418],[1705708800000,22383239893.13225],[1705795200000,8742685618.842075],[1705881600000,7375172401.676388],[1705968000000,28682189093.05966],[1706054400000,27340392964.10615],[1706140800000,20416186504.057236],[1706227200000,12329524792.522268],[1706313600000,20878573788.263638],[1706400000000,9844140113.16917],[1706486400000,12607124883.848349],[1706572800000,19085104248.29378],[1706659200000,22618759527.17684],[1706745600000,20533587440.086643],[1706832000000,20636966305.840107],[1706918400000,17247488100.16845],[1707004800000,7204501674.165378],[1707091200000,10436964474.709417],[1707177600000,17391461496.537296],[1707264000000,15924809340.146698],[1707350400000,19676663584.994637],[1707436800000,25715815919.968224],[1707523200000,38938892280.48865],[1707609600000,15263610371.064165],[1707696000000,12223157201.843592],[1707782400000,34984086720.421394],[1707868800000,34861630132.88448],[1707955200000,38955768310.822464],[1708041600000,29689990497.43616],[1708128000000,23058964466.517426],[1708214400000,18420761945.184425],[1708300800000,15788371337.35062],[1708387200000,20933904079.177547],[1708473600000,31913880466.34793],[1708560000000,28304827430.508766],[1708646400000,21925852529.42366],[1708732800000,20897974088.66788],[1708819200000,14334928103.442892],[1708905600000,14284884544.825203],[1708992000000,32632631614.771385],[1709078400000,47944138521.27305],[1709164800000,80469773523.04562],[1709251200000,62652966681.79769],[1709337600000,33838491238.65334],[1709424000000,23234013063.10625],[1709510400000,25003431615.645008],[1709596800000,68683749167.89397],[1709683200000,88793359828.09514],[1709769600000,67115921901.30683],[1709856000000,44611078613.12767],[1709942400000,57885098776.72138],[1710028800000,19518157761.86789],[1710115200000,33506857287.23204],[1710201600000,60984557068.80768],[1710288000000,59112467277.932526],[1710374400000,47399187528.719086],[1710460800000,57955447999.744316],[1710547200000,74565965097.54878],[1710633600000,45237986618.562836],[1710720000000,43360819029.21664],[1710806400000,47000236982.08203],[1710892800000,73786793068.606],[1710979200000,64770977376.002335],[1711065600000,44547287696.10954],[1711152000000,39081986975.866745],[1711238400000,23474954022.615948],[1711324800000,25997014256.79051],[1711411200000,41354490140.194534],[1711497600000,33470731576.137875],[1711584000000,38323058412.463455],[1711670400000,28222733898.22986],[1711756800000,23875651073.0657],[1711843200000,15193189000.910406],[1711929600000,18273877855.502132],[1712016000000,33562044549.864765],[1712102400000,41940693527.11473],[1712188800000,32810919678.179626],[1712275200000,34615710161.11247],[1712361600000,32399476179.27087],[1712448000000,17631945851.64823],[1712534400000,16561889813.524487],[1712620800000,30480013239.47029],[1712707200000,33601878224.764893],[1712793600000,35733622227.3955],[1712880000000,28076812702.66515],[1712966400000,40787556830.5058],[1713052800000,46175487248.79834],[1713139200000,37831941525.483795],[1713225600000,40270749314.591446],[1713312000000,39692534875.41359],[1713398400000,38541431787.99773],[1713484800000,33730360102.954216],[1713571200000,48982665986.2395],[1713657600000,15570292448.765707],[1713744000000,18381678273.477943],[1713830400000,26045239342.36995],[1713916800000,21708521696.923584],[1714003200000,28919311055.77435],[1714089600000,23029161947.69737],[1714176000000,21788821603.454067],[1714262400000,17940895060.380325],[1714348800000,15114077954.721254],[1714435200000,25421790237.986824],[1714521600000,37064033196.407425],[1714608000000,47686124997.17279],[1714694400000,26331959408.546562],[1714780800000,31194900456.511097],[1714867200000,19047182119.65269],[1714953600000,16836129152.427391],[1715040000000,16635968731.550478],[1715126400000,18530378080.389984],[1715212800000,18891699679.907948],[1715299200000,23963094002.49368],[1715385600000,22254420525.397045],[1715472000000,11244904093.616146],[1715558400000,12111167668.150711],[1715644800000,25701869152.978073],[1715731200000,19954749749.200596],[1715817600000,34989448098.62592],[1715904000000,26791795060.979183],[1715990400000,23344382280.64292],[1716076800000,11972376997.253046],[1716163200000,8343132501.680276],[1716249600000,33076900352.36851],[1716336000000,37863059660.65226],[1716422400000,28693829269.986565],[1716508800000,36282391585.036026],[1716595200000,25915528544.90744],[1716681600000,14683494912.702457],[1716768000000,10364325595.831345],[1716854400000,17479681941.591335],[1716940800000,28821382088.28565],[1717027200000,22515367160.244823],[1717113600000,23398765116.409836],[1717200000000,18130599420.539665],[1717286400000,9878000585.622467],[1717372800000,14869923072.588507],[1717459200000,27377900221.38685],[1717545600000,29004797739.730236],[1717632000000,29943165536.127033],[1717718400000,21678984842.985905],[1717804800000,17208262650.83919],[1717891200000,9889885385.793055],[1717977600000,9705557901.477417],[1718064000000,17963155583.8117],[1718150400000,35859341572.46878],[1718236800000,33314086852.608078],[1718323200000,27612809188.818436],[1718409600000,26082269218.757824],[1718496000000,12570767640.817629],[1718582400000,11817587184.625206],[1718668800000,27381634087.485992],[1718755200000,38534540549.638336],[1718841600000,20174670906.88063],[1718928000000,24288361251.269745],[1719014400000,23096069748.525814],[1719100800000,5929957294.213889],[1719187200000,10055620107.878874],[1719273600000,39167894649.76987],[1719360000000,19707107706.19171],[1719446400000,21446724014.390743],[1719532800000,17608135427.23568],[1719619200000,22760288280.98671],[1719705600000,10666911569.81817],[1719792000000,16181763633.619421],[1719878400000,23818378375.692726],[1719964800000,16838886952.541449],[1720051200000,28126682562.287437],[1720137600000,39882312622.7753],[1720224000000,55388162417.01709],[1720310400000,19642932662.272545],[1720396800000,18668734939.988163],[1720483200000,38420746451.49442],[1720569600000,26274120844.467396],[1720656000000,23991700389.182217],[1720742400000,26852746247.844795],[1720828800000,23352541611.613438],[1720915200000,15352362821.061203],[1721001600000,19562980897.157185],[1721088000000,34699131677.40675],[1721174400000,37524680143.79378],[1721260800000,30584615622.361465],[1721347200000,23826232916.692142],[1721433600000,33963050379.528145],[1721520000000,15932968631.498928],[1721606400000,24843581686.185513],[1721692800000,39625015138.62209],[1721779200000,33266571810.64418],[1721865600000,26460367769.009254],[1721952000000,33026960946.45104],[1722038400000,28347985942.522514],[1722124800000,28493532219.991848],[1722211200000,15823930171.331104],[1722297600000,39304449047.5405],[1722384000000,26709586357.683525],[1722470400000,29010595429.980762],[1722556800000,36070582812.4214],[1722643200000,35716608343.00602],[1722729600000,30124639091.24687],[1722816000000,30857488073.92184],[1722902400000,109805375344.71971],[1722988800000,48165283096.90922],[1723075200000,38065296810.65033],[1723161600000,45075752485.89274],[1723248000000,30945556000.44254],[1723334400000,12282202454.06668],[1723420800000,19899321258.11229],[1723507200000,36030650038.289856],[1723593600000,29062190504.223873],[1723680000000,25500715375.97391],[1723766400000,30524957963.753498],[1723852800000,27528656719.12384],[1723939200000,11301669424.835934],[1724025600000,16014988081.814625],[1724112000000,21678542798.058605],[1724198400000,28280403717.648746],[1724284800000,29546835293.844273],[1724371200000,25338482846.447086],[1724457600000,40203764990.14742],[1724544000000,19681198155.428112],[1724630400000,15978900606.43484],[1724716800000,16027077290.044294],[1724803200000,34111499405.192535],[1724889600000,37771373954.530266],[1724976000000,29388408896.179905],[1725062400000,39813795557.68244],[1725148800000,10306119140.205893],[1725235200000,23055759148.414356],[1725321600000,25313134436.021263],[1725408000000,24283254518.8608],[1725494400000,33886402300.26096],[1725580800000,27004866037.61644],[1725667200000,45320178289.28158],[1725753600000,15372585933.106846],[1725840000000,16815561367.908846],[1725926400000,32308980055.771793],[1726012800000,26930906291.377064],[1726099200000,34299672189.86188],[1726185600000,30851533892.901806],[1726272000000,28788956794.35168],[1726358400000,14256812437.764132],[1726444800000,15593989422.240282],[1726531200000,28931645426.28441],[1726617600000,30740118553.114304],[1726704000000,36395588024.81709],[1726790400000,37601462347.57837],[1726876800000,31844524527.859264],[1726963200000,11582366076.384974],[1727049600000,18230121450.62975],[1727136000000,21597115107.60136],[1727222400000,28142705656.780155],[1727308800000,23437869613.173584],[1727395200000,33996117325.546436],[1727481600000,29239510929.881367],[1727568000000,13733976640.534172],[1727654400000,11593612614.885637],[1727740800000,31537924652.0654],[1727827200000,49034177042.745705],[1727913600000,37901679060.51742],[1728000000000,34172777744.048645],[1728086400000,27639040540.389217],[1728172800000,10103308836.609213],[1728259200000,13300847513.28617],[1728345600000,30869381700.261482],[1728432000000,26081711472.44388],[1728518400000,26075363404.124302],[1728604800000,26964801126.052986],[1728691200000,29250057048.886543],[1728777600000,16143457901.477922],[1728864000000,15338765154.550152],[1728950400000,42678099498.38608],[1729036800000,47590231395.44467],[1729123200000,37424938104.72408],[1729209600000,31867101644.482155],[1729296000000,36776163863.41151],[1729382400000,12978490558.257559],[1729468800000,15877367461.98269],[1729555200000,37433699396.538925],[1729641600000,29080874803.888737],[1729728000000,30311730450.974777],[1729814400000,33147468234.34605],[1729900800000,44837230618.6183],[1729987200000,19600259820.499084],[1730073600000,15316545074.05997],[1730160000000,38825503403.448814],[1730246400000,60527540932.56764],[1730332800000,40056440641.14229],[1730419200000,42173773194.33304],[1730505600000,50469710696.11748],[1730592000000,13526495563.210878],[1730678400000,34630826043.86812],[1730764800000,42472713673.43949],[1730851200000,36620093013.85483],[1730937600000,119717779475.74457],[1731024000000,61665436983.2205],[1731110400000,47267635583.06925],[1731196800000,29323966699.251873],[1731283200000,87239403420.88857],[1731369600000,125646393751.07103],[1731456000000,143470295938.74728],[1731542400000,128767560174.35379],[1731628800000,94639219723.79593],[1731715200000,80017548477.67146],[1731801600000,46711735711.32105],[1731888000000,46088268734.089355],[1731974400000,73036195338.83128],[1732060800000,75881616541.24971],[1732147200000,76555842681.1068],[1732233600000,112837970883.96764],[1732320000000,82303465655.24199],[1732406400000,45511800251.80984],[1732492800000,48355173230.56986],[1732579200000,85585174959.04805],[1732665600000,92837558437.63956],[1732752000000,77130214035.7825],[1732838400000,46460805592.15066],[1732924800000,70577516541.62772],[1733011200000,41198720283.14104],[1733097600000,46618694880.34837],[1733184000000,96193940544.53133],[1733270400000,83671044173.51279],[1733356800000,93446650410.37816],[1733443200000,179935267250.6917],[1733529600000,109509066272.2067],[1733616000000,57765605617.94603],[1733702400000,59316795003.98513],[1733788800000,139118734393.82437],[1733875200000,119485287617.8021],[1733961600000,112782750873.94843],[1734048000000,95565580844.76036],[1734134400000,75010198346.95139],[1734220800000,54581936836.852325],[1734307200000,67211229482.25423],[1734393600000,109519261247.90494],[1734396005000,105347250732.53983]] +} diff --git a/example/assets/fonts/digital-7.ttf b/example/assets/fonts/digital-7.ttf new file mode 100755 index 0000000..5dbe6f9 Binary files /dev/null and b/example/assets/fonts/digital-7.ttf differ diff --git a/example/assets/icons/fitness-svgrepo-com.svg b/example/assets/icons/fitness-svgrepo-com.svg new file mode 100644 index 0000000..d54ea58 --- /dev/null +++ b/example/assets/icons/fitness-svgrepo-com.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/assets/icons/fl_chart_logo_icon.png b/example/assets/icons/fl_chart_logo_icon.png new file mode 100644 index 0000000..289dbdc Binary files /dev/null and b/example/assets/icons/fl_chart_logo_icon.png differ diff --git a/example/assets/icons/fl_chart_logo_text.svg b/example/assets/icons/fl_chart_logo_text.svg new file mode 100644 index 0000000..6b7cbc2 --- /dev/null +++ b/example/assets/icons/fl_chart_logo_text.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/example/assets/icons/ic_bar_chart.svg b/example/assets/icons/ic_bar_chart.svg new file mode 100644 index 0000000..812fd71 --- /dev/null +++ b/example/assets/icons/ic_bar_chart.svg @@ -0,0 +1,3 @@ + + + diff --git a/example/assets/icons/ic_candle_chart.svg b/example/assets/icons/ic_candle_chart.svg new file mode 100644 index 0000000..3fae725 --- /dev/null +++ b/example/assets/icons/ic_candle_chart.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/example/assets/icons/ic_line_chart.svg b/example/assets/icons/ic_line_chart.svg new file mode 100644 index 0000000..494621c --- /dev/null +++ b/example/assets/icons/ic_line_chart.svg @@ -0,0 +1,3 @@ + + + diff --git a/example/assets/icons/ic_pie_chart.svg b/example/assets/icons/ic_pie_chart.svg new file mode 100644 index 0000000..cdddf24 --- /dev/null +++ b/example/assets/icons/ic_pie_chart.svg @@ -0,0 +1,3 @@ + + + diff --git a/example/assets/icons/ic_radar_chart.svg b/example/assets/icons/ic_radar_chart.svg new file mode 100644 index 0000000..0edccb5 --- /dev/null +++ b/example/assets/icons/ic_radar_chart.svg @@ -0,0 +1,3 @@ + + + diff --git a/example/assets/icons/ic_scatter_chart.svg b/example/assets/icons/ic_scatter_chart.svg new file mode 100644 index 0000000..684445d --- /dev/null +++ b/example/assets/icons/ic_scatter_chart.svg @@ -0,0 +1,3 @@ + + + diff --git a/example/assets/icons/image_annotation.png b/example/assets/icons/image_annotation.png new file mode 100644 index 0000000..76f980f Binary files /dev/null and b/example/assets/icons/image_annotation.png differ diff --git a/example/assets/icons/librarian-svgrepo-com.svg b/example/assets/icons/librarian-svgrepo-com.svg new file mode 100644 index 0000000..22b493d --- /dev/null +++ b/example/assets/icons/librarian-svgrepo-com.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/assets/icons/ophthalmology-svgrepo-com.svg b/example/assets/icons/ophthalmology-svgrepo-com.svg new file mode 100644 index 0000000..056a5e8 --- /dev/null +++ b/example/assets/icons/ophthalmology-svgrepo-com.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/assets/icons/worker-svgrepo-com.svg b/example/assets/icons/worker-svgrepo-com.svg new file mode 100644 index 0000000..50cf404 --- /dev/null +++ b/example/assets/icons/worker-svgrepo-com.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/devtools_options.yaml b/example/devtools_options.yaml new file mode 100644 index 0000000..7e7e7f6 --- /dev/null +++ b/example/devtools_options.yaml @@ -0,0 +1 @@ +extensions: diff --git a/example/ios/.gitignore b/example/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/example/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..7c56964 --- /dev/null +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 12.0 + + diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/example/ios/Podfile b/example/ios/Podfile new file mode 100644 index 0000000..279576f --- /dev/null +++ b/example/ios/Podfile @@ -0,0 +1,41 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock new file mode 100644 index 0000000..395ff45 --- /dev/null +++ b/example/ios/Podfile.lock @@ -0,0 +1,35 @@ +PODS: + - Flutter (1.0.0) + - package_info_plus (0.4.5): + - Flutter + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - url_launcher_ios (0.0.1): + - Flutter + +DEPENDENCIES: + - Flutter (from `Flutter`) + - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + package_info_plus: + :path: ".symlinks/plugins/package_info_plus/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" + +SPEC CHECKSUMS: + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + +PODFILE CHECKSUM: c4c93c5f6502fe2754f48404d3594bf779584011 + +COCOAPODS: 1.16.2 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..60a4e28 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,569 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3A769574DE769ECF798B0709 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 380B38C853A9B1A4CE259991 /* Pods_Runner.framework */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 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 = ( + ); + 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 = ""; }; + 380B38C853A9B1A4CE259991 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 70B7C328B4B2F1CF5A8F4CD5 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; 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 = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 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 = ""; }; + A5FD4176E495AE357451309C /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + F92DACD635C19D794086368F /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3A769574DE769ECF798B0709 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 387A47388B1D7AAF78141AE7 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 380B38C853A9B1A4CE259991 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 87E9D592125921BB4FA5096E /* Pods */ = { + isa = PBXGroup; + children = ( + 70B7C328B4B2F1CF5A8F4CD5 /* Pods-Runner.debug.xcconfig */, + F92DACD635C19D794086368F /* Pods-Runner.release.xcconfig */, + A5FD4176E495AE357451309C /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 87E9D592125921BB4FA5096E /* Pods */, + 387A47388B1D7AAF78141AE7 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + B16E897CCC57E0F4B2344803 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 00C61598CDE2223281B7DB47 /* [CP] Embed Pods Frameworks */, + ); + 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 = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + 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 */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 00C61598CDE2223281B7DB47 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + 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"; + }; + B16E897CCC57E0F4B2344803 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift 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 */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + 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_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_IMPLICIT_RETAIN_SELF = 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 = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = YANXYADU5H; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "FL Chart"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flchart.app; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + 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_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_IMPLICIT_RETAIN_SELF = 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 = 12.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + 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_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_IMPLICIT_RETAIN_SELF = 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 = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = YANXYADU5H; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "FL Chart"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flchart.app; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = YANXYADU5H; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "FL Chart"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flchart.app; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + 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..919434a --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + 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..5e31d3d --- /dev/null +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..b636303 --- /dev/null +++ b/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} 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..d5876d3 --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "app_logo_1024.jpg", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/app_logo_1024.jpg b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/app_logo_1024.jpg new file mode 100644 index 0000000..53a0bc8 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/app_logo_1024.jpg differ diff --git a/example/ios/Runner/Assets.xcassets/Contents.json b/example/ios/Runner/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..fe135f2 --- /dev/null +++ b/example/ios/Runner/Info.plist @@ -0,0 +1,53 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + FL Chart + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + FL Chart App + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + ITSAppUsesNonExemptEncryption + + + diff --git a/example/ios/Runner/Runner-Bridging-Header.h b/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/example/lib/cubits/app/app_cubit.dart b/example/lib/cubits/app/app_cubit.dart new file mode 100644 index 0000000..ade13d3 --- /dev/null +++ b/example/lib/cubits/app/app_cubit.dart @@ -0,0 +1,43 @@ +import 'package:fl_chart_app/urls.dart'; +import 'package:fl_chart_app/util/app_utils.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:equatable/equatable.dart'; + +part 'app_state.dart'; + +class AppCubit extends Cubit { + AppCubit() : super(const AppState()) { + initialize(); + } + + void initialize() async { + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + emit(state.copyWith( + currentPackageInfo: packageInfo, + availableVersionToUpdate: '', + usingFlChartVersion: BuildConstants.usingFlChartVersion, + showDownloadNativeAppButton: kIsWeb || kIsWasm, + )); + } + + void onVersionClicked() { + AppUtils().tryToLaunchUrl( + Urls.getVersionReleaseUrl(state.usingFlChartVersion), + ); + } + + void hideDownloadNativeAppButton() { + emit(state.copyWith( + showDownloadNativeAppButton: false, + )); + } +} + +class BuildConstants { + static const String usingFlChartVersion = String.fromEnvironment( + 'USING_FL_CHART_VERSION', + defaultValue: '', + ); +} diff --git a/example/lib/cubits/app/app_state.dart b/example/lib/cubits/app/app_state.dart new file mode 100644 index 0000000..a3fdbe8 --- /dev/null +++ b/example/lib/cubits/app/app_state.dart @@ -0,0 +1,39 @@ +part of 'app_cubit.dart'; + +class AppState extends Equatable { + final PackageInfo? currentPackageInfo; + final String availableVersionToUpdate; + final String usingFlChartVersion; + final bool showDownloadNativeAppButton; + + String? get appVersion => currentPackageInfo?.version; + + const AppState([ + this.currentPackageInfo, + this.availableVersionToUpdate = '', + this.usingFlChartVersion = '', + this.showDownloadNativeAppButton = false, + ]); + + AppState copyWith({ + PackageInfo? currentPackageInfo, + String? availableVersionToUpdate, + String? usingFlChartVersion, + bool? showDownloadNativeAppButton, + }) { + return AppState( + currentPackageInfo ?? this.currentPackageInfo, + availableVersionToUpdate ?? this.availableVersionToUpdate, + usingFlChartVersion ?? this.usingFlChartVersion, + showDownloadNativeAppButton ?? this.showDownloadNativeAppButton, + ); + } + + @override + List get props => [ + currentPackageInfo, + availableVersionToUpdate, + usingFlChartVersion, + showDownloadNativeAppButton, + ]; +} diff --git a/example/lib/main.dart b/example/lib/main.dart new file mode 100644 index 0000000..a021e21 --- /dev/null +++ b/example/lib/main.dart @@ -0,0 +1,38 @@ +import 'package:fl_chart_app/cubits/app/app_cubit.dart'; +import 'package:fl_chart_app/presentation/resources/app_resources.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:google_fonts/google_fonts.dart'; + +import 'presentation/router/app_router.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider(create: (BuildContext context) => AppCubit()), + ], + child: MaterialApp.router( + title: AppTexts.appName, + theme: ThemeData( + brightness: Brightness.dark, + useMaterial3: true, + textTheme: GoogleFonts.assistantTextTheme( + Theme.of(context).textTheme.apply( + bodyColor: AppColors.mainTextColor3, + ), + ), + scaffoldBackgroundColor: AppColors.pageBackground, + ), + routerConfig: appRouterConfig, + ), + ); + } +} diff --git a/example/lib/presentation/menu/app_menu.dart b/example/lib/presentation/menu/app_menu.dart new file mode 100644 index 0000000..f7ee854 --- /dev/null +++ b/example/lib/presentation/menu/app_menu.dart @@ -0,0 +1,166 @@ +import 'package:dartx/dartx.dart'; +import 'package:fl_chart_app/cubits/app/app_cubit.dart'; +import 'package:fl_chart_app/presentation/resources/app_resources.dart'; +import 'package:fl_chart_app/urls.dart'; +import 'package:fl_chart_app/util/app_helper.dart'; +import 'package:fl_chart_app/util/app_utils.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import 'fl_chart_banner.dart'; +import 'menu_row.dart'; + +class AppMenu extends StatefulWidget { + final List menuItems; + final int currentSelectedIndex; + final Function(int, ChartMenuItem) onItemSelected; + final VoidCallback? onBannerClicked; + + const AppMenu({ + super.key, + required this.menuItems, + required this.currentSelectedIndex, + required this.onItemSelected, + required this.onBannerClicked, + }); + + @override + AppMenuState createState() => AppMenuState(); +} + +class AppMenuState extends State { + @override + Widget build(BuildContext context) { + return Container( + color: AppColors.itemsBackground, + child: Column( + children: [ + SafeArea( + child: AspectRatio( + aspectRatio: 3, + child: Center( + child: InkWell( + onTap: widget.onBannerClicked, + child: const FlChartBanner(), + ), + ), + ), + ), + Expanded( + child: ListView.builder( + itemBuilder: (context, position) { + final menuItem = widget.menuItems[position]; + return MenuRow( + text: menuItem.text, + svgPath: menuItem.iconPath, + isSelected: widget.currentSelectedIndex == position, + onTap: () { + widget.onItemSelected(position, menuItem); + }, + onDocumentsTap: () async { + final url = Uri.parse(menuItem.chartType.documentationUrl); + if (await canLaunchUrl(url)) { + await launchUrl(url); + } + }, + ); + }, + itemCount: widget.menuItems.length, + ), + ), + const _AppVersionRow(), + ], + ), + ); + } +} + +class _AppVersionRow extends StatelessWidget { + const _AppVersionRow(); + + @override + Widget build(BuildContext context) { + return BlocBuilder(builder: (context, state) { + if (state.appVersion.isNullOrBlank) { + return Container(); + } + return Container( + margin: const EdgeInsets.all(12), + child: Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(10.0), + child: RichText( + text: TextSpan( + text: '', + style: DefaultTextStyle.of(context).style, + children: [ + const TextSpan(text: 'App version: '), + TextSpan( + text: 'v${state.appVersion!}', + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + if (state.usingFlChartVersion.isNotBlank) ...[ + TextSpan( + text: '\nfl_chart: ', + recognizer: TapGestureRecognizer() + ..onTap = BlocProvider.of(context) + .onVersionClicked, + ), + TextSpan( + text: 'v${state.usingFlChartVersion}', + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + recognizer: TapGestureRecognizer() + ..onTap = BlocProvider.of(context) + .onVersionClicked, + ), + ] + ], + ), + ), + ), + ), + state.availableVersionToUpdate.isNotBlank + ? TextButton( + onPressed: () {}, + child: Text( + 'Update to ${state.availableVersionToUpdate}', + style: const TextStyle( + color: AppColors.primary, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ) + : TextButton( + onPressed: () => AppUtils().tryToLaunchUrl(Urls.aboutUrl), + child: const Text( + 'About', + style: TextStyle( + color: AppColors.primary, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ); + }); + } +} + +class ChartMenuItem { + final ChartType chartType; + final String text; + final String iconPath; + + const ChartMenuItem(this.chartType, this.text, this.iconPath); +} diff --git a/example/lib/presentation/menu/fl_chart_banner.dart b/example/lib/presentation/menu/fl_chart_banner.dart new file mode 100644 index 0000000..27558da --- /dev/null +++ b/example/lib/presentation/menu/fl_chart_banner.dart @@ -0,0 +1,38 @@ +import 'package:fl_chart_app/presentation/resources/app_resources.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class FlChartBanner extends StatelessWidget { + const FlChartBanner({super.key}); + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, constraints) { + final maxWidth = constraints.maxWidth; + + final imageSize = maxWidth / 5.14; + final space = maxWidth / 16.0; + final textWidth = maxWidth / 2.8; + + return Row( + children: [ + SizedBox( + width: imageSize, + ), + Image.asset( + AppAssets.flChartLogoIcon, + width: imageSize, + height: imageSize, + ), + SizedBox( + width: space, + ), + SvgPicture.asset( + AppAssets.flChartLogoText, + width: textWidth, + ), + ], + ); + }); + } +} diff --git a/example/lib/presentation/menu/menu_row.dart b/example/lib/presentation/menu/menu_row.dart new file mode 100644 index 0000000..4606f8c --- /dev/null +++ b/example/lib/presentation/menu/menu_row.dart @@ -0,0 +1,99 @@ +import 'package:fl_chart_app/presentation/resources/app_resources.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class MenuRow extends StatefulWidget { + final String text; + final String svgPath; + final bool isSelected; + final VoidCallback onTap; + final VoidCallback onDocumentsTap; + + const MenuRow({ + super.key, + required this.text, + required this.svgPath, + required this.isSelected, + required this.onTap, + required this.onDocumentsTap, + }); + + @override + State createState() => _MenuRowState(); +} + +class _MenuRowState extends State { + bool get _showSelectedState => widget.isSelected; + + bool isHovered = false; + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: InkWell( + onHover: (bool hovered) { + setState(() { + isHovered = hovered; + }); + }, + onTap: widget.onTap, + child: SizedBox( + height: AppDimens.menuRowHeight, + child: Row( + children: [ + const SizedBox( + width: 36, + ), + SvgPicture.asset( + widget.svgPath, + width: AppDimens.menuIconSize, + height: AppDimens.menuIconSize, + colorFilter: + const ColorFilter.mode(AppColors.primary, BlendMode.srcIn), + ), + const SizedBox( + width: 18, + ), + Text( + widget.text, + style: TextStyle( + color: _showSelectedState ? AppColors.primary : Colors.white, + fontSize: AppDimens.menuTextSize, + ), + ), + Expanded(child: Container()), + _DocumentationIcon(onTap: widget.onDocumentsTap), + const SizedBox( + width: 18, + ), + ], + ), + ), + ), + ); + } +} + +class _DocumentationIcon extends StatelessWidget { + const _DocumentationIcon({ + required this.onTap, + }); + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: AppDimens.menuDocumentationIconSize, + height: AppDimens.menuDocumentationIconSize, + child: IconButton( + onPressed: onTap, + icon: const Icon( + Icons.article, + color: AppColors.contentColorWhite, + ), + tooltip: 'Documentation', + ), + ); + } +} diff --git a/example/lib/presentation/pages/chart_samples_page.dart b/example/lib/presentation/pages/chart_samples_page.dart new file mode 100644 index 0000000..056fef5 --- /dev/null +++ b/example/lib/presentation/pages/chart_samples_page.dart @@ -0,0 +1,42 @@ +import 'package:fl_chart_app/presentation/resources/app_resources.dart'; +import 'package:fl_chart_app/presentation/samples/chart_samples.dart'; +import 'package:fl_chart_app/presentation/widgets/chart_holder.dart'; +import 'package:fl_chart_app/util/app_helper.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; + +class ChartSamplesPage extends StatelessWidget { + final ChartType chartType; + + final samples = ChartSamples.samples; + + ChartSamplesPage({ + super.key, + required this.chartType, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.transparent, + body: MasonryGridView.builder( + itemCount: samples[chartType]!.length, + key: ValueKey(chartType), + padding: const EdgeInsets.only( + left: AppDimens.chartSamplesSpace, + right: AppDimens.chartSamplesSpace, + top: AppDimens.chartSamplesSpace, + bottom: AppDimens.chartSamplesSpace + 68, + ), + crossAxisSpacing: AppDimens.chartSamplesSpace, + mainAxisSpacing: AppDimens.chartSamplesSpace, + itemBuilder: (BuildContext context, int index) { + return ChartHolder(chartSample: samples[chartType]![index]); + }, + gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 600, + ), + ), + ); + } +} diff --git a/example/lib/presentation/pages/home_page.dart b/example/lib/presentation/pages/home_page.dart new file mode 100644 index 0000000..7fc0678 --- /dev/null +++ b/example/lib/presentation/pages/home_page.dart @@ -0,0 +1,116 @@ +import 'package:dartx/dartx.dart'; +import 'package:fl_chart_app/cubits/app/app_cubit.dart'; +import 'package:fl_chart_app/presentation/menu/app_menu.dart'; +import 'package:fl_chart_app/presentation/resources/app_resources.dart'; +import 'package:fl_chart_app/presentation/widgets/download_native_app_button.dart'; +import 'package:fl_chart_app/urls.dart'; +import 'package:fl_chart_app/util/app_helper.dart'; +import 'package:fl_chart_app/util/app_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; + +import 'chart_samples_page.dart'; + +class HomePage extends StatelessWidget { + HomePage({ + super.key, + required this.showingChartType, + }) { + _initMenuItems(); + } + + void _initMenuItems() { + _menuItemsIndices = {}; + _menuItems = ChartType.values.mapIndexed( + (int index, ChartType type) { + _menuItemsIndices[type] = index; + return ChartMenuItem( + type, + type.displayName, + type.assetIcon, + ); + }, + ).toList(); + } + + final ChartType showingChartType; + late final Map _menuItemsIndices; + late final List _menuItems; + + @override + Widget build(BuildContext context) { + final selectedMenuIndex = _menuItemsIndices[showingChartType]!; + return LayoutBuilder( + builder: (context, constraints) { + final needsDrawer = constraints.maxWidth <= + AppDimens.menuMaxNeededWidth + AppDimens.chartBoxMinWidth; + final appMenuWidget = AppMenu( + menuItems: _menuItems, + currentSelectedIndex: selectedMenuIndex, + onItemSelected: (newIndex, chartMenuItem) { + context.go('/${chartMenuItem.chartType.name}'); + if (needsDrawer) { + /// to close the drawer + Navigator.of(context).pop(); + } + }, + onBannerClicked: () => AppUtils().tryToLaunchUrl(Urls.flChartUrl), + ); + final samplesSectionWidget = + ChartSamplesPage(chartType: showingChartType); + final body = needsDrawer + ? samplesSectionWidget + : Row( + children: [ + SizedBox( + width: AppDimens.menuMaxNeededWidth, + child: appMenuWidget, + ), + Expanded( + child: samplesSectionWidget, + ) + ], + ); + + return BlocBuilder( + builder: (context, state) { + return Scaffold( + body: Stack( + children: [ + body, + if (state.showDownloadNativeAppButton) + Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.only(bottom: 24.0), + child: DownloadNativeAppButton( + onClose: () => context + .read() + .hideDownloadNativeAppButton(), + onDownload: () => + AppUtils().tryToLaunchUrl(Urls.downloadUrl), + ), + ), + ), + ], + ), + drawer: needsDrawer + ? Drawer( + child: appMenuWidget, + ) + : null, + appBar: needsDrawer + ? AppBar( + elevation: 0, + backgroundColor: Colors.transparent, + title: Text(showingChartType.displayName), + ) + : null, + ); + }, + ); + }, + ); + } +} diff --git a/example/lib/presentation/presentation_utils.dart b/example/lib/presentation/presentation_utils.dart new file mode 100644 index 0000000..037bf50 --- /dev/null +++ b/example/lib/presentation/presentation_utils.dart @@ -0,0 +1,16 @@ +import 'package:flutter/cupertino.dart'; +import 'package:intl/intl.dart'; + +class AppUtils { + static String getFormattedCurrency( + BuildContext context, + double value, { + bool noDecimals = true, + }) { + final germanFormat = NumberFormat.currency( + symbol: '€', + decimalDigits: noDecimals && value % 1 == 0 ? 0 : 2, + ); + return germanFormat.format(value); + } +} diff --git a/example/lib/presentation/resources/app_assets.dart b/example/lib/presentation/resources/app_assets.dart new file mode 100644 index 0000000..08da1e7 --- /dev/null +++ b/example/lib/presentation/resources/app_assets.dart @@ -0,0 +1,23 @@ +import 'package:fl_chart_app/util/app_helper.dart'; + +class AppAssets { + static String getChartIcon(ChartType type) { + switch (type) { + case ChartType.line: + return 'assets/icons/ic_line_chart.svg'; + case ChartType.bar: + return 'assets/icons/ic_bar_chart.svg'; + case ChartType.pie: + return 'assets/icons/ic_pie_chart.svg'; + case ChartType.scatter: + return 'assets/icons/ic_scatter_chart.svg'; + case ChartType.radar: + return 'assets/icons/ic_radar_chart.svg'; + case ChartType.candlestick: + return 'assets/icons/ic_candle_chart.svg'; + } + } + + static const flChartLogoIcon = 'assets/icons/fl_chart_logo_icon.png'; + static const flChartLogoText = 'assets/icons/fl_chart_logo_text.svg'; +} diff --git a/example/lib/presentation/resources/app_colors.dart b/example/lib/presentation/resources/app_colors.dart new file mode 100644 index 0000000..a1ca589 --- /dev/null +++ b/example/lib/presentation/resources/app_colors.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; + +class AppColors { + static const Color primary = contentColorCyan; + static const Color menuBackground = Color(0xFF090912); + static const Color itemsBackground = Color(0xFF1B2339); + static const Color pageBackground = Color(0xFF282E45); + static const Color mainTextColor1 = Colors.white; + static const Color mainTextColor2 = Colors.white70; + static const Color mainTextColor3 = Colors.white38; + static const Color mainGridLineColor = Colors.white10; + static const Color borderColor = Colors.white54; + static const Color gridLinesColor = Color(0x11FFFFFF); + + static const Color contentColorBlack = Colors.black; + static const Color contentColorWhite = Colors.white; + static const Color contentColorBlue = Color(0xFF2196F3); + static const Color contentColorYellow = Color(0xFFFFC300); + static const Color contentColorOrange = Color(0xFFFF683B); + static const Color contentColorGreen = Color(0xFF3BFF49); + static const Color contentColorPurple = Color(0xFF6E1BFF); + static const Color contentColorPink = Color(0xFFFF3AF2); + static const Color contentColorRed = Color(0xFFE80054); + static const Color contentColorCyan = Color(0xFF50E4FF); +} diff --git a/example/lib/presentation/resources/app_dimens.dart b/example/lib/presentation/resources/app_dimens.dart new file mode 100644 index 0000000..b11c81b --- /dev/null +++ b/example/lib/presentation/resources/app_dimens.dart @@ -0,0 +1,13 @@ +class AppDimens { + static const double menuMaxNeededWidth = 304; + static const double menuRowHeight = 74; + static const double menuIconSize = 32; + static const double menuDocumentationIconSize = 44; + static const double menuTextSize = 20; + + static const double chartBoxMinWidth = 350; + + static const double defaultRadius = 8; + static const double chartSamplesSpace = 32.0; + static const double chartSamplesMinWidth = 350; +} diff --git a/example/lib/presentation/resources/app_resources.dart b/example/lib/presentation/resources/app_resources.dart new file mode 100644 index 0000000..15c1039 --- /dev/null +++ b/example/lib/presentation/resources/app_resources.dart @@ -0,0 +1,4 @@ +export 'app_colors.dart'; +export 'app_assets.dart'; +export 'app_dimens.dart'; +export 'app_texts.dart'; diff --git a/example/lib/presentation/resources/app_texts.dart b/example/lib/presentation/resources/app_texts.dart new file mode 100644 index 0000000..b2ffa4e --- /dev/null +++ b/example/lib/presentation/resources/app_texts.dart @@ -0,0 +1,3 @@ +class AppTexts { + static const appName = 'FL Chart App'; +} diff --git a/example/lib/presentation/router/app_router.dart b/example/lib/presentation/router/app_router.dart new file mode 100644 index 0000000..80dc3e1 --- /dev/null +++ b/example/lib/presentation/router/app_router.dart @@ -0,0 +1,37 @@ +import 'package:fl_chart_app/presentation/pages/home_page.dart'; +import 'package:fl_chart_app/presentation/resources/app_colors.dart'; +import 'package:fl_chart_app/util/app_helper.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +final appRouterConfig = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (context, state) => Container(color: AppColors.pageBackground), + redirect: (context, state) { + return '/${ChartType.values.first.name}'; + }, + ), + ...ChartType.values.map( + (ChartType chartType) => GoRoute( + path: '/${chartType.name}', + pageBuilder: (BuildContext context, GoRouterState state) => + MaterialPage( + /// We set a key for HomePage to prevent recreate it + /// when user choose a new chart type to show + key: const ValueKey('home_page'), + child: HomePage(showingChartType: chartType), + ), + ), + ), + GoRoute( + path: '/:any', + builder: (context, state) => Container(color: AppColors.pageBackground), + redirect: (context, state) { + // Unsupported path, we redirect it to /, which redirects it to /line + return '/'; + }, + ), + ], +); diff --git a/example/lib/presentation/samples/bar/bar_chart_sample1.dart b/example/lib/presentation/samples/bar/bar_chart_sample1.dart new file mode 100644 index 0000000..a2b5aa4 --- /dev/null +++ b/example/lib/presentation/samples/bar/bar_chart_sample1.dart @@ -0,0 +1,397 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:fl_chart_app/presentation/resources/app_resources.dart'; +import 'package:fl_chart_app/util/extensions/color_extensions.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; + +class BarChartSample1 extends StatefulWidget { + BarChartSample1({super.key}); + + List get availableColors => const [ + AppColors.contentColorPurple, + AppColors.contentColorYellow, + AppColors.contentColorBlue, + AppColors.contentColorOrange, + AppColors.contentColorPink, + AppColors.contentColorRed, + ]; + + final Color barBackgroundColor = + AppColors.contentColorWhite.darken().withValues(alpha: 0.3); + final Color barColor = AppColors.contentColorWhite; + final Color touchedBarColor = AppColors.contentColorGreen; + + @override + State createState() => BarChartSample1State(); +} + +class BarChartSample1State extends State { + final Duration animDuration = const Duration(milliseconds: 250); + + int touchedIndex = -1; + + bool isPlaying = false; + + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: 1, + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text( + 'Mingguan', + style: TextStyle( + color: AppColors.contentColorGreen, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox( + height: 4, + ), + Text( + 'Grafik konsumsi kalori', + style: TextStyle( + color: AppColors.contentColorGreen.darken(), + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox( + height: 38, + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: BarChart( + isPlaying ? randomData() : mainBarData(), + duration: animDuration, + ), + ), + ), + const SizedBox( + height: 12, + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(8), + child: Align( + alignment: Alignment.topRight, + child: IconButton( + icon: Icon( + isPlaying ? Icons.pause : Icons.play_arrow, + color: AppColors.contentColorGreen, + ), + onPressed: () { + setState(() { + isPlaying = !isPlaying; + if (isPlaying) { + refreshState(); + } + }); + }, + ), + ), + ) + ], + ), + ); + } + + BarChartGroupData makeGroupData( + int x, + double y, { + bool isTouched = false, + Color? barColor, + double width = 22, + List showTooltips = const [], + }) { + barColor ??= widget.barColor; + return BarChartGroupData( + x: x, + barRods: [ + BarChartRodData( + toY: isTouched ? y + 1 : y, + color: isTouched ? widget.touchedBarColor : barColor, + width: width, + borderSide: isTouched + ? BorderSide(color: widget.touchedBarColor.darken(80)) + : const BorderSide(color: Colors.white, width: 0), + backDrawRodData: BackgroundBarChartRodData( + show: true, + toY: 20, + color: widget.barBackgroundColor, + ), + ), + ], + showingTooltipIndicators: showTooltips, + ); + } + + List showingGroups() => List.generate(7, (i) { + switch (i) { + case 0: + return makeGroupData(0, 5, isTouched: i == touchedIndex); + case 1: + return makeGroupData(1, 6.5, isTouched: i == touchedIndex); + case 2: + return makeGroupData(2, 5, isTouched: i == touchedIndex); + case 3: + return makeGroupData(3, 7.5, isTouched: i == touchedIndex); + case 4: + return makeGroupData(4, 9, isTouched: i == touchedIndex); + case 5: + return makeGroupData(5, 11.5, isTouched: i == touchedIndex); + case 6: + return makeGroupData(6, 6.5, isTouched: i == touchedIndex); + default: + return throw Error(); + } + }); + + BarChartData mainBarData() { + return BarChartData( + barTouchData: BarTouchData( + touchTooltipData: BarTouchTooltipData( + getTooltipColor: (_) => Colors.blueGrey, + tooltipHorizontalAlignment: FLHorizontalAlignment.right, + tooltipMargin: -10, + getTooltipItem: (group, groupIndex, rod, rodIndex) { + String weekDay; + switch (group.x) { + case 0: + weekDay = 'Monday'; + break; + case 1: + weekDay = 'Tuesday'; + break; + case 2: + weekDay = 'Wednesday'; + break; + case 3: + weekDay = 'Thursday'; + break; + case 4: + weekDay = 'Friday'; + break; + case 5: + weekDay = 'Saturday'; + break; + case 6: + weekDay = 'Sunday'; + break; + default: + throw Error(); + } + return BarTooltipItem( + '$weekDay\n', + const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 18, + ), + children: [ + TextSpan( + text: (rod.toY - 1).toString(), + style: const TextStyle( + color: Colors.white, //widget.touchedBarColor, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ], + ); + }, + ), + touchCallback: (FlTouchEvent event, barTouchResponse) { + setState(() { + if (!event.isInterestedForInteractions || + barTouchResponse == null || + barTouchResponse.spot == null) { + touchedIndex = -1; + return; + } + touchedIndex = barTouchResponse.spot!.touchedBarGroupIndex; + }); + }, + ), + titlesData: FlTitlesData( + show: true, + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: getTitles, + reservedSize: 38, + ), + ), + leftTitles: const AxisTitles( + sideTitles: SideTitles( + showTitles: false, + ), + ), + ), + borderData: FlBorderData( + show: false, + ), + barGroups: showingGroups(), + gridData: const FlGridData(show: false), + ); + } + + Widget getTitles(double value, TitleMeta meta) { + const style = TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 14, + ); + Widget text; + switch (value.toInt()) { + case 0: + text = const Text('M', style: style); + break; + case 1: + text = const Text('T', style: style); + break; + case 2: + text = const Text('W', style: style); + break; + case 3: + text = const Text('T', style: style); + break; + case 4: + text = const Text('F', style: style); + break; + case 5: + text = const Text('S', style: style); + break; + case 6: + text = const Text('S', style: style); + break; + default: + text = const Text('', style: style); + break; + } + return SideTitleWidget( + meta: meta, + space: 16, + child: text, + ); + } + + BarChartData randomData() { + return BarChartData( + barTouchData: const BarTouchData( + enabled: false, + ), + titlesData: FlTitlesData( + show: true, + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: getTitles, + reservedSize: 38, + ), + ), + leftTitles: const AxisTitles( + sideTitles: SideTitles( + showTitles: false, + ), + ), + topTitles: const AxisTitles( + sideTitles: SideTitles( + showTitles: false, + ), + ), + rightTitles: const AxisTitles( + sideTitles: SideTitles( + showTitles: false, + ), + ), + ), + borderData: FlBorderData( + show: false, + ), + barGroups: List.generate(7, (i) { + switch (i) { + case 0: + return makeGroupData( + 0, + Random().nextInt(15).toDouble() + 6, + barColor: widget.availableColors[ + Random().nextInt(widget.availableColors.length)], + ); + case 1: + return makeGroupData( + 1, + Random().nextInt(15).toDouble() + 6, + barColor: widget.availableColors[ + Random().nextInt(widget.availableColors.length)], + ); + case 2: + return makeGroupData( + 2, + Random().nextInt(15).toDouble() + 6, + barColor: widget.availableColors[ + Random().nextInt(widget.availableColors.length)], + ); + case 3: + return makeGroupData( + 3, + Random().nextInt(15).toDouble() + 6, + barColor: widget.availableColors[ + Random().nextInt(widget.availableColors.length)], + ); + case 4: + return makeGroupData( + 4, + Random().nextInt(15).toDouble() + 6, + barColor: widget.availableColors[ + Random().nextInt(widget.availableColors.length)], + ); + case 5: + return makeGroupData( + 5, + Random().nextInt(15).toDouble() + 6, + barColor: widget.availableColors[ + Random().nextInt(widget.availableColors.length)], + ); + case 6: + return makeGroupData( + 6, + Random().nextInt(15).toDouble() + 6, + barColor: widget.availableColors[ + Random().nextInt(widget.availableColors.length)], + ); + default: + return throw Error(); + } + }), + gridData: const FlGridData(show: false), + ); + } + + Future refreshState() async { + setState(() {}); + await Future.delayed( + animDuration + const Duration(milliseconds: 50), + ); + if (isPlaying) { + await refreshState(); + } + } +} diff --git a/example/lib/presentation/samples/bar/bar_chart_sample2.dart b/example/lib/presentation/samples/bar/bar_chart_sample2.dart new file mode 100644 index 0000000..4edfd36 --- /dev/null +++ b/example/lib/presentation/samples/bar/bar_chart_sample2.dart @@ -0,0 +1,283 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart_app/presentation/resources/app_resources.dart'; +import 'package:fl_chart_app/util/extensions/color_extensions.dart'; +import 'package:flutter/material.dart'; + +class BarChartSample2 extends StatefulWidget { + BarChartSample2({super.key}); + final Color leftBarColor = AppColors.contentColorYellow; + final Color rightBarColor = AppColors.contentColorRed; + final Color avgColor = + AppColors.contentColorOrange.avg(AppColors.contentColorRed); + @override + State createState() => BarChartSample2State(); +} + +class BarChartSample2State extends State { + final double width = 7; + + late List rawBarGroups; + late List showingBarGroups; + + int touchedGroupIndex = -1; + + @override + void initState() { + super.initState(); + final barGroup1 = makeGroupData(0, 5, 12); + final barGroup2 = makeGroupData(1, 16, 12); + final barGroup3 = makeGroupData(2, 18, 5); + final barGroup4 = makeGroupData(3, 20, 16); + final barGroup5 = makeGroupData(4, 17, 6); + final barGroup6 = makeGroupData(5, 19, 1.5); + final barGroup7 = makeGroupData(6, 10, 1.5); + + final items = [ + barGroup1, + barGroup2, + barGroup3, + barGroup4, + barGroup5, + barGroup6, + barGroup7, + ]; + + rawBarGroups = items; + + showingBarGroups = rawBarGroups; + } + + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: 1, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + makeTransactionsIcon(), + const SizedBox( + width: 38, + ), + const Text( + 'Transactions', + style: TextStyle(color: Colors.white, fontSize: 22), + ), + const SizedBox( + width: 4, + ), + const Text( + 'state', + style: TextStyle(color: Color(0xff77839a), fontSize: 16), + ), + ], + ), + const SizedBox( + height: 38, + ), + Expanded( + child: BarChart( + BarChartData( + maxY: 20, + barTouchData: BarTouchData( + touchTooltipData: BarTouchTooltipData( + getTooltipColor: ((group) { + return Colors.grey; + }), + getTooltipItem: (a, b, c, d) => null, + ), + touchCallback: (FlTouchEvent event, response) { + if (response == null || response.spot == null) { + setState(() { + touchedGroupIndex = -1; + showingBarGroups = List.of(rawBarGroups); + }); + return; + } + + touchedGroupIndex = response.spot!.touchedBarGroupIndex; + + setState(() { + if (!event.isInterestedForInteractions) { + touchedGroupIndex = -1; + showingBarGroups = List.of(rawBarGroups); + return; + } + showingBarGroups = List.of(rawBarGroups); + if (touchedGroupIndex != -1) { + var sum = 0.0; + for (final rod + in showingBarGroups[touchedGroupIndex].barRods) { + sum += rod.toY; + } + final avg = sum / + showingBarGroups[touchedGroupIndex] + .barRods + .length; + + showingBarGroups[touchedGroupIndex] = + showingBarGroups[touchedGroupIndex].copyWith( + barRods: showingBarGroups[touchedGroupIndex] + .barRods + .map((rod) { + return rod.copyWith( + toY: avg, color: widget.avgColor); + }).toList(), + ); + } + }); + }, + ), + titlesData: FlTitlesData( + show: true, + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: bottomTitles, + reservedSize: 42, + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 28, + interval: 1, + getTitlesWidget: leftTitles, + ), + ), + ), + borderData: FlBorderData( + show: false, + ), + barGroups: showingBarGroups, + gridData: const FlGridData(show: false), + ), + ), + ), + const SizedBox( + height: 12, + ), + ], + ), + ), + ); + } + + Widget leftTitles(double value, TitleMeta meta) { + const style = TextStyle( + color: Color(0xff7589a2), + fontWeight: FontWeight.bold, + fontSize: 14, + ); + String text; + if (value == 0) { + text = '1K'; + } else if (value == 10) { + text = '5K'; + } else if (value == 19) { + text = '10K'; + } else { + return Container(); + } + return SideTitleWidget( + meta: meta, + space: 0, + child: Text(text, style: style), + ); + } + + Widget bottomTitles(double value, TitleMeta meta) { + final titles = ['Mn', 'Te', 'Wd', 'Tu', 'Fr', 'St', 'Su']; + + final Widget text = Text( + titles[value.toInt()], + style: const TextStyle( + color: Color(0xff7589a2), + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ); + + return SideTitleWidget( + meta: meta, + space: 16, //margin top + child: text, + ); + } + + BarChartGroupData makeGroupData(int x, double y1, double y2) { + return BarChartGroupData( + barsSpace: 4, + x: x, + barRods: [ + BarChartRodData( + toY: y1, + color: widget.leftBarColor, + width: width, + ), + BarChartRodData( + toY: y2, + color: widget.rightBarColor, + width: width, + ), + ], + ); + } + + Widget makeTransactionsIcon() { + const width = 4.5; + const space = 3.5; + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: width, + height: 10, + color: Colors.white.withValues(alpha: 0.4), + ), + const SizedBox( + width: space, + ), + Container( + width: width, + height: 28, + color: Colors.white.withValues(alpha: 0.8), + ), + const SizedBox( + width: space, + ), + Container( + width: width, + height: 42, + color: Colors.white.withValues(alpha: 1), + ), + const SizedBox( + width: space, + ), + Container( + width: width, + height: 28, + color: Colors.white.withValues(alpha: 0.8), + ), + const SizedBox( + width: space, + ), + Container( + width: width, + height: 10, + color: Colors.white.withValues(alpha: 0.4), + ), + ], + ); + } +} diff --git a/example/lib/presentation/samples/bar/bar_chart_sample3.dart b/example/lib/presentation/samples/bar/bar_chart_sample3.dart new file mode 100644 index 0000000..b06296e --- /dev/null +++ b/example/lib/presentation/samples/bar/bar_chart_sample3.dart @@ -0,0 +1,209 @@ +import 'package:fl_chart_app/presentation/resources/app_resources.dart'; +import 'package:fl_chart_app/util/extensions/color_extensions.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; + +class _BarChart extends StatelessWidget { + const _BarChart(); + + @override + Widget build(BuildContext context) { + return BarChart( + BarChartData( + barTouchData: barTouchData, + titlesData: titlesData, + borderData: borderData, + barGroups: barGroups, + gridData: const FlGridData(show: false), + alignment: BarChartAlignment.spaceAround, + maxY: 20, + ), + ); + } + + BarTouchData get barTouchData => BarTouchData( + enabled: false, + touchTooltipData: BarTouchTooltipData( + getTooltipColor: (group) => Colors.transparent, + tooltipPadding: EdgeInsets.zero, + tooltipMargin: 8, + getTooltipItem: ( + BarChartGroupData group, + int groupIndex, + BarChartRodData rod, + int rodIndex, + ) { + return BarTooltipItem( + rod.toY.round().toString(), + const TextStyle( + color: AppColors.contentColorCyan, + fontWeight: FontWeight.bold, + ), + ); + }, + ), + ); + + Widget getTitles(double value, TitleMeta meta) { + final style = TextStyle( + color: AppColors.contentColorBlue.darken(20), + fontWeight: FontWeight.bold, + fontSize: 14, + ); + String text; + switch (value.toInt()) { + case 0: + text = 'Mn'; + break; + case 1: + text = 'Te'; + break; + case 2: + text = 'Wd'; + break; + case 3: + text = 'Tu'; + break; + case 4: + text = 'Fr'; + break; + case 5: + text = 'St'; + break; + case 6: + text = 'Sn'; + break; + default: + text = ''; + break; + } + return SideTitleWidget( + meta: meta, + space: 4, + child: Text(text, style: style), + ); + } + + FlTitlesData get titlesData => FlTitlesData( + show: true, + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 30, + getTitlesWidget: getTitles, + ), + ), + leftTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + ); + + FlBorderData get borderData => FlBorderData( + show: false, + ); + + LinearGradient get _barsGradient => LinearGradient( + colors: [ + AppColors.contentColorBlue.darken(20), + AppColors.contentColorCyan, + ], + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + ); + + List get barGroups => [ + BarChartGroupData( + x: 0, + barRods: [ + BarChartRodData( + toY: 8, + gradient: _barsGradient, + ) + ], + showingTooltipIndicators: [0], + ), + BarChartGroupData( + x: 1, + barRods: [ + BarChartRodData( + toY: 10, + gradient: _barsGradient, + ) + ], + showingTooltipIndicators: [0], + ), + BarChartGroupData( + x: 2, + barRods: [ + BarChartRodData( + toY: 14, + gradient: _barsGradient, + ) + ], + showingTooltipIndicators: [0], + ), + BarChartGroupData( + x: 3, + barRods: [ + BarChartRodData( + toY: 15, + gradient: _barsGradient, + ) + ], + showingTooltipIndicators: [0], + ), + BarChartGroupData( + x: 4, + barRods: [ + BarChartRodData( + toY: 13, + gradient: _barsGradient, + ) + ], + showingTooltipIndicators: [0], + ), + BarChartGroupData( + x: 5, + barRods: [ + BarChartRodData( + toY: 10, + gradient: _barsGradient, + ) + ], + showingTooltipIndicators: [0], + ), + BarChartGroupData( + x: 6, + barRods: [ + BarChartRodData( + toY: 16, + gradient: _barsGradient, + ) + ], + showingTooltipIndicators: [0], + ), + ]; +} + +class BarChartSample3 extends StatefulWidget { + const BarChartSample3({super.key}); + + @override + State createState() => BarChartSample3State(); +} + +class BarChartSample3State extends State { + @override + Widget build(BuildContext context) { + return const AspectRatio( + aspectRatio: 1.6, + child: _BarChart(), + ); + } +} diff --git a/example/lib/presentation/samples/bar/bar_chart_sample4.dart b/example/lib/presentation/samples/bar/bar_chart_sample4.dart new file mode 100644 index 0000000..ae66a30 --- /dev/null +++ b/example/lib/presentation/samples/bar/bar_chart_sample4.dart @@ -0,0 +1,352 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart_app/presentation/resources/app_resources.dart'; +import 'package:fl_chart_app/util/extensions/color_extensions.dart'; +import 'package:flutter/material.dart'; + +class BarChartSample4 extends StatefulWidget { + BarChartSample4({super.key}); + + final Color dark = AppColors.contentColorCyan.darken(60); + final Color normal = AppColors.contentColorCyan.darken(30); + final Color light = AppColors.contentColorCyan; + + @override + State createState() => BarChartSample4State(); +} + +class BarChartSample4State extends State { + Widget bottomTitles(double value, TitleMeta meta) { + const style = TextStyle(fontSize: 10); + String text; + switch (value.toInt()) { + case 0: + text = 'Apr'; + break; + case 1: + text = 'May'; + break; + case 2: + text = 'Jun'; + break; + case 3: + text = 'Jul'; + break; + case 4: + text = 'Aug'; + break; + default: + text = ''; + break; + } + return SideTitleWidget( + meta: meta, + child: Text(text, style: style), + ); + } + + Widget leftTitles(double value, TitleMeta meta) { + if (value == meta.max) { + return Container(); + } + const style = TextStyle( + fontSize: 10, + ); + return SideTitleWidget( + meta: meta, + child: Text( + meta.formattedValue, + style: style, + ), + ); + } + + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: 1.66, + child: Padding( + padding: const EdgeInsets.only(top: 16), + child: LayoutBuilder( + builder: (context, constraints) { + final barsSpace = 4.0 * constraints.maxWidth / 400; + final barsWidth = 8.0 * constraints.maxWidth / 400; + return BarChart( + BarChartData( + alignment: BarChartAlignment.center, + barTouchData: const BarTouchData( + enabled: false, + ), + titlesData: FlTitlesData( + show: true, + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 28, + getTitlesWidget: bottomTitles, + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 40, + getTitlesWidget: leftTitles, + ), + ), + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + ), + gridData: FlGridData( + show: true, + checkToShowHorizontalLine: (value) => value % 10 == 0, + getDrawingHorizontalLine: (value) => FlLine( + color: AppColors.borderColor.withValues(alpha: 0.1), + strokeWidth: 1, + ), + drawVerticalLine: false, + ), + borderData: FlBorderData( + show: false, + ), + groupsSpace: barsSpace, + barGroups: getData(barsWidth, barsSpace), + ), + ); + }, + ), + ), + ); + } + + List getData(double barsWidth, double barsSpace) { + return [ + BarChartGroupData( + x: 0, + barsSpace: barsSpace, + barRods: [ + BarChartRodData( + toY: 17000000000, + rodStackItems: [ + BarChartRodStackItem(0, 2000000000, widget.dark), + BarChartRodStackItem(2000000000, 12000000000, widget.normal), + BarChartRodStackItem(12000000000, 17000000000, widget.light), + ], + borderRadius: BorderRadius.zero, + width: barsWidth, + ), + BarChartRodData( + toY: 24000000000, + rodStackItems: [ + BarChartRodStackItem(0, 13000000000, widget.dark), + BarChartRodStackItem(13000000000, 14000000000, widget.normal), + BarChartRodStackItem(14000000000, 24000000000, widget.light), + ], + borderRadius: BorderRadius.zero, + width: barsWidth, + ), + BarChartRodData( + toY: 23000000000.5, + rodStackItems: [ + BarChartRodStackItem(0, 6000000000.5, widget.dark), + BarChartRodStackItem(6000000000.5, 18000000000, widget.normal), + BarChartRodStackItem(18000000000, 23000000000.5, widget.light), + ], + borderRadius: BorderRadius.zero, + width: barsWidth, + ), + BarChartRodData( + toY: 29000000000, + rodStackItems: [ + BarChartRodStackItem(0, 9000000000, widget.dark), + BarChartRodStackItem(9000000000, 15000000000, widget.normal), + BarChartRodStackItem(15000000000, 29000000000, widget.light), + ], + borderRadius: BorderRadius.zero, + width: barsWidth, + ), + BarChartRodData( + toY: 32000000000, + rodStackItems: [ + BarChartRodStackItem(0, 2000000000.5, widget.dark), + BarChartRodStackItem(2000000000.5, 17000000000.5, widget.normal), + BarChartRodStackItem(17000000000.5, 32000000000, widget.light), + ], + borderRadius: BorderRadius.zero, + width: barsWidth, + ), + ], + ), + BarChartGroupData( + x: 1, + barsSpace: barsSpace, + barRods: [ + BarChartRodData( + toY: 31000000000, + rodStackItems: [ + BarChartRodStackItem(0, 11000000000, widget.dark), + BarChartRodStackItem(11000000000, 18000000000, widget.normal), + BarChartRodStackItem(18000000000, 31000000000, widget.light), + ], + borderRadius: BorderRadius.zero, + width: barsWidth, + ), + BarChartRodData( + toY: 35000000000, + rodStackItems: [ + BarChartRodStackItem(0, 14000000000, widget.dark), + BarChartRodStackItem(14000000000, 27000000000, widget.normal), + BarChartRodStackItem(27000000000, 35000000000, widget.light), + ], + borderRadius: BorderRadius.zero, + width: barsWidth, + ), + BarChartRodData( + toY: 31000000000, + rodStackItems: [ + BarChartRodStackItem(0, 8000000000, widget.dark), + BarChartRodStackItem(8000000000, 24000000000, widget.normal), + BarChartRodStackItem(24000000000, 31000000000, widget.light), + ], + borderRadius: BorderRadius.zero, + width: barsWidth, + ), + BarChartRodData( + toY: 15000000000, + rodStackItems: [ + BarChartRodStackItem(0, 6000000000.5, widget.dark), + BarChartRodStackItem(6000000000.5, 12000000000.5, widget.normal), + BarChartRodStackItem(12000000000.5, 15000000000, widget.light), + ], + borderRadius: BorderRadius.zero, + width: barsWidth, + ), + BarChartRodData( + toY: 17000000000, + rodStackItems: [ + BarChartRodStackItem(0, 9000000000, widget.dark), + BarChartRodStackItem(9000000000, 15000000000, widget.normal), + BarChartRodStackItem(15000000000, 17000000000, widget.light), + ], + borderRadius: BorderRadius.zero, + width: barsWidth, + ), + ], + ), + BarChartGroupData( + x: 2, + barsSpace: barsSpace, + barRods: [ + BarChartRodData( + toY: 34000000000, + rodStackItems: [ + BarChartRodStackItem(0, 6000000000, widget.dark), + BarChartRodStackItem(6000000000, 23000000000, widget.normal), + BarChartRodStackItem(23000000000, 34000000000, widget.light), + ], + borderRadius: BorderRadius.zero, + width: barsWidth, + ), + BarChartRodData( + toY: 32000000000, + rodStackItems: [ + BarChartRodStackItem(0, 7000000000, widget.dark), + BarChartRodStackItem(7000000000, 24000000000, widget.normal), + BarChartRodStackItem(24000000000, 32000000000, widget.light), + ], + borderRadius: BorderRadius.zero, + width: barsWidth, + ), + BarChartRodData( + toY: 14000000000.5, + rodStackItems: [ + BarChartRodStackItem(0, 1000000000.5, widget.dark), + BarChartRodStackItem(1000000000.5, 12000000000, widget.normal), + BarChartRodStackItem(12000000000, 14000000000.5, widget.light), + ], + borderRadius: BorderRadius.zero, + width: barsWidth, + ), + BarChartRodData( + toY: 20000000000, + rodStackItems: [ + BarChartRodStackItem(0, 4000000000, widget.dark), + BarChartRodStackItem(4000000000, 15000000000, widget.normal), + BarChartRodStackItem(15000000000, 20000000000, widget.light), + ], + borderRadius: BorderRadius.zero, + width: barsWidth, + ), + BarChartRodData( + toY: 24000000000, + rodStackItems: [ + BarChartRodStackItem(0, 4000000000, widget.dark), + BarChartRodStackItem(4000000000, 15000000000, widget.normal), + BarChartRodStackItem(15000000000, 24000000000, widget.light), + ], + borderRadius: BorderRadius.zero, + width: barsWidth, + ), + ], + ), + BarChartGroupData( + x: 3, + barsSpace: barsSpace, + barRods: [ + BarChartRodData( + toY: 14000000000, + rodStackItems: [ + BarChartRodStackItem(0, 1000000000.5, widget.dark), + BarChartRodStackItem(1000000000.5, 12000000000, widget.normal), + BarChartRodStackItem(12000000000, 14000000000, widget.light), + ], + borderRadius: BorderRadius.zero, + width: barsWidth, + ), + BarChartRodData( + toY: 27000000000, + rodStackItems: [ + BarChartRodStackItem(0, 7000000000, widget.dark), + BarChartRodStackItem(7000000000, 25000000000, widget.normal), + BarChartRodStackItem(25000000000, 27000000000, widget.light), + ], + borderRadius: BorderRadius.zero, + width: barsWidth, + ), + BarChartRodData( + toY: 29000000000, + rodStackItems: [ + BarChartRodStackItem(0, 6000000000, widget.dark), + BarChartRodStackItem(6000000000, 23000000000, widget.normal), + BarChartRodStackItem(23000000000, 29000000000, widget.light), + ], + borderRadius: BorderRadius.zero, + width: barsWidth, + ), + BarChartRodData( + toY: 16000000000.5, + rodStackItems: [ + BarChartRodStackItem(0, 9000000000, widget.dark), + BarChartRodStackItem(9000000000, 15000000000, widget.normal), + BarChartRodStackItem(15000000000, 16000000000.5, widget.light), + ], + borderRadius: BorderRadius.zero, + width: barsWidth, + ), + BarChartRodData( + toY: 15000000000, + rodStackItems: [ + BarChartRodStackItem(0, 7000000000, widget.dark), + BarChartRodStackItem(7000000000, 12000000000.5, widget.normal), + BarChartRodStackItem(12000000000.5, 15000000000, widget.light), + ], + borderRadius: BorderRadius.zero, + width: barsWidth, + ), + ], + ), + ]; + } +} diff --git a/example/lib/presentation/samples/bar/bar_chart_sample5.dart b/example/lib/presentation/samples/bar/bar_chart_sample5.dart new file mode 100644 index 0000000..8603c54 --- /dev/null +++ b/example/lib/presentation/samples/bar/bar_chart_sample5.dart @@ -0,0 +1,360 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart_app/presentation/resources/app_resources.dart'; +import 'package:fl_chart_app/util/app_utils.dart'; +import 'package:flutter/material.dart'; + +class BarChartSample5 extends StatefulWidget { + const BarChartSample5({super.key}); + + @override + State createState() => BarChartSample5State(); +} + +class BarChartSample5State extends State { + static const double barWidth = 22; + static const shadowOpacity = 0.2; + static const mainItems = >{ + 0: [2, 3, 2.5, 8], + 1: [-1.8, -2.7, -3, -6.5], + 2: [1.5, 2, 3.5, 6], + 3: [1.5, 1.5, 4, 6.5], + 4: [-2, -2, -5, -9], + 5: [-1.2, -1.5, -4.3, -10], + 6: [1.2, 4.8, 5, 5], + }; + int touchedIndex = -1; + + @override + void initState() { + super.initState(); + } + + Widget bottomTitles(double value, TitleMeta meta) { + const style = TextStyle(color: Colors.white, fontSize: 10); + String text; + switch (value.toInt()) { + case 0: + text = 'Mon'; + break; + case 1: + text = 'Tue'; + break; + case 2: + text = 'Wed'; + break; + case 3: + text = 'Thu'; + break; + case 4: + text = 'Fri'; + break; + case 5: + text = 'Sat'; + break; + case 6: + text = 'Sun'; + break; + default: + text = ''; + break; + } + return SideTitleWidget( + meta: meta, + child: Text(text, style: style), + ); + } + + Widget topTitles(double value, TitleMeta meta) { + const style = TextStyle(color: Colors.white, fontSize: 10); + String text; + switch (value.toInt()) { + case 0: + text = 'Mon'; + break; + case 1: + text = 'Tue'; + break; + case 2: + text = 'Wed'; + break; + case 3: + text = 'Thu'; + break; + case 4: + text = 'Fri'; + break; + case 5: + text = 'Sat'; + break; + case 6: + text = 'Sun'; + break; + default: + return Container(); + } + return SideTitleWidget( + meta: meta, + child: Text(text, style: style), + ); + } + + Widget leftTitles(double value, TitleMeta meta) { + const style = TextStyle(color: Colors.white, fontSize: 10); + String text; + if (value == 0) { + text = '0'; + } else { + text = '${value.toInt()}0k'; + } + return SideTitleWidget( + angle: AppUtils().degreeToRadian(value < 0 ? -45 : 45), + meta: meta, + space: 4, + child: Text( + text, + style: style, + textAlign: TextAlign.center, + ), + ); + } + + Widget rightTitles(double value, TitleMeta meta) { + const style = TextStyle(color: Colors.white, fontSize: 10); + String text; + if (value == 0) { + text = '0'; + } else { + text = '${value.toInt()}0k'; + } + return SideTitleWidget( + angle: AppUtils().degreeToRadian(90), + meta: meta, + space: 0, + child: Text( + text, + style: style, + textAlign: TextAlign.center, + ), + ); + } + + BarChartGroupData generateGroup( + int x, + double value1, + double value2, + double value3, + double value4, + ) { + final isTop = value1 > 0; + final sum = value1 + value2 + value3 + value4; + final isTouched = touchedIndex == x; + return BarChartGroupData( + x: x, + groupVertically: true, + showingTooltipIndicators: isTouched ? [0] : [], + barRods: [ + BarChartRodData( + toY: sum, + width: barWidth, + borderRadius: isTop + ? const BorderRadius.only( + topLeft: Radius.circular(6), + topRight: Radius.circular(6), + ) + : const BorderRadius.only( + bottomLeft: Radius.circular(6), + bottomRight: Radius.circular(6), + ), + rodStackItems: [ + BarChartRodStackItem( + 0, + value1, + AppColors.contentColorGreen, + BorderSide( + color: Colors.white, + width: isTouched ? 2 : 0, + ), + ), + BarChartRodStackItem( + value1, + value1 + value2, + AppColors.contentColorYellow, + BorderSide( + color: Colors.white, + width: isTouched ? 2 : 0, + ), + ), + BarChartRodStackItem( + value1 + value2, + value1 + value2 + value3, + AppColors.contentColorPink, + BorderSide( + color: Colors.white, + width: isTouched ? 2 : 0, + ), + ), + BarChartRodStackItem( + value1 + value2 + value3, + value1 + value2 + value3 + value4, + AppColors.contentColorBlue, + BorderSide( + color: Colors.white, + width: isTouched ? 2 : 0, + ), + ), + ], + ), + BarChartRodData( + toY: -sum, + width: barWidth, + color: Colors.transparent, + borderRadius: isTop + ? const BorderRadius.only( + bottomLeft: Radius.circular(6), + bottomRight: Radius.circular(6), + ) + : const BorderRadius.only( + topLeft: Radius.circular(6), + topRight: Radius.circular(6), + ), + rodStackItems: [ + BarChartRodStackItem( + 0, + -value1, + AppColors.contentColorGreen.withValues( + alpha: isTouched ? shadowOpacity * 2 : shadowOpacity), + const BorderSide(color: Colors.transparent), + ), + BarChartRodStackItem( + -value1, + -(value1 + value2), + AppColors.contentColorYellow.withValues( + alpha: isTouched ? shadowOpacity * 2 : shadowOpacity), + const BorderSide(color: Colors.transparent), + ), + BarChartRodStackItem( + -(value1 + value2), + -(value1 + value2 + value3), + AppColors.contentColorPink.withValues( + alpha: isTouched ? shadowOpacity * 2 : shadowOpacity), + const BorderSide(color: Colors.transparent), + ), + BarChartRodStackItem( + -(value1 + value2 + value3), + -(value1 + value2 + value3 + value4), + AppColors.contentColorBlue.withValues( + alpha: isTouched ? shadowOpacity * 2 : shadowOpacity), + const BorderSide(color: Colors.transparent), + ), + ], + ), + ], + ); + } + + bool isShadowBar(int rodIndex) => rodIndex == 1; + + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: 0.8, + child: Padding( + padding: const EdgeInsets.only(top: 16), + child: BarChart( + BarChartData( + alignment: BarChartAlignment.center, + maxY: 20, + minY: -20, + groupsSpace: 12, + barTouchData: BarTouchData( + handleBuiltInTouches: false, + touchCallback: (FlTouchEvent event, barTouchResponse) { + if (!event.isInterestedForInteractions || + barTouchResponse == null || + barTouchResponse.spot == null) { + setState(() { + touchedIndex = -1; + }); + return; + } + final rodIndex = barTouchResponse.spot!.touchedRodDataIndex; + if (isShadowBar(rodIndex)) { + setState(() { + touchedIndex = -1; + }); + return; + } + setState(() { + touchedIndex = barTouchResponse.spot!.touchedBarGroupIndex; + }); + }, + ), + titlesData: FlTitlesData( + show: true, + topTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 32, + getTitlesWidget: topTitles, + ), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 32, + getTitlesWidget: bottomTitles, + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: leftTitles, + interval: 5, + reservedSize: 42, + ), + ), + rightTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: rightTitles, + interval: 5, + reservedSize: 42, + ), + ), + ), + gridData: FlGridData( + show: true, + checkToShowHorizontalLine: (value) => value % 5 == 0, + getDrawingHorizontalLine: (value) { + if (value == 0) { + return FlLine( + color: AppColors.borderColor.withValues(alpha: 0.1), + strokeWidth: 3, + ); + } + return FlLine( + color: AppColors.borderColor.withValues(alpha: 0.05), + strokeWidth: 0.8, + ); + }, + ), + borderData: FlBorderData( + show: false, + ), + barGroups: mainItems.entries + .map( + (e) => generateGroup( + e.key, + e.value[0], + e.value[1], + e.value[2], + e.value[3], + ), + ) + .toList(), + ), + ), + ), + ); + } +} diff --git a/example/lib/presentation/samples/bar/bar_chart_sample6.dart b/example/lib/presentation/samples/bar/bar_chart_sample6.dart new file mode 100644 index 0000000..6a4c7aa --- /dev/null +++ b/example/lib/presentation/samples/bar/bar_chart_sample6.dart @@ -0,0 +1,184 @@ +import 'package:fl_chart_app/presentation/resources/app_resources.dart'; +import 'package:fl_chart_app/presentation/widgets/legend_widget.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; + +class BarChartSample6 extends StatelessWidget { + const BarChartSample6({super.key}); + + final pilateColor = AppColors.contentColorPurple; + final cyclingColor = AppColors.contentColorCyan; + final quickWorkoutColor = AppColors.contentColorBlue; + final betweenSpace = 0.2; + + BarChartGroupData generateGroupData( + int x, + double pilates, + double quickWorkout, + double cycling, + ) { + return BarChartGroupData( + x: x, + groupVertically: true, + barRods: [ + BarChartRodData( + fromY: 0, + toY: pilates, + color: pilateColor, + width: 5, + ), + BarChartRodData( + fromY: pilates + betweenSpace, + toY: pilates + betweenSpace + quickWorkout, + color: quickWorkoutColor, + width: 5, + ), + BarChartRodData( + fromY: pilates + betweenSpace + quickWorkout + betweenSpace, + toY: pilates + betweenSpace + quickWorkout + betweenSpace + cycling, + color: cyclingColor, + width: 5, + ), + ], + ); + } + + Widget bottomTitles(double value, TitleMeta meta) { + const style = TextStyle(fontSize: 10); + String text; + switch (value.toInt()) { + case 0: + text = 'JAN'; + break; + case 1: + text = 'FEB'; + break; + case 2: + text = 'MAR'; + break; + case 3: + text = 'APR'; + break; + case 4: + text = 'MAY'; + break; + case 5: + text = 'JUN'; + break; + case 6: + text = 'JUL'; + break; + case 7: + text = 'AUG'; + break; + case 8: + text = 'SEP'; + break; + case 9: + text = 'OCT'; + break; + case 10: + text = 'NOV'; + break; + case 11: + text = 'DEC'; + break; + default: + text = ''; + } + return SideTitleWidget( + meta: meta, + child: Text(text, style: style), + ); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Activity', + style: TextStyle( + color: AppColors.contentColorBlue, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + LegendsListWidget( + legends: [ + Legend('Pilates', pilateColor), + Legend('Quick workouts', quickWorkoutColor), + Legend('Cycling', cyclingColor), + ], + ), + const SizedBox(height: 14), + AspectRatio( + aspectRatio: 2, + child: BarChart( + BarChartData( + alignment: BarChartAlignment.spaceBetween, + titlesData: FlTitlesData( + leftTitles: const AxisTitles(), + rightTitles: const AxisTitles(), + topTitles: const AxisTitles(), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: bottomTitles, + reservedSize: 20, + ), + ), + ), + barTouchData: const BarTouchData(enabled: false), + borderData: FlBorderData(show: false), + gridData: const FlGridData(show: false), + barGroups: [ + generateGroupData(0, 2, 3, 2), + generateGroupData(1, 2, 5, 1.7), + generateGroupData(2, 1.3, 3.1, 2.8), + generateGroupData(3, 3.1, 4, 3.1), + generateGroupData(4, 0.8, 3.3, 3.4), + generateGroupData(5, 2, 5.6, 1.8), + generateGroupData(6, 1.3, 3.2, 2), + generateGroupData(7, 2.3, 3.2, 3), + generateGroupData(8, 2, 4.8, 2.5), + generateGroupData(9, 1.2, 3.2, 2.5), + generateGroupData(10, 1, 4.8, 3), + generateGroupData(11, 2, 4.4, 2.8), + ], + maxY: 11 + (betweenSpace * 3), + extraLinesData: ExtraLinesData( + horizontalLines: [ + HorizontalLine( + y: 3.3, + color: pilateColor, + strokeWidth: 1, + dashArray: [20, 4], + ), + HorizontalLine( + y: 8, + color: quickWorkoutColor, + strokeWidth: 1, + dashArray: [20, 4], + ), + HorizontalLine( + y: 11, + color: cyclingColor, + strokeWidth: 1, + dashArray: [20, 4], + ), + ], + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/example/lib/presentation/samples/bar/bar_chart_sample7.dart b/example/lib/presentation/samples/bar/bar_chart_sample7.dart new file mode 100644 index 0000000..bd92ba5 --- /dev/null +++ b/example/lib/presentation/samples/bar/bar_chart_sample7.dart @@ -0,0 +1,254 @@ +import 'dart:math' as math; + +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart_app/presentation/resources/app_resources.dart'; +import 'package:flutter/material.dart'; + +class BarChartSample7 extends StatefulWidget { + BarChartSample7({super.key}); + + final shadowColor = const Color(0xFFCCCCCC); + final dataList = [ + const _BarData(AppColors.contentColorYellow, 18, 18), + const _BarData(AppColors.contentColorGreen, 17, 8), + const _BarData(AppColors.contentColorOrange, 10, 15), + const _BarData(AppColors.contentColorPink, 2.5, 5), + const _BarData(AppColors.contentColorBlue, 2, 2.5), + const _BarData(AppColors.contentColorRed, 2, 2), + ]; + + @override + State createState() => _BarChartSample7State(); +} + +class _BarChartSample7State extends State { + BarChartGroupData generateBarGroup( + int x, + Color color, + double value, + double shadowValue, + ) { + return BarChartGroupData( + x: x, + barRods: [ + BarChartRodData( + toY: value, + color: color, + width: 6, + ), + BarChartRodData( + toY: shadowValue, + color: widget.shadowColor, + width: 6, + ), + ], + showingTooltipIndicators: touchedGroupIndex == x ? [0] : [], + ); + } + + int touchedGroupIndex = -1; + + int rotationTurns = 1; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(24), + child: Column( + children: [ + Row( + children: [ + Expanded(child: Container()), + const Text( + 'Horizontal Bar Chart', + style: TextStyle( + color: AppColors.mainTextColor1, + fontSize: 20, + ), + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Tooltip( + message: 'Rotate the chart 90 degrees (cw)', + child: IconButton( + onPressed: () { + setState(() { + rotationTurns += 1; + }); + }, + icon: RotatedBox( + quarterTurns: rotationTurns - 1, + child: const Icon( + Icons.rotate_90_degrees_cw, + ), + ), + ), + ), + )), + ], + ), + const SizedBox(height: 18), + AspectRatio( + aspectRatio: 1.4, + child: BarChart( + BarChartData( + alignment: BarChartAlignment.spaceBetween, + rotationQuarterTurns: rotationTurns, + borderData: FlBorderData( + show: true, + border: Border.symmetric( + horizontal: BorderSide( + color: AppColors.borderColor.withValues(alpha: 0.2), + ), + ), + ), + titlesData: FlTitlesData( + show: true, + leftTitles: const AxisTitles( + drawBelowEverything: true, + sideTitles: SideTitles( + showTitles: true, + reservedSize: 30, + ), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 36, + getTitlesWidget: (value, meta) { + final index = value.toInt(); + return SideTitleWidget( + meta: meta, + child: _IconWidget( + color: widget.dataList[index].color, + isSelected: touchedGroupIndex == index, + ), + ); + }, + ), + ), + rightTitles: const AxisTitles(), + topTitles: const AxisTitles(), + ), + gridData: FlGridData( + show: true, + drawVerticalLine: false, + getDrawingHorizontalLine: (value) => FlLine( + color: AppColors.borderColor.withValues(alpha: 0.2), + strokeWidth: 1, + ), + ), + barGroups: widget.dataList.asMap().entries.map((e) { + final index = e.key; + final data = e.value; + return generateBarGroup( + index, + data.color, + data.value, + data.shadowValue, + ); + }).toList(), + maxY: 20, + barTouchData: BarTouchData( + enabled: true, + handleBuiltInTouches: false, + touchTooltipData: BarTouchTooltipData( + getTooltipColor: (group) => Colors.transparent, + tooltipMargin: 0, + getTooltipItem: ( + BarChartGroupData group, + int groupIndex, + BarChartRodData rod, + int rodIndex, + ) { + return BarTooltipItem( + rod.toY.toString(), + TextStyle( + fontWeight: FontWeight.bold, + color: rod.color, + fontSize: 18, + shadows: const [ + Shadow( + color: Colors.black26, + blurRadius: 12, + ) + ], + ), + ); + }, + ), + touchCallback: (event, response) { + if (event.isInterestedForInteractions && + response != null && + response.spot != null) { + setState(() { + touchedGroupIndex = response.spot!.touchedBarGroupIndex; + }); + } else { + setState(() { + touchedGroupIndex = -1; + }); + } + }, + ), + ), + ), + ), + ], + ), + ); + } +} + +class _BarData { + const _BarData(this.color, this.value, this.shadowValue); + + final Color color; + final double value; + final double shadowValue; +} + +class _IconWidget extends ImplicitlyAnimatedWidget { + const _IconWidget({ + required this.color, + required this.isSelected, + }) : super(duration: const Duration(milliseconds: 300)); + final Color color; + final bool isSelected; + + @override + ImplicitlyAnimatedWidgetState createState() => + _IconWidgetState(); +} + +class _IconWidgetState extends AnimatedWidgetBaseState<_IconWidget> { + Tween? _rotationTween; + + @override + Widget build(BuildContext context) { + final rotation = math.pi * 4 * _rotationTween!.evaluate(animation); + final scale = 1 + _rotationTween!.evaluate(animation) * 0.5; + return Transform( + transform: Matrix4.rotationZ(rotation).scaled(scale, scale), + origin: const Offset(14, 14), + child: Icon( + widget.isSelected ? Icons.face_retouching_natural : Icons.face, + color: widget.color, + size: 28, + ), + ); + } + + @override + void forEachTween(TweenVisitor visitor) { + _rotationTween = visitor( + _rotationTween, + widget.isSelected ? 1.0 : 0.0, + (dynamic value) => Tween( + begin: value as double, + end: widget.isSelected ? 1.0 : 0.0, + ), + ) as Tween?; + } +} diff --git a/example/lib/presentation/samples/bar/bar_chart_sample8.dart b/example/lib/presentation/samples/bar/bar_chart_sample8.dart new file mode 100644 index 0000000..434208e --- /dev/null +++ b/example/lib/presentation/samples/bar/bar_chart_sample8.dart @@ -0,0 +1,174 @@ +import 'dart:math'; + +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart_app/presentation/resources/app_resources.dart'; +import 'package:fl_chart_app/util/extensions/color_extensions.dart'; +import 'package:flutter/material.dart'; + +class BarChartSample8 extends StatefulWidget { + BarChartSample8({super.key}); + + final Color barBackgroundColor = + AppColors.contentColorWhite.darken().withValues(alpha: 0.3); + final Color barColor = AppColors.contentColorWhite; + + @override + State createState() => BarChartSample1State(); +} + +class BarChartSample1State extends State { + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: 1, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.graphic_eq), + const SizedBox( + width: 32, + ), + Text( + 'Sales forecasting chart', + style: TextStyle( + color: widget.barColor, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox( + height: 32, + ), + Expanded( + child: BarChart( + randomData(), + ), + ), + ], + ), + ), + ); + } + + BarChartGroupData makeGroupData( + int x, + double y, + FlErrorRange errorRange, + ) { + return BarChartGroupData( + x: x, + barRods: [ + BarChartRodData( + toY: y, + toYErrorRange: errorRange, + color: x >= 4 ? Colors.transparent : widget.barColor, + borderRadius: BorderRadius.zero, + borderDashArray: x >= 4 ? [4, 4] : null, + width: 22, + borderSide: BorderSide(color: widget.barColor, width: 2.0), + ), + ], + ); + } + + Widget getTitles(double value, TitleMeta meta) { + const style = TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 14, + ); + List days = ['M', 'T', 'W', 'T', 'F', 'S', 'S']; + + Widget text = Text( + days[value.toInt()], + style: style, + ); + + return SideTitleWidget( + meta: meta, + space: 16, + child: text, + ); + } + + BarChartData randomData() { + return BarChartData( + maxY: 300.0, + barTouchData: const BarTouchData( + enabled: false, + ), + titlesData: FlTitlesData( + show: true, + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: getTitles, + reservedSize: 38, + ), + ), + leftTitles: const AxisTitles( + sideTitles: SideTitles( + reservedSize: 30, + showTitles: true, + ), + ), + topTitles: const AxisTitles( + sideTitles: SideTitles( + showTitles: false, + ), + ), + rightTitles: const AxisTitles( + sideTitles: SideTitles( + showTitles: false, + ), + ), + ), + borderData: FlBorderData( + show: false, + ), + barGroups: List.generate( + 7, + (i) { + final y = Random().nextInt(290).toDouble() + 10; + final lowerBy = y < 50 + ? Random().nextDouble() * 10 + : Random().nextDouble() * 30 + 5; + final upperBy = y > 290 + ? Random().nextDouble() * 10 + : Random().nextDouble() * 30 + 5; + return makeGroupData( + i, + y, + FlErrorRange( + lowerBy: lowerBy, + upperBy: upperBy, + ), + ); + }, + ), + gridData: const FlGridData(show: false), + errorIndicatorData: FlErrorIndicatorData( + painter: _errorPainter, + ), + ); + } + + FlSpotErrorRangePainter _errorPainter( + BarChartSpotErrorRangeCallbackInput input, + ) => + FlSimpleErrorPainter( + lineWidth: 2.0, + capLength: 14, + lineColor: input.groupIndex < 4 + ? AppColors.contentColorOrange + : AppColors.primary.withValues(alpha: 0.5), + ); +} diff --git a/example/lib/presentation/samples/candlestick/candlestick_chart_sample1.dart b/example/lib/presentation/samples/candlestick/candlestick_chart_sample1.dart new file mode 100644 index 0000000..f78d504 --- /dev/null +++ b/example/lib/presentation/samples/candlestick/candlestick_chart_sample1.dart @@ -0,0 +1,348 @@ +import 'package:equatable/equatable.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart_app/presentation/resources/app_colors.dart'; +import 'package:fl_chart_app/util/csv_parser.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class CandlestickChartSample1 extends StatefulWidget { + const CandlestickChartSample1({super.key}); + + @override + State createState() => CandlestickChartSample1State(); +} + +class CandlestickChartSample1State extends State { + List>? _btcMonthlyData; + + int _currentMonthIndex = 0; + late final List monthsNames; + + final int minDays = 1; + final int maxDays = 31; + late final FlLine _gridLine; + + @override + void initState() { + monthsNames = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', + ]; + _loadData(); + _gridLine = FlLine( + color: Colors.blueGrey.withValues(alpha: 0.4), + strokeWidth: 0.4, + dashArray: [8, 4], + ); + super.initState(); + } + + void _loadData() async { + final data = await rootBundle + .loadString('assets/data/bitcoin_2023-01-01_2023-12-31.csv'); + final rows = CsvParser.parse(data); + if (!mounted) { + return; + } + setState(() { + final allData = rows.skip(1).map((row) { + // 2023-12-31,2024-01-01 + return _BtcCandlestickData( + datetime: DateTime.parse(row[0]), + open: double.parse(row[2]), + high: double.parse(row[3]), + low: double.parse(row[4]), + close: double.parse(row[5]), + volume: double.parse(row[6]), + marketCap: double.parse(row[7]), + ); + }).toList(); + + _btcMonthlyData = List.generate(12, (index) { + final month = index + 1; + final monthData = allData + .where((element) => element.datetime.month == month) + .toList(); + monthData.sort((a, b) => a.datetime.compareTo(b.datetime)); + return monthData; + }); + }); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const SizedBox(height: 18), + const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'BTC Price 2024', + style: TextStyle( + color: AppColors.contentColorYellow, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 18), + Row( + children: [ + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: IconButton( + onPressed: _canGoPrevious ? _previousMonth : null, + icon: const Icon(Icons.navigate_before_rounded), + ), + ), + ), + SizedBox( + width: 92, + child: Text( + monthsNames[_currentMonthIndex], + textAlign: TextAlign.center, + style: const TextStyle( + color: AppColors.contentColorWhite, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: IconButton( + onPressed: _canGoNext ? _nextMonth : null, + icon: const Icon(Icons.navigate_next_rounded), + ), + ), + ), + ], + ), + const SizedBox(height: 18), + AspectRatio( + aspectRatio: 1.5, + child: Stack( + children: [ + if (_btcMonthlyData != null) + Padding( + padding: const EdgeInsets.only( + top: 0.0, + right: 18.0, + ), + child: CandlestickChart( + CandlestickChartData( + candlestickSpots: _btcMonthlyData![_currentMonthIndex] + .asMap() + .entries + .map((entry) { + final index = entry.key; + final data = entry.value; + return CandlestickSpot( + x: index.toDouble(), + open: data.open, + high: data.high, + low: data.low, + close: data.close, + ); + }).toList(), + minX: 0, + maxX: 31, + gridData: FlGridData( + show: true, + getDrawingHorizontalLine: (_) => _gridLine, + getDrawingVerticalLine: (_) => _gridLine, + ), + titlesData: FlTitlesData( + show: true, + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + leftTitles: AxisTitles( + drawBelowEverything: true, + sideTitles: SideTitles( + showTitles: true, + maxIncluded: false, + minIncluded: false, + reservedSize: 60, + getTitlesWidget: _leftTitles, + ), + ), + bottomTitles: AxisTitles( + axisNameWidget: Container( + margin: const EdgeInsets.only(bottom: 20), + child: const Text( + 'Day of month', + style: TextStyle( + color: AppColors.contentColorGreen, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ), + axisNameSize: 40, + sideTitles: SideTitles( + showTitles: true, + reservedSize: 38, + maxIncluded: false, + interval: 1, + getTitlesWidget: _bottomTitles, + ), + ), + ), + touchedPointIndicator: AxisSpotIndicator( + painter: AxisLinesIndicatorPainter( + verticalLineProvider: (x) { + final data = + _btcMonthlyData![_currentMonthIndex][x.toInt()]; + + return VerticalLine( + x: x, + color: (data.isUp + ? AppColors.contentColorGreen + : AppColors.contentColorRed) + .withValues(alpha: 0.5), + strokeWidth: 1, + ); + }, + horizontalLineProvider: (y) => HorizontalLine( + y: y, + label: HorizontalLineLabel( + show: true, + style: const TextStyle( + color: AppColors.contentColorYellow, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + labelResolver: (hLine) => + hLine.y.toInt().toString(), + alignment: Alignment.topLeft), + color: AppColors.contentColorYellow.withValues( + alpha: 0.8, + ), + strokeWidth: 1, + ), + ), + ), + ), + ), + ), + if (_btcMonthlyData == null) + const Center( + child: CircularProgressIndicator(), + ) + ], + ), + ), + ], + ); + } + + bool get _canGoNext => _currentMonthIndex < 11; + + bool get _canGoPrevious => _currentMonthIndex > 0; + + void _previousMonth() { + if (!_canGoPrevious) { + return; + } + + setState(() { + _currentMonthIndex--; + }); + } + + void _nextMonth() { + if (!_canGoNext) { + return; + } + setState(() { + _currentMonthIndex++; + }); + } + + Widget _bottomTitles(double value, TitleMeta meta) { + final day = value.toInt() + 1; + + final isImportantToShow = day % 5 == 0 || day == 1; + + if (!isImportantToShow) { + return const SizedBox(); + } + + return SideTitleWidget( + meta: meta, + child: Text( + day.toString(), + style: const TextStyle( + color: AppColors.contentColorGreen, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ); + } + + Widget _leftTitles(double value, TitleMeta meta) { + return SideTitleWidget( + meta: meta, + child: Text( + meta.formattedValue, + style: const TextStyle( + color: AppColors.contentColorYellow, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ); + } +} + +class _BtcCandlestickData with EquatableMixin { + _BtcCandlestickData({ + required this.datetime, + required this.open, + required this.high, + required this.low, + required this.close, + required this.volume, + required this.marketCap, + }); + + final DateTime datetime; + final double open; + final double high; + final double low; + final double close; + final double volume; + final double marketCap; + + bool get isUp => open < close; + + @override + List get props => [ + datetime, + open, + high, + low, + close, + volume, + marketCap, + ]; +} diff --git a/example/lib/presentation/samples/chart_sample.dart b/example/lib/presentation/samples/chart_sample.dart new file mode 100644 index 0000000..ea1eef1 --- /dev/null +++ b/example/lib/presentation/samples/chart_sample.dart @@ -0,0 +1,48 @@ +import 'package:fl_chart_app/urls.dart'; +import 'package:fl_chart_app/util/app_helper.dart'; +import 'package:flutter/cupertino.dart'; + +abstract class ChartSample { + final int number; + final WidgetBuilder builder; + ChartType get type; + String get name => '${type.displayName} Sample $number'; + String get url => Urls.getChartSourceCodeUrl(type, number); + ChartSample(this.number, this.builder); +} + +class LineChartSample extends ChartSample { + LineChartSample(super.number, super.builder); + @override + ChartType get type => ChartType.line; +} + +class BarChartSample extends ChartSample { + BarChartSample(super.number, super.builder); + @override + ChartType get type => ChartType.bar; +} + +class PieChartSample extends ChartSample { + PieChartSample(super.number, super.builder); + @override + ChartType get type => ChartType.pie; +} + +class ScatterChartSample extends ChartSample { + ScatterChartSample(super.number, super.builder); + @override + ChartType get type => ChartType.scatter; +} + +class RadarChartSample extends ChartSample { + RadarChartSample(super.number, super.builder); + @override + ChartType get type => ChartType.radar; +} + +class CandlestickChartSample extends ChartSample { + CandlestickChartSample(super.number, super.builder); + @override + ChartType get type => ChartType.candlestick; +} diff --git a/example/lib/presentation/samples/chart_samples.dart b/example/lib/presentation/samples/chart_samples.dart new file mode 100644 index 0000000..74d2ec7 --- /dev/null +++ b/example/lib/presentation/samples/chart_samples.dart @@ -0,0 +1,76 @@ +import 'package:fl_chart_app/presentation/samples/candlestick/candlestick_chart_sample1.dart'; +import 'package:fl_chart_app/util/app_helper.dart'; + +import 'bar/bar_chart_sample1.dart'; +import 'bar/bar_chart_sample2.dart'; +import 'bar/bar_chart_sample3.dart'; +import 'bar/bar_chart_sample4.dart'; +import 'bar/bar_chart_sample5.dart'; +import 'bar/bar_chart_sample6.dart'; +import 'bar/bar_chart_sample7.dart'; +import 'bar/bar_chart_sample8.dart'; +import 'chart_sample.dart'; +import 'line/line_chart_sample1.dart'; +import 'line/line_chart_sample10.dart'; +import 'line/line_chart_sample11.dart'; +import 'line/line_chart_sample12.dart'; +import 'line/line_chart_sample13.dart'; +import 'line/line_chart_sample2.dart'; +import 'line/line_chart_sample3.dart'; +import 'line/line_chart_sample4.dart'; +import 'line/line_chart_sample5.dart'; +import 'line/line_chart_sample6.dart'; +import 'line/line_chart_sample7.dart'; +import 'line/line_chart_sample8.dart'; +import 'line/line_chart_sample9.dart'; +import 'pie/pie_chart_sample1.dart'; +import 'pie/pie_chart_sample2.dart'; +import 'pie/pie_chart_sample3.dart'; +import 'radar/radar_chart_sample1.dart'; +import 'scatter/scatter_chart_sample1.dart'; +import 'scatter/scatter_chart_sample2.dart'; + +class ChartSamples { + static final Map> samples = { + ChartType.line: [ + LineChartSample(1, (context) => const LineChartSample1()), + LineChartSample(2, (context) => const LineChartSample2()), + LineChartSample(3, (context) => LineChartSample3()), + LineChartSample(4, (context) => LineChartSample4()), + LineChartSample(5, (context) => const LineChartSample5()), + LineChartSample(6, (context) => LineChartSample6()), + LineChartSample(7, (context) => LineChartSample7()), + LineChartSample(8, (context) => const LineChartSample8()), + LineChartSample(9, (context) => LineChartSample9()), + LineChartSample(10, (context) => const LineChartSample10()), + LineChartSample(11, (context) => const LineChartSample11()), + LineChartSample(12, (context) => const LineChartSample12()), + LineChartSample(13, (context) => const LineChartSample13()), + ], + ChartType.bar: [ + BarChartSample(1, (context) => BarChartSample1()), + BarChartSample(2, (context) => BarChartSample2()), + BarChartSample(3, (context) => const BarChartSample3()), + BarChartSample(4, (context) => BarChartSample4()), + BarChartSample(5, (context) => const BarChartSample5()), + BarChartSample(6, (context) => const BarChartSample6()), + BarChartSample(7, (context) => BarChartSample7()), + BarChartSample(8, (context) => BarChartSample8()), + ], + ChartType.pie: [ + PieChartSample(1, (context) => const PieChartSample1()), + PieChartSample(2, (context) => const PieChartSample2()), + PieChartSample(3, (context) => const PieChartSample3()), + ], + ChartType.scatter: [ + ScatterChartSample(1, (context) => ScatterChartSample1()), + ScatterChartSample(2, (context) => const ScatterChartSample2()), + ], + ChartType.radar: [ + RadarChartSample(1, (context) => RadarChartSample1()), + ], + ChartType.candlestick: [ + CandlestickChartSample(1, (context) => const CandlestickChartSample1()), + ] + }; +} diff --git a/example/lib/presentation/samples/line/line_chart_sample1.dart b/example/lib/presentation/samples/line/line_chart_sample1.dart new file mode 100644 index 0000000..f9ad583 --- /dev/null +++ b/example/lib/presentation/samples/line/line_chart_sample1.dart @@ -0,0 +1,366 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart_app/presentation/resources/app_resources.dart'; +import 'package:flutter/material.dart'; + +class _LineChart extends StatelessWidget { + const _LineChart({required this.isShowingMainData}); + + final bool isShowingMainData; + + @override + Widget build(BuildContext context) { + return LineChart( + isShowingMainData ? sampleData1 : sampleData2, + duration: const Duration(milliseconds: 250), + ); + } + + LineChartData get sampleData1 => LineChartData( + lineTouchData: lineTouchData1, + gridData: gridData, + titlesData: titlesData1, + borderData: borderData, + lineBarsData: lineBarsData1, + minX: 0, + maxX: 14, + maxY: 4, + minY: 0, + ); + + LineChartData get sampleData2 => LineChartData( + lineTouchData: lineTouchData2, + gridData: gridData, + titlesData: titlesData2, + borderData: borderData, + lineBarsData: lineBarsData2, + minX: 0, + maxX: 14, + maxY: 6, + minY: 0, + ); + + LineTouchData get lineTouchData1 => LineTouchData( + handleBuiltInTouches: true, + touchTooltipData: LineTouchTooltipData( + getTooltipColor: (touchedSpot) => + Colors.blueGrey.withValues(alpha: 0.8), + ), + ); + + FlTitlesData get titlesData1 => FlTitlesData( + bottomTitles: AxisTitles( + sideTitles: bottomTitles, + ), + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + leftTitles: AxisTitles( + sideTitles: leftTitles(), + ), + ); + + List get lineBarsData1 => [ + lineChartBarData1_1, + lineChartBarData1_2, + lineChartBarData1_3, + ]; + + LineTouchData get lineTouchData2 => const LineTouchData( + enabled: false, + ); + + FlTitlesData get titlesData2 => FlTitlesData( + bottomTitles: AxisTitles( + sideTitles: bottomTitles, + ), + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + leftTitles: AxisTitles( + sideTitles: leftTitles(), + ), + ); + + List get lineBarsData2 => [ + lineChartBarData2_1, + lineChartBarData2_2, + lineChartBarData2_3, + ]; + + Widget leftTitleWidgets(double value, TitleMeta meta) { + const style = TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + ); + String text; + switch (value.toInt()) { + case 1: + text = '1m'; + break; + case 2: + text = '2m'; + break; + case 3: + text = '3m'; + break; + case 4: + text = '5m'; + break; + case 5: + text = '6m'; + break; + default: + return Container(); + } + + return SideTitleWidget( + meta: meta, + child: Text( + text, + style: style, + textAlign: TextAlign.center, + ), + ); + } + + SideTitles leftTitles() => SideTitles( + getTitlesWidget: leftTitleWidgets, + showTitles: true, + interval: 1, + reservedSize: 40, + ); + + Widget bottomTitleWidgets(double value, TitleMeta meta) { + const style = TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ); + Widget text; + switch (value.toInt()) { + case 2: + text = const Text('SEPT', style: style); + break; + case 7: + text = const Text('OCT', style: style); + break; + case 12: + text = const Text('DEC', style: style); + break; + default: + text = const Text(''); + break; + } + + return SideTitleWidget( + meta: meta, + space: 10, + child: text, + ); + } + + SideTitles get bottomTitles => SideTitles( + showTitles: true, + reservedSize: 32, + interval: 1, + getTitlesWidget: bottomTitleWidgets, + ); + + FlGridData get gridData => const FlGridData(show: false); + + FlBorderData get borderData => FlBorderData( + show: true, + border: Border( + bottom: BorderSide( + color: AppColors.primary.withValues(alpha: 0.2), width: 4), + left: const BorderSide(color: Colors.transparent), + right: const BorderSide(color: Colors.transparent), + top: const BorderSide(color: Colors.transparent), + ), + ); + + LineChartBarData get lineChartBarData1_1 => LineChartBarData( + isCurved: true, + color: AppColors.contentColorGreen, + barWidth: 8, + isStrokeCapRound: true, + dotData: const FlDotData(show: false), + belowBarData: BarAreaData(show: false), + spots: const [ + FlSpot(1, 1), + FlSpot(3, 1.5), + FlSpot(5, 1.4), + FlSpot(7, 3.4), + FlSpot(10, 2), + FlSpot(12, 2.2), + FlSpot(13, 1.8), + ], + ); + + LineChartBarData get lineChartBarData1_2 => LineChartBarData( + isCurved: true, + color: AppColors.contentColorPink, + barWidth: 8, + isStrokeCapRound: true, + dotData: const FlDotData(show: false), + belowBarData: BarAreaData( + show: false, + color: AppColors.contentColorPink.withValues(alpha: 0), + ), + spots: const [ + FlSpot(1, 1), + FlSpot(3, 2.8), + FlSpot(7, 1.2), + FlSpot(10, 2.8), + FlSpot(12, 2.6), + FlSpot(13, 3.9), + ], + ); + + LineChartBarData get lineChartBarData1_3 => LineChartBarData( + isCurved: true, + color: AppColors.contentColorCyan, + barWidth: 8, + isStrokeCapRound: true, + dotData: const FlDotData(show: false), + belowBarData: BarAreaData(show: false), + spots: const [ + FlSpot(1, 2.8), + FlSpot(3, 1.9), + FlSpot(6, 3), + FlSpot(10, 1.3), + FlSpot(13, 2.5), + ], + ); + + LineChartBarData get lineChartBarData2_1 => LineChartBarData( + isCurved: true, + curveSmoothness: 0, + color: AppColors.contentColorGreen.withValues(alpha: 0.5), + barWidth: 4, + isStrokeCapRound: true, + dotData: const FlDotData(show: false), + belowBarData: BarAreaData(show: false), + spots: const [ + FlSpot(1, 1), + FlSpot(3, 4), + FlSpot(5, 1.8), + FlSpot(7, 5), + FlSpot(10, 2), + FlSpot(12, 2.2), + FlSpot(13, 1.8), + ], + ); + + LineChartBarData get lineChartBarData2_2 => LineChartBarData( + isCurved: true, + color: AppColors.contentColorPink.withValues(alpha: 0.5), + barWidth: 4, + isStrokeCapRound: true, + dotData: const FlDotData(show: false), + belowBarData: BarAreaData( + show: true, + color: AppColors.contentColorPink.withValues(alpha: 0.2), + ), + spots: const [ + FlSpot(1, 1), + FlSpot(3, 2.8), + FlSpot(7, 1.2), + FlSpot(10, 2.8), + FlSpot(12, 2.6), + FlSpot(13, 3.9), + ], + ); + + LineChartBarData get lineChartBarData2_3 => LineChartBarData( + isCurved: true, + curveSmoothness: 0, + color: AppColors.contentColorCyan.withValues(alpha: 0.5), + barWidth: 2, + isStrokeCapRound: true, + dotData: const FlDotData(show: true), + belowBarData: BarAreaData(show: false), + spots: const [ + FlSpot(1, 3.8), + FlSpot(3, 1.9), + FlSpot(6, 5), + FlSpot(10, 3.3), + FlSpot(13, 4.5), + ], + ); +} + +class LineChartSample1 extends StatefulWidget { + const LineChartSample1({super.key}); + + @override + State createState() => LineChartSample1State(); +} + +class LineChartSample1State extends State { + late bool isShowingMainData; + + @override + void initState() { + super.initState(); + isShowingMainData = true; + } + + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: 1.23, + child: Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox( + height: 37, + ), + const Text( + 'Monthly Sales', + style: TextStyle( + color: AppColors.primary, + fontSize: 32, + fontWeight: FontWeight.bold, + letterSpacing: 2, + ), + textAlign: TextAlign.center, + ), + const SizedBox( + height: 37, + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only(right: 16, left: 6), + child: _LineChart(isShowingMainData: isShowingMainData), + ), + ), + const SizedBox( + height: 10, + ), + ], + ), + IconButton( + icon: Icon( + Icons.refresh, + color: + Colors.white.withValues(alpha: isShowingMainData ? 1.0 : 0.5), + ), + onPressed: () { + setState(() { + isShowingMainData = !isShowingMainData; + }); + }, + ) + ], + ), + ); + } +} diff --git a/example/lib/presentation/samples/line/line_chart_sample10.dart b/example/lib/presentation/samples/line/line_chart_sample10.dart new file mode 100644 index 0000000..5b1a28e --- /dev/null +++ b/example/lib/presentation/samples/line/line_chart_sample10.dart @@ -0,0 +1,146 @@ +import 'dart:async'; +import 'dart:math' as math; + +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart_app/presentation/resources/app_resources.dart'; +import 'package:flutter/material.dart'; + +class LineChartSample10 extends StatefulWidget { + const LineChartSample10({super.key}); + + final Color sinColor = AppColors.contentColorBlue; + final Color cosColor = AppColors.contentColorPink; + + @override + State createState() => _LineChartSample10State(); +} + +class _LineChartSample10State extends State { + final limitCount = 100; + final sinPoints = []; + final cosPoints = []; + + double xValue = 0; + double step = 0.05; + + late Timer timer; + + @override + void initState() { + super.initState(); + timer = Timer.periodic(const Duration(milliseconds: 40), (timer) { + while (sinPoints.length > limitCount) { + sinPoints.removeAt(0); + cosPoints.removeAt(0); + } + setState(() { + sinPoints.add(FlSpot(xValue, math.sin(xValue))); + cosPoints.add(FlSpot(xValue, math.cos(xValue))); + }); + xValue += step; + }); + } + + @override + Widget build(BuildContext context) { + return cosPoints.isNotEmpty + ? Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 12), + Text( + 'x: ${xValue.toStringAsFixed(1)}', + style: const TextStyle( + color: AppColors.mainTextColor2, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + Text( + 'sin: ${sinPoints.last.y.toStringAsFixed(1)}', + style: TextStyle( + color: widget.sinColor, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + Text( + 'cos: ${cosPoints.last.y.toStringAsFixed(1)}', + style: TextStyle( + color: widget.cosColor, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox( + height: 12, + ), + AspectRatio( + aspectRatio: 1.5, + child: Padding( + padding: const EdgeInsets.only(bottom: 24.0), + child: LineChart( + LineChartData( + minY: -1, + maxY: 1, + minX: sinPoints.first.x, + maxX: sinPoints.last.x, + lineTouchData: const LineTouchData(enabled: false), + clipData: const FlClipData.all(), + gridData: const FlGridData( + show: true, + drawVerticalLine: false, + ), + borderData: FlBorderData(show: false), + lineBarsData: [ + sinLine(sinPoints), + cosLine(cosPoints), + ], + titlesData: const FlTitlesData( + show: false, + ), + ), + ), + ), + ) + ], + ) + : Container(); + } + + LineChartBarData sinLine(List points) { + return LineChartBarData( + spots: points, + dotData: const FlDotData( + show: false, + ), + gradient: LinearGradient( + colors: [widget.sinColor.withValues(alpha: 0), widget.sinColor], + stops: const [0.1, 1.0], + ), + barWidth: 4, + isCurved: false, + ); + } + + LineChartBarData cosLine(List points) { + return LineChartBarData( + spots: points, + dotData: const FlDotData( + show: false, + ), + gradient: LinearGradient( + colors: [widget.cosColor.withValues(alpha: 0), widget.cosColor], + stops: const [0.1, 1.0], + ), + barWidth: 4, + isCurved: false, + ); + } + + @override + void dispose() { + timer.cancel(); + super.dispose(); + } +} diff --git a/example/lib/presentation/samples/line/line_chart_sample11.dart b/example/lib/presentation/samples/line/line_chart_sample11.dart new file mode 100644 index 0000000..303ddf2 --- /dev/null +++ b/example/lib/presentation/samples/line/line_chart_sample11.dart @@ -0,0 +1,202 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; + +class LineChartSample11 extends StatefulWidget { + const LineChartSample11({super.key}); + + @override + State createState() => _LineChartSample11State(); +} + +class _LineChartSample11State extends State { + var baselineX = 0.0; + var baselineY = 0.0; + + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: 1.5, + child: Padding( + padding: const EdgeInsets.only( + top: 18.0, + right: 18.0, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Row( + children: [ + RotatedBox( + quarterTurns: 1, + child: Slider( + value: baselineY, + onChanged: (newValue) { + setState(() { + baselineY = newValue; + }); + }, + min: -10, + max: 10, + ), + ), + Expanded( + child: _Chart( + baselineX, + (20 - (baselineY + 10)) - 10, + ), + ) + ], + ), + ), + Slider( + value: baselineX, + onChanged: (newValue) { + setState(() { + baselineX = newValue; + }); + }, + min: -10, + max: 10, + ), + ], + ), + ), + ); + } +} + +class _Chart extends StatelessWidget { + final double baselineX; + final double baselineY; + + const _Chart(this.baselineX, this.baselineY) : super(); + + Widget getHorizontalTitles(value, TitleMeta meta) { + TextStyle style; + if ((value - baselineX).abs() <= 0.1) { + style = const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ); + } else { + style = const TextStyle( + color: Colors.white60, + fontSize: 14, + ); + } + return Padding( + padding: const EdgeInsets.all(4.0), + child: Text(meta.formattedValue, style: style), + ); + } + + Widget getVerticalTitles(value, TitleMeta meta) { + TextStyle style; + if ((value - baselineY).abs() <= 0.1) { + style = const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ); + } else { + style = const TextStyle( + color: Colors.white60, + fontSize: 14, + ); + } + + return Padding( + padding: const EdgeInsets.all(4.0), + child: Text(meta.formattedValue, style: style), + ); + } + + FlLine getHorizontalVerticalLine(double value) { + if ((value - baselineY).abs() <= 0.1) { + return const FlLine( + color: Colors.white70, + strokeWidth: 1, + dashArray: [8, 4], + ); + } else { + return const FlLine( + color: Colors.blueGrey, + strokeWidth: 0.4, + dashArray: [8, 4], + ); + } + } + + FlLine getVerticalVerticalLine(double value) { + if ((value - baselineX).abs() <= 0.1) { + return const FlLine( + color: Colors.white70, + strokeWidth: 1, + dashArray: [8, 4], + ); + } else { + return const FlLine( + color: Colors.blueGrey, + strokeWidth: 0.4, + dashArray: [8, 4], + ); + } + } + + @override + Widget build(BuildContext context) { + return LineChart( + LineChartData( + lineBarsData: [ + LineChartBarData( + spots: [], + ), + ], + titlesData: FlTitlesData( + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: getVerticalTitles, + reservedSize: 36, + ), + ), + topTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: getHorizontalTitles, + reservedSize: 32), + ), + rightTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: getVerticalTitles, + reservedSize: 36, + ), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: getHorizontalTitles, + reservedSize: 32), + ), + ), + gridData: FlGridData( + show: true, + drawHorizontalLine: true, + drawVerticalLine: true, + getDrawingHorizontalLine: getHorizontalVerticalLine, + getDrawingVerticalLine: getVerticalVerticalLine, + ), + minY: -10, + maxY: 10, + baselineY: baselineY, + minX: -10, + maxX: 10, + baselineX: baselineX, + ), + duration: Duration.zero, + ); + } +} diff --git a/example/lib/presentation/samples/line/line_chart_sample12.dart b/example/lib/presentation/samples/line/line_chart_sample12.dart new file mode 100644 index 0000000..a56ec1c --- /dev/null +++ b/example/lib/presentation/samples/line/line_chart_sample12.dart @@ -0,0 +1,416 @@ +import 'dart:convert'; + +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart_app/presentation/presentation_utils.dart'; +import 'package:fl_chart_app/presentation/resources/app_colors.dart'; +import 'package:fl_chart_app/util/extensions/color_extensions.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class LineChartSample12 extends StatefulWidget { + const LineChartSample12({super.key}); + + @override + State createState() => _LineChartSample12State(); +} + +class _LineChartSample12State extends State { + List<(DateTime, double)>? _bitcoinPriceHistory; + late TransformationController _transformationController; + bool _isPanEnabled = true; + bool _isScaleEnabled = true; + + @override + void initState() { + _reloadData(); + _transformationController = TransformationController(); + super.initState(); + } + + void _reloadData() async { + final dataStr = await rootBundle.loadString( + 'assets/data/btc_last_year_price.json', + ); + if (!mounted) { + return; + } + final json = jsonDecode(dataStr) as Map; + setState(() { + _bitcoinPriceHistory = (json['prices'] as List).map((item) { + final timestamp = item[0] as int; + final price = item[1] as double; + return (DateTime.fromMillisecondsSinceEpoch(timestamp), price); + }).toList(); + }); + } + + @override + Widget build(BuildContext context) { + const leftReservedSize = 52.0; + return Column( + spacing: 16, + children: [ + LayoutBuilder( + builder: (context, constraints) { + final width = constraints.maxWidth; + return width >= 380 + ? Row( + children: [ + const SizedBox(width: leftReservedSize), + const _ChartTitle(), + const Spacer(), + Center( + child: _TransformationButtons( + controller: _transformationController, + ), + ), + ], + ) + : Column( + children: [ + const _ChartTitle(), + const SizedBox(height: 16), + _TransformationButtons( + controller: _transformationController, + ), + ], + ); + }, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + spacing: 16, + children: [ + const Text('Pan'), + Switch( + value: _isPanEnabled, + onChanged: (value) { + setState(() { + _isPanEnabled = value; + }); + }, + ), + const Text('Scale'), + Switch( + value: _isScaleEnabled, + onChanged: (value) { + setState(() { + _isScaleEnabled = value; + }); + }, + ), + ], + ), + ), + AspectRatio( + aspectRatio: 1.4, + child: Padding( + padding: const EdgeInsets.only( + top: 0.0, + right: 18.0, + ), + child: LineChart( + transformationConfig: FlTransformationConfig( + scaleAxis: FlScaleAxis.horizontal, + minScale: 1.0, + maxScale: 25.0, + panEnabled: _isPanEnabled, + scaleEnabled: _isScaleEnabled, + transformationController: _transformationController, + ), + LineChartData( + lineBarsData: [ + LineChartBarData( + spots: _bitcoinPriceHistory?.asMap().entries.map((e) { + final index = e.key; + final item = e.value; + final value = item.$2; + return FlSpot(index.toDouble(), value); + }).toList() ?? + [], + dotData: const FlDotData(show: false), + color: AppColors.contentColorYellow, + barWidth: 1, + shadow: const Shadow( + color: AppColors.contentColorYellow, + blurRadius: 2, + ), + belowBarData: BarAreaData( + show: true, + gradient: LinearGradient( + colors: [ + AppColors.contentColorYellow.withValues(alpha: 0.2), + AppColors.contentColorYellow.withValues(alpha: 0.0), + ], + stops: const [0.5, 1.0], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + ), + ], + lineTouchData: LineTouchData( + touchSpotThreshold: 5, + getTouchLineStart: (_, __) => -double.infinity, + getTouchLineEnd: (_, __) => double.infinity, + getTouchedSpotIndicator: + (LineChartBarData barData, List spotIndexes) { + return spotIndexes.map((spotIndex) { + return TouchedSpotIndicatorData( + const FlLine( + color: AppColors.contentColorRed, + strokeWidth: 1.5, + dashArray: [8, 2], + ), + FlDotData( + show: true, + getDotPainter: (spot, percent, barData, index) { + return FlDotCirclePainter( + radius: 6, + color: AppColors.contentColorYellow, + strokeWidth: 0, + strokeColor: AppColors.contentColorYellow, + ); + }, + ), + ); + }).toList(); + }, + touchTooltipData: LineTouchTooltipData( + getTooltipItems: (List touchedBarSpots) { + return touchedBarSpots.map((barSpot) { + final price = barSpot.y; + final date = + _bitcoinPriceHistory![barSpot.x.toInt()].$1; + return LineTooltipItem( + '', + const TextStyle( + color: AppColors.contentColorBlack, + fontWeight: FontWeight.bold, + ), + children: [ + TextSpan( + text: '${date.year}/${date.month}/${date.day}', + style: TextStyle( + color: AppColors.contentColorGreen.darken(20), + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + TextSpan( + text: '\n${AppUtils.getFormattedCurrency( + context, + price, + noDecimals: true, + )}', + style: const TextStyle( + color: AppColors.contentColorYellow, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ], + ); + }).toList(); + }, + getTooltipColor: (LineBarSpot barSpot) => + AppColors.contentColorBlack, + ), + ), + titlesData: FlTitlesData( + show: true, + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + leftTitles: const AxisTitles( + drawBelowEverything: true, + sideTitles: SideTitles( + showTitles: true, + reservedSize: leftReservedSize, + maxIncluded: false, + minIncluded: false, + ), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 38, + maxIncluded: false, + getTitlesWidget: (double value, TitleMeta meta) { + final date = _bitcoinPriceHistory![value.toInt()].$1; + return SideTitleWidget( + meta: meta, + child: Transform.rotate( + angle: -45 * 3.14 / 180, + child: Text( + '${date.month}/${date.day}', + style: const TextStyle( + color: AppColors.contentColorGreen, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + ); + }, + ), + ), + ), + ), + duration: Duration.zero, + ), + ), + ), + ], + ); + } + + @override + void dispose() { + _transformationController.dispose(); + super.dispose(); + } +} + +class _ChartTitle extends StatelessWidget { + const _ChartTitle(); + + @override + Widget build(BuildContext context) { + return const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 14), + Text( + 'Bitcoin Price History', + style: TextStyle( + color: AppColors.contentColorYellow, + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + Text( + '2023/12/19 - 2024/12/17', + style: TextStyle( + color: AppColors.contentColorGreen, + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + SizedBox(height: 14), + ], + ); + } +} + +class _TransformationButtons extends StatelessWidget { + const _TransformationButtons({ + required this.controller, + }); + + final TransformationController controller; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Tooltip( + message: 'Zoom in', + child: IconButton( + icon: const Icon( + Icons.add, + size: 16, + ), + onPressed: _transformationZoomIn, + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Tooltip( + message: 'Move left', + child: IconButton( + icon: const Icon( + Icons.arrow_back_ios, + size: 16, + ), + onPressed: _transformationMoveLeft, + ), + ), + Tooltip( + message: 'Reset zoom', + child: IconButton( + icon: const Icon( + Icons.refresh, + size: 16, + ), + onPressed: _transformationReset, + ), + ), + Tooltip( + message: 'Move right', + child: IconButton( + icon: const Icon( + Icons.arrow_forward_ios, + size: 16, + ), + onPressed: _transformationMoveRight, + ), + ), + ], + ), + Tooltip( + message: 'Zoom out', + child: IconButton( + icon: const Icon( + Icons.minimize, + size: 16, + ), + onPressed: _transformationZoomOut, + ), + ), + ], + ); + } + + void _transformationReset() { + controller.value = Matrix4.identity(); + } + + void _transformationZoomIn() { + controller.value *= Matrix4.diagonal3Values( + 1.1, + 1.1, + 1, + ); + } + + void _transformationMoveLeft() { + controller.value *= Matrix4.translationValues( + 20, + 0, + 0, + ); + } + + void _transformationMoveRight() { + controller.value *= Matrix4.translationValues( + -20, + 0, + 0, + ); + } + + void _transformationZoomOut() { + controller.value *= Matrix4.diagonal3Values( + 0.9, + 0.9, + 1, + ); + } +} diff --git a/example/lib/presentation/samples/line/line_chart_sample13.dart b/example/lib/presentation/samples/line/line_chart_sample13.dart new file mode 100644 index 0000000..0b4e5f6 --- /dev/null +++ b/example/lib/presentation/samples/line/line_chart_sample13.dart @@ -0,0 +1,494 @@ +import 'package:equatable/equatable.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart_app/presentation/resources/app_resources.dart'; +import 'package:fl_chart_app/util/app_utils.dart'; +import 'package:fl_chart_app/util/csv_parser.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class LineChartSample13 extends StatefulWidget { + const LineChartSample13({super.key}); + + @override + State createState() => _LineChartSample13State(); +} + +class _LineChartSample13State extends State { + List>? monthlyWeatherData; + int _currentMonthIndex = 0; + late final List monthsNames; + + final int minDays = 1; + final int maxDays = 31; + late final double overallMinTemp; + late final double overallMaxTemp; + + int _interactedSpotIndex = -1; + + @override + void initState() { + monthsNames = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', + ]; + _loadWeatherData(); + super.initState(); + } + + void _loadWeatherData() async { + final data = + await rootBundle.loadString('assets/data/amsterdam_2024_weather.csv'); + final rows = CsvParser.parse(data); + if (!mounted) { + return; + } + setState(() { + final allWeatherData = + rows.skip(1).map((row) => _WeatherData.fromCsv(row)).toList(); + monthlyWeatherData = List.generate(12, (index) { + final month = index + 1; + return allWeatherData + .where((element) => element.datetime.month == month) + .toList(); + }); + overallMinTemp = allWeatherData + .map((e) => e.temp) + .reduce((value, element) => value < element ? value : element); + overallMaxTemp = allWeatherData + .map((e) => e.temp) + .reduce((value, element) => value > element ? value : element); + }); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const SizedBox(height: 18), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Amsterdam Weather 2024', + style: TextStyle( + color: AppColors.contentColorOrange, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + Tooltip( + message: 'Source: visualcrossing.com', + child: IconButton( + onPressed: () { + AppUtils().tryToLaunchUrl( + 'https://www.visualcrossing.com/weather-history/Amsterdam,Netherlands/metric/2024-01-01/2024-12-31', + ); + }, + icon: const Icon( + Icons.info_outline_rounded, + color: AppColors.contentColorOrange, + size: 18, + )), + ) + ], + ), + const SizedBox(height: 18), + Row( + children: [ + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: IconButton( + onPressed: _canGoPrevious ? _previousMonth : null, + icon: const Icon(Icons.navigate_before_rounded), + ), + ), + ), + SizedBox( + width: 92, + child: Text( + monthsNames[_currentMonthIndex], + textAlign: TextAlign.center, + style: const TextStyle( + color: AppColors.contentColorBlue, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: IconButton( + onPressed: _canGoNext ? _nextMonth : null, + icon: const Icon(Icons.navigate_next_rounded), + ), + ), + ), + ], + ), + const SizedBox(height: 18), + AspectRatio( + aspectRatio: 1.4, + child: Stack( + children: [ + if (monthlyWeatherData != null) + Padding( + padding: const EdgeInsets.only( + top: 0.0, + right: 18.0, + ), + child: LineChart( + LineChartData( + minY: overallMinTemp - 5, + maxY: overallMaxTemp + 5, + minX: 0, + maxX: 31, + lineBarsData: [ + LineChartBarData( + spots: monthlyWeatherData![_currentMonthIndex] + .asMap() + .entries + .map((e) { + final index = e.key; + final item = e.value; + final value = item.temp; + return FlSpot( + index.toDouble(), + value, + yError: FlErrorRange( + lowerBy: (item.tempmin - value).abs(), + upperBy: item.tempmax - value, + ), + ); + }).toList(), + isCurved: false, + dotData: const FlDotData(show: false), + color: AppColors.contentColorBlue, + barWidth: 1, + errorIndicatorData: FlErrorIndicatorData( + show: true, + painter: _errorPainter, + ), + ), + ], + gridData: FlGridData( + show: true, + drawHorizontalLine: true, + drawVerticalLine: false, + horizontalInterval: 5, + getDrawingHorizontalLine: _horizontalGridLines, + ), + titlesData: FlTitlesData( + show: true, + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + leftTitles: AxisTitles( + drawBelowEverything: true, + sideTitles: SideTitles( + showTitles: true, + maxIncluded: false, + minIncluded: false, + reservedSize: 40, + getTitlesWidget: (double value, TitleMeta meta) { + return SideTitleWidget( + meta: meta, + child: Text( + '${meta.formattedValue}°', + ), + ); + }, + ), + ), + bottomTitles: AxisTitles( + axisNameWidget: Container( + margin: const EdgeInsets.only(bottom: 20), + child: const Text( + 'Day of month', + style: TextStyle( + color: AppColors.contentColorGreen, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ), + axisNameSize: 40, + sideTitles: SideTitles( + showTitles: true, + reservedSize: 38, + maxIncluded: false, + interval: 1, + getTitlesWidget: _bottomTitles, + ), + ), + ), + lineTouchData: LineTouchData( + enabled: true, + handleBuiltInTouches: false, + touchCallback: _touchCallback, + ), + ), + ), + ), + if (monthlyWeatherData == null) + const Center( + child: CircularProgressIndicator(), + ) + ], + ), + ), + ], + ); + } + + bool get _canGoNext => _currentMonthIndex < 11; + + bool get _canGoPrevious => _currentMonthIndex > 0; + + void _previousMonth() { + if (!_canGoPrevious) { + return; + } + + setState(() { + _currentMonthIndex--; + }); + } + + void _nextMonth() { + if (!_canGoNext) { + return; + } + setState(() { + _currentMonthIndex++; + }); + } + + FlSpotErrorRangePainter _errorPainter( + LineChartSpotErrorRangeCallbackInput input, + ) => + FlSimpleErrorPainter( + lineWidth: 1.0, + lineColor: _interactedSpotIndex == input.spotIndex + ? Colors.white + : Colors.white38, + showErrorTexts: _interactedSpotIndex == input.spotIndex, + ); + + FlLine _horizontalGridLines(double value) { + final isZero = value == 0.0; + return FlLine( + color: isZero ? Colors.white38 : Colors.blueGrey, + strokeWidth: isZero ? 0.8 : 0.4, + dashArray: isZero ? null : [8, 4], + ); + } + + Widget _bottomTitles(double value, TitleMeta meta) { + final day = value.toInt() + 1; + + final isDayHovered = _interactedSpotIndex == day - 1; + + final isImportantToShow = day % 5 == 0 || day == 1; + + if (!isImportantToShow && !isDayHovered) { + return const SizedBox(); + } + + return SideTitleWidget( + meta: meta, + child: Text( + day.toString(), + style: TextStyle( + color: isDayHovered + ? AppColors.contentColorWhite + : AppColors.contentColorGreen, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ); + } + + _touchCallback(FlTouchEvent event, LineTouchResponse? touchResponse) { + if (!event.isInterestedForInteractions || + touchResponse?.lineBarSpots == null || + touchResponse!.lineBarSpots!.isEmpty) { + setState(() { + _interactedSpotIndex = -1; + }); + return; + } + + setState(() { + _interactedSpotIndex = touchResponse.lineBarSpots!.first.spotIndex; + }); + } + + @override + void dispose() { + super.dispose(); + } +} + +class _WeatherData with EquatableMixin { + final String name; + final DateTime datetime; + final double tempmax; + final double tempmin; + final double temp; + final double feelslikemax; + final double feelslikemin; + final double feelslike; + final double dew; + final double humidity; + final double precip; + final double precipprob; + final double precipcover; + final String preciptype; + final double snow; + final double snowdepth; + final double windgust; + final double windspeed; + final double winddir; + final double sealevelpressure; + final double cloudcover; + final double visibility; + final double solarradiation; + final double solarenergy; + final double uvindex; + final double severerisk; + final DateTime sunrise; + final DateTime sunset; + final double moonphase; + final String conditions; + final String description; + final String icon; + final String stations; + + const _WeatherData({ + required this.name, + required this.datetime, + required this.tempmax, + required this.tempmin, + required this.temp, + required this.feelslikemax, + required this.feelslikemin, + required this.feelslike, + required this.dew, + required this.humidity, + required this.precip, + required this.precipprob, + required this.precipcover, + required this.preciptype, + required this.snow, + required this.snowdepth, + required this.windgust, + required this.windspeed, + required this.winddir, + required this.sealevelpressure, + required this.cloudcover, + required this.visibility, + required this.solarradiation, + required this.solarenergy, + required this.uvindex, + required this.severerisk, + required this.sunrise, + required this.sunset, + required this.moonphase, + required this.conditions, + required this.description, + required this.icon, + required this.stations, + }); + + // parse from csv row + // name,datetime,tempmax,tempmin,temp,feelslikemax,feelslikemin,feelslike,dew,humidity,precip,precipprob,precipcover,preciptype,snow,snowdepth,windgust,windspeed,winddir,sealevelpressure,cloudcover,visibility,solarradiation,solarenergy,uvindex,severerisk,sunrise,sunset,moonphase,conditions,description,icon,stations + // "Amsterdam,Netherlands",2024-01-01,9.1,6.4,8,5.3,2.5,4.1,5.1,82.4,14.26,100,37.5,rain,0,0,53.9,40.2,225.9,1000.1,88.7,20.5,20.6,1.8,2,,2024-01-01T08:50:34,2024-01-01T16:37:06,0.68,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999" + factory _WeatherData.fromCsv(List row) => _WeatherData( + name: row[0], + datetime: DateTime.parse(row[1]), + tempmax: double.parse(row[2]), + tempmin: double.parse(row[3]), + temp: double.parse(row[4]), + feelslikemax: double.parse(row[5]), + feelslikemin: double.parse(row[6]), + feelslike: double.parse(row[7]), + dew: double.parse(row[8]), + humidity: double.parse(row[9]), + precip: double.parse(row[10]), + precipprob: double.parse(row[11]), + precipcover: double.parse(row[12]), + preciptype: row[13], + snow: double.parse(row[14]), + snowdepth: double.parse(row[15]), + windgust: double.parse(row[16]), + windspeed: double.parse(row[17]), + winddir: double.parse(row[18]), + sealevelpressure: double.parse(row[19]), + cloudcover: double.parse(row[20]), + visibility: double.parse(row[21]), + solarradiation: double.parse(row[22]), + solarenergy: double.parse(row[23]), + uvindex: double.parse(row[24]), + severerisk: row[25].isEmpty ? 0 : double.parse(row[25]), + sunrise: DateTime.parse(row[26]), + sunset: DateTime.parse(row[27]), + moonphase: double.parse(row[28]), + conditions: row[29], + description: row[30], + icon: row[31], + stations: row[32], + ); + + @override + List get props => [ + name, + datetime, + tempmax, + tempmin, + temp, + feelslikemax, + feelslikemin, + feelslike, + dew, + humidity, + precip, + precipprob, + precipcover, + preciptype, + snow, + snowdepth, + windgust, + windspeed, + winddir, + sealevelpressure, + cloudcover, + visibility, + solarradiation, + solarenergy, + uvindex, + severerisk, + sunrise, + sunset, + moonphase, + conditions, + description, + icon, + stations, + ]; +} diff --git a/example/lib/presentation/samples/line/line_chart_sample2.dart b/example/lib/presentation/samples/line/line_chart_sample2.dart new file mode 100644 index 0000000..a22e2b3 --- /dev/null +++ b/example/lib/presentation/samples/line/line_chart_sample2.dart @@ -0,0 +1,294 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart_app/presentation/resources/app_resources.dart'; +import 'package:flutter/material.dart'; + +class LineChartSample2 extends StatefulWidget { + const LineChartSample2({super.key}); + + @override + State createState() => _LineChartSample2State(); +} + +class _LineChartSample2State extends State { + List gradientColors = [ + AppColors.contentColorCyan, + AppColors.contentColorBlue, + ]; + + bool showAvg = false; + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + AspectRatio( + aspectRatio: 1.70, + child: Padding( + padding: const EdgeInsets.only( + right: 18, + left: 12, + top: 24, + bottom: 12, + ), + child: LineChart( + showAvg ? avgData() : mainData(), + ), + ), + ), + SizedBox( + width: 60, + height: 34, + child: TextButton( + onPressed: () { + setState(() { + showAvg = !showAvg; + }); + }, + child: Text( + 'avg', + style: TextStyle( + fontSize: 12, + color: showAvg + ? Colors.white.withValues(alpha: 0.5) + : Colors.white, + ), + ), + ), + ), + ], + ); + } + + Widget bottomTitleWidgets(double value, TitleMeta meta) { + const style = TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ); + Widget text; + switch (value.toInt()) { + case 2: + text = const Text('MAR', style: style); + break; + case 5: + text = const Text('JUN', style: style); + break; + case 8: + text = const Text('SEP', style: style); + break; + default: + text = const Text('', style: style); + break; + } + + return SideTitleWidget( + meta: meta, + child: text, + ); + } + + Widget leftTitleWidgets(double value, TitleMeta meta) { + const style = TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15, + ); + String text; + switch (value.toInt()) { + case 1: + text = '10K'; + break; + case 3: + text = '30k'; + break; + case 5: + text = '50k'; + break; + default: + return Container(); + } + + return Text(text, style: style, textAlign: TextAlign.left); + } + + LineChartData mainData() { + return LineChartData( + gridData: FlGridData( + show: true, + drawVerticalLine: true, + horizontalInterval: 1, + verticalInterval: 1, + getDrawingHorizontalLine: (value) { + return const FlLine( + color: AppColors.mainGridLineColor, + strokeWidth: 1, + ); + }, + getDrawingVerticalLine: (value) { + return const FlLine( + color: AppColors.mainGridLineColor, + strokeWidth: 1, + ); + }, + ), + titlesData: FlTitlesData( + show: true, + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 30, + interval: 1, + getTitlesWidget: bottomTitleWidgets, + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + interval: 1, + getTitlesWidget: leftTitleWidgets, + reservedSize: 42, + ), + ), + ), + borderData: FlBorderData( + show: true, + border: Border.all(color: const Color(0xff37434d)), + ), + minX: 0, + maxX: 11, + minY: 0, + maxY: 6, + lineBarsData: [ + LineChartBarData( + spots: const [ + FlSpot(0, 3), + FlSpot(2.6, 2), + FlSpot(4.9, 5), + FlSpot(6.8, 3.1), + FlSpot(8, 4), + FlSpot(9.5, 3), + FlSpot(11, 4), + ], + isCurved: true, + gradient: LinearGradient( + colors: gradientColors, + ), + barWidth: 5, + isStrokeCapRound: true, + dotData: const FlDotData( + show: false, + ), + belowBarData: BarAreaData( + show: true, + gradient: LinearGradient( + colors: gradientColors + .map((color) => color.withValues(alpha: 0.3)) + .toList(), + ), + ), + ), + ], + ); + } + + LineChartData avgData() { + return LineChartData( + lineTouchData: const LineTouchData(enabled: false), + gridData: FlGridData( + show: true, + drawHorizontalLine: true, + verticalInterval: 1, + horizontalInterval: 1, + getDrawingVerticalLine: (value) { + return const FlLine( + color: Color(0xff37434d), + strokeWidth: 1, + ); + }, + getDrawingHorizontalLine: (value) { + return const FlLine( + color: Color(0xff37434d), + strokeWidth: 1, + ); + }, + ), + titlesData: FlTitlesData( + show: true, + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 30, + getTitlesWidget: bottomTitleWidgets, + interval: 1, + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: leftTitleWidgets, + reservedSize: 42, + interval: 1, + ), + ), + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + ), + borderData: FlBorderData( + show: true, + border: Border.all(color: const Color(0xff37434d)), + ), + minX: 0, + maxX: 11, + minY: 0, + maxY: 6, + lineBarsData: [ + LineChartBarData( + spots: const [ + FlSpot(0, 3.44), + FlSpot(2.6, 3.44), + FlSpot(4.9, 3.44), + FlSpot(6.8, 3.44), + FlSpot(8, 3.44), + FlSpot(9.5, 3.44), + FlSpot(11, 3.44), + ], + isCurved: true, + gradient: LinearGradient( + colors: [ + ColorTween(begin: gradientColors[0], end: gradientColors[1]) + .lerp(0.2)!, + ColorTween(begin: gradientColors[0], end: gradientColors[1]) + .lerp(0.2)!, + ], + ), + barWidth: 5, + isStrokeCapRound: true, + dotData: const FlDotData( + show: false, + ), + belowBarData: BarAreaData( + show: true, + gradient: LinearGradient( + colors: [ + ColorTween(begin: gradientColors[0], end: gradientColors[1]) + .lerp(0.2)! + .withValues(alpha: 0.1), + ColorTween(begin: gradientColors[0], end: gradientColors[1]) + .lerp(0.2)! + .withValues(alpha: 0.1), + ], + ), + ), + ), + ], + ); + } +} diff --git a/example/lib/presentation/samples/line/line_chart_sample3.dart b/example/lib/presentation/samples/line/line_chart_sample3.dart new file mode 100644 index 0000000..f79dbf9 --- /dev/null +++ b/example/lib/presentation/samples/line/line_chart_sample3.dart @@ -0,0 +1,451 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart_app/presentation/resources/app_resources.dart'; +import 'package:flutter/material.dart'; + +class LineChartSample3 extends StatefulWidget { + LineChartSample3({ + super.key, + Color? lineColor, + Color? indicatorLineColor, + Color? indicatorTouchedLineColor, + Color? indicatorSpotStrokeColor, + Color? indicatorTouchedSpotStrokeColor, + Color? bottomTextColor, + Color? bottomTouchedTextColor, + Color? averageLineColor, + Color? tooltipBgColor, + Color? tooltipTextColor, + }) : lineColor = lineColor ?? AppColors.contentColorRed, + indicatorLineColor = indicatorLineColor ?? + AppColors.contentColorYellow.withValues(alpha: 0.2), + indicatorTouchedLineColor = + indicatorTouchedLineColor ?? AppColors.contentColorYellow, + indicatorSpotStrokeColor = indicatorSpotStrokeColor ?? + AppColors.contentColorYellow.withValues(alpha: 0.5), + indicatorTouchedSpotStrokeColor = + indicatorTouchedSpotStrokeColor ?? AppColors.contentColorYellow, + bottomTextColor = bottomTextColor ?? + AppColors.contentColorYellow.withValues(alpha: 0.2), + bottomTouchedTextColor = + bottomTouchedTextColor ?? AppColors.contentColorYellow, + averageLineColor = averageLineColor ?? + AppColors.contentColorGreen.withValues(alpha: 0.8), + tooltipBgColor = tooltipBgColor ?? AppColors.contentColorGreen, + tooltipTextColor = tooltipTextColor ?? Colors.black; + + final Color lineColor; + final Color indicatorLineColor; + final Color indicatorTouchedLineColor; + final Color indicatorSpotStrokeColor; + final Color indicatorTouchedSpotStrokeColor; + final Color bottomTextColor; + final Color bottomTouchedTextColor; + final Color averageLineColor; + final Color tooltipBgColor; + final Color tooltipTextColor; + + List get weekDays => + const ['Sat', 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri']; + + List get yValues => const [1.3, 1, 1.8, 1.5, 2.2, 1.8, 3]; + + @override + State createState() => _LineChartSample3State(); +} + +class _LineChartSample3State extends State { + late double touchedValue; + + bool fitInsideBottomTitle = true; + bool fitInsideLeftTitle = false; + + @override + void initState() { + touchedValue = -1; + super.initState(); + } + + Widget leftTitleWidgets(double value, TitleMeta meta) { + if (value % 1 != 0) { + return Container(); + } + final style = TextStyle( + color: AppColors.mainTextColor1.withValues(alpha: 0.5), + fontSize: 10, + ); + String text; + switch (value.toInt()) { + case 0: + text = ''; + break; + case 1: + text = '1k calories'; + break; + case 2: + text = '2k calories'; + break; + case 3: + text = '3k calories'; + break; + default: + return Container(); + } + + return SideTitleWidget( + meta: meta, + space: 6, + fitInside: fitInsideLeftTitle + ? SideTitleFitInsideData.fromTitleMeta(meta) + : SideTitleFitInsideData.disable(), + child: Text(text, style: style, textAlign: TextAlign.center), + ); + } + + Widget bottomTitleWidgets(double value, TitleMeta meta) { + final isTouched = value == touchedValue; + final style = TextStyle( + color: isTouched ? widget.bottomTouchedTextColor : widget.bottomTextColor, + fontWeight: FontWeight.bold, + ); + + if (value % 1 != 0) { + return Container(); + } + return SideTitleWidget( + space: 4, + meta: meta, + fitInside: fitInsideBottomTitle + ? SideTitleFitInsideData.fromTitleMeta(meta, distanceFromEdge: 0) + : SideTitleFitInsideData.disable(), + child: Text( + widget.weekDays[value.toInt()], + style: style, + ), + ); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 10), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Average Line', + style: TextStyle( + color: widget.averageLineColor.withValues(alpha: 1), + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + const Text( + ' and ', + style: TextStyle( + color: AppColors.mainTextColor1, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + Text( + 'Indicators', + style: TextStyle( + color: widget.indicatorLineColor.withValues(alpha: 1), + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ], + ), + const SizedBox( + height: 18, + ), + AspectRatio( + aspectRatio: 2, + child: Padding( + padding: const EdgeInsets.only(right: 20.0, left: 12), + child: LineChart( + LineChartData( + lineTouchData: LineTouchData( + getTouchedSpotIndicator: + (LineChartBarData barData, List spotIndexes) { + return spotIndexes.map((spotIndex) { + final spot = barData.spots[spotIndex]; + if (spot.x == 0 || spot.x == 6) { + return null; + } + return TouchedSpotIndicatorData( + FlLine( + color: widget.indicatorTouchedLineColor, + strokeWidth: 4, + ), + FlDotData( + getDotPainter: (spot, percent, barData, index) { + if (index.isEven) { + return FlDotCirclePainter( + radius: 8, + color: Colors.white, + strokeWidth: 5, + strokeColor: + widget.indicatorTouchedSpotStrokeColor, + ); + } else { + return FlDotSquarePainter( + size: 16, + color: Colors.white, + strokeWidth: 5, + strokeColor: + widget.indicatorTouchedSpotStrokeColor, + ); + } + }, + ), + ); + }).toList(); + }, + touchTooltipData: LineTouchTooltipData( + getTooltipColor: (touchedSpot) => widget.tooltipBgColor, + getTooltipItems: (List touchedBarSpots) { + return touchedBarSpots.map((barSpot) { + final flSpot = barSpot; + if (flSpot.x == 0 || flSpot.x == 6) { + return null; + } + + TextAlign textAlign; + switch (flSpot.x.toInt()) { + case 1: + textAlign = TextAlign.left; + break; + case 5: + textAlign = TextAlign.right; + break; + default: + textAlign = TextAlign.center; + } + + return LineTooltipItem( + '${widget.weekDays[flSpot.x.toInt()]} \n', + TextStyle( + color: widget.tooltipTextColor, + fontWeight: FontWeight.bold, + ), + children: [ + TextSpan( + text: flSpot.y.toString(), + style: TextStyle( + color: widget.tooltipTextColor, + fontWeight: FontWeight.w900, + ), + ), + const TextSpan( + text: ' k ', + style: TextStyle( + fontStyle: FontStyle.italic, + fontWeight: FontWeight.w900, + ), + ), + const TextSpan( + text: 'calories', + style: TextStyle( + fontWeight: FontWeight.normal, + ), + ), + ], + textAlign: textAlign, + ); + }).toList(); + }, + ), + touchCallback: + (FlTouchEvent event, LineTouchResponse? lineTouch) { + if (!event.isInterestedForInteractions || + lineTouch == null || + lineTouch.lineBarSpots == null) { + setState(() { + touchedValue = -1; + }); + return; + } + final value = lineTouch.lineBarSpots![0].x; + + if (value == 0 || value == 6) { + setState(() { + touchedValue = -1; + }); + return; + } + + setState(() { + touchedValue = value; + }); + }, + ), + extraLinesData: ExtraLinesData( + horizontalLines: [ + HorizontalLine( + y: 1.8, + color: widget.averageLineColor, + strokeWidth: 3, + dashArray: [20, 10], + ), + ], + ), + lineBarsData: [ + LineChartBarData( + isStepLineChart: true, + spots: widget.yValues.asMap().entries.map((e) { + return FlSpot(e.key.toDouble(), e.value); + }).toList(), + isCurved: false, + barWidth: 4, + color: widget.lineColor, + belowBarData: BarAreaData( + show: true, + gradient: LinearGradient( + colors: [ + widget.lineColor.withValues(alpha: 0.5), + widget.lineColor.withValues(alpha: 0), + ], + stops: const [0.5, 1.0], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + spotsLine: BarAreaSpotsLine( + show: true, + flLineStyle: FlLine( + color: widget.indicatorLineColor, + strokeWidth: 2, + ), + checkToShowSpotLine: (spot) { + if (spot.x == 0 || spot.x == 6) { + return false; + } + + return true; + }, + ), + ), + dotData: FlDotData( + show: true, + getDotPainter: (spot, percent, barData, index) { + if (index.isEven) { + return FlDotCirclePainter( + radius: 6, + color: Colors.white, + strokeWidth: 3, + strokeColor: widget.indicatorSpotStrokeColor, + ); + } else { + return FlDotSquarePainter( + size: 12, + color: Colors.white, + strokeWidth: 3, + strokeColor: widget.indicatorSpotStrokeColor, + ); + } + }, + checkToShowDot: (spot, barData) { + return spot.x != 0 && spot.x != 6; + }, + ), + ), + ], + minY: 0, + borderData: FlBorderData( + show: true, + border: Border.all( + color: AppColors.borderColor, + ), + ), + gridData: FlGridData( + show: true, + drawHorizontalLine: true, + drawVerticalLine: true, + checkToShowHorizontalLine: (value) => value % 1 == 0, + checkToShowVerticalLine: (value) => value % 1 == 0, + getDrawingHorizontalLine: (value) { + if (value == 0) { + return const FlLine( + color: AppColors.contentColorOrange, + strokeWidth: 2, + ); + } else { + return const FlLine( + color: AppColors.mainGridLineColor, + strokeWidth: 0.5, + ); + } + }, + getDrawingVerticalLine: (value) { + if (value == 0) { + return const FlLine( + color: Colors.redAccent, + strokeWidth: 10, + ); + } else { + return const FlLine( + color: AppColors.mainGridLineColor, + strokeWidth: 0.5, + ); + } + }, + ), + titlesData: FlTitlesData( + show: true, + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 46, + getTitlesWidget: leftTitleWidgets, + ), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 40, + getTitlesWidget: bottomTitleWidgets, + ), + ), + ), + ), + ), + ), + ), + Column( + children: [ + const Text('Fit Inside Title Option'), + const Divider(), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Left Title'), + Switch( + value: fitInsideLeftTitle, + onChanged: (value) => setState(() { + fitInsideLeftTitle = value; + }), + ), + const Text('Bottom Title'), + Switch( + value: fitInsideBottomTitle, + onChanged: (value) => setState(() { + fitInsideBottomTitle = value; + }), + ) + ], + ), + ], + ), + ], + ); + } +} diff --git a/example/lib/presentation/samples/line/line_chart_sample4.dart b/example/lib/presentation/samples/line/line_chart_sample4.dart new file mode 100644 index 0000000..b03ddd5 --- /dev/null +++ b/example/lib/presentation/samples/line/line_chart_sample4.dart @@ -0,0 +1,202 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart_app/presentation/resources/app_resources.dart'; +import 'package:flutter/material.dart'; + +class LineChartSample4 extends StatelessWidget { + LineChartSample4({ + super.key, + Color? mainLineColor, + Color? belowLineColor, + Color? aboveLineColor, + }) : mainLineColor = + mainLineColor ?? AppColors.contentColorYellow.withValues(alpha: 1), + belowLineColor = + belowLineColor ?? AppColors.contentColorPink.withValues(alpha: 1), + aboveLineColor = aboveLineColor ?? + AppColors.contentColorPurple.withValues(alpha: 0.7); + + final Color mainLineColor; + final Color belowLineColor; + final Color aboveLineColor; + + Widget bottomTitleWidgets(double value, TitleMeta meta) { + String text; + switch (value.toInt()) { + case 0: + text = 'Jan'; + break; + case 1: + text = 'Feb'; + break; + case 2: + text = 'Mar'; + break; + case 3: + text = 'Apr'; + break; + case 4: + text = 'May'; + break; + case 5: + text = 'Jun'; + break; + case 6: + text = 'Jul'; + break; + case 7: + text = 'Aug'; + break; + case 8: + text = 'Sep'; + break; + case 9: + text = 'Oct'; + break; + case 10: + text = 'Nov'; + break; + case 11: + text = 'Dec'; + break; + default: + return Container(); + } + + return SideTitleWidget( + meta: meta, + space: 4, + child: Text( + text, + style: TextStyle( + fontSize: 10, + color: mainLineColor, + fontWeight: FontWeight.bold, + ), + ), + ); + } + + Widget leftTitleWidgets(double value, TitleMeta meta) { + const style = TextStyle( + color: AppColors.mainTextColor3, + fontSize: 12, + ); + return SideTitleWidget( + meta: meta, + child: Text('\$ ${value + 0.5}', style: style), + ); + } + + @override + Widget build(BuildContext context) { + const cutOffYValue = 5.0; + + return AspectRatio( + aspectRatio: 2, + child: Padding( + padding: const EdgeInsets.only( + left: 12, + right: 28, + top: 22, + bottom: 12, + ), + child: LineChart( + LineChartData( + lineTouchData: const LineTouchData(enabled: false), + lineBarsData: [ + LineChartBarData( + spots: const [ + FlSpot(0, 4), + FlSpot(1, 3.5), + FlSpot(2, 4.5), + FlSpot(3, 1), + FlSpot(4, 4), + FlSpot(5, 6), + FlSpot(6, 6.5), + FlSpot(7, 6), + FlSpot(8, 4), + FlSpot(9, 6), + FlSpot(10, 6), + FlSpot(11, 7), + ], + isCurved: true, + barWidth: 8, + color: mainLineColor, + belowBarData: BarAreaData( + show: true, + color: belowLineColor, + cutOffY: cutOffYValue, + applyCutOffY: true, + ), + aboveBarData: BarAreaData( + show: true, + color: aboveLineColor, + cutOffY: cutOffYValue, + applyCutOffY: true, + ), + dotData: const FlDotData( + show: false, + ), + ), + ], + minY: 0, + titlesData: FlTitlesData( + show: true, + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + bottomTitles: AxisTitles( + axisNameWidget: Text( + '2019', + style: TextStyle( + fontSize: 10, + color: mainLineColor, + fontWeight: FontWeight.bold, + ), + ), + sideTitles: SideTitles( + showTitles: true, + reservedSize: 18, + interval: 1, + getTitlesWidget: bottomTitleWidgets, + ), + ), + leftTitles: AxisTitles( + axisNameSize: 20, + axisNameWidget: const Text( + 'Value', + style: TextStyle( + color: AppColors.mainTextColor2, + ), + ), + sideTitles: SideTitles( + showTitles: true, + interval: 1, + reservedSize: 40, + getTitlesWidget: leftTitleWidgets, + ), + ), + ), + borderData: FlBorderData( + show: true, + border: Border.all( + color: AppColors.borderColor, + ), + ), + gridData: FlGridData( + show: true, + drawVerticalLine: false, + horizontalInterval: 1, + checkToShowHorizontalLine: (double value) { + return value == 1 || value == 6 || value == 4 || value == 5; + }, + ), + ), + ), + ), + ); + } +} diff --git a/example/lib/presentation/samples/line/line_chart_sample5.dart b/example/lib/presentation/samples/line/line_chart_sample5.dart new file mode 100644 index 0000000..61ff3b9 --- /dev/null +++ b/example/lib/presentation/samples/line/line_chart_sample5.dart @@ -0,0 +1,289 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart_app/presentation/resources/app_resources.dart'; +import 'package:flutter/material.dart'; + +class LineChartSample5 extends StatefulWidget { + const LineChartSample5({ + super.key, + Color? gradientColor1, + Color? gradientColor2, + Color? gradientColor3, + Color? indicatorStrokeColor, + }) : gradientColor1 = gradientColor1 ?? AppColors.contentColorBlue, + gradientColor2 = gradientColor2 ?? AppColors.contentColorPink, + gradientColor3 = gradientColor3 ?? AppColors.contentColorRed, + indicatorStrokeColor = indicatorStrokeColor ?? AppColors.mainTextColor1; + + final Color gradientColor1; + final Color gradientColor2; + final Color gradientColor3; + final Color indicatorStrokeColor; + + @override + State createState() => _LineChartSample5State(); +} + +class _LineChartSample5State extends State { + List showingTooltipOnSpots = [1, 3, 5]; + + List get allSpots => const [ + FlSpot(0, 1), + FlSpot(1, 2), + FlSpot(2, 1.5), + FlSpot(3, 3), + FlSpot(4, 3.5), + FlSpot(5, 5), + FlSpot(6, 8), + ]; + + Widget bottomTitleWidgets(double value, TitleMeta meta, double chartWidth) { + final style = TextStyle( + fontWeight: FontWeight.bold, + color: AppColors.contentColorPink, + fontFamily: 'Digital', + fontSize: 18 * chartWidth / 500, + ); + String text; + switch (value.toInt()) { + case 0: + text = '00:00'; + break; + case 1: + text = '04:00'; + break; + case 2: + text = '08:00'; + break; + case 3: + text = '12:00'; + break; + case 4: + text = '16:00'; + break; + case 5: + text = '20:00'; + break; + case 6: + text = '23:59'; + break; + default: + return Container(); + } + + return SideTitleWidget( + meta: meta, + child: Text(text, style: style), + ); + } + + @override + Widget build(BuildContext context) { + final lineBarsData = [ + LineChartBarData( + showingIndicators: showingTooltipOnSpots, + spots: allSpots, + isCurved: true, + barWidth: 4, + shadow: const Shadow( + blurRadius: 8, + ), + belowBarData: BarAreaData( + show: true, + gradient: LinearGradient( + colors: [ + widget.gradientColor1.withValues(alpha: 0.4), + widget.gradientColor2.withValues(alpha: 0.4), + widget.gradientColor3.withValues(alpha: 0.4), + ], + ), + ), + dotData: const FlDotData(show: false), + gradient: LinearGradient( + colors: [ + widget.gradientColor1, + widget.gradientColor2, + widget.gradientColor3, + ], + stops: const [0.1, 0.4, 0.9], + ), + ), + ]; + + final tooltipsOnBar = lineBarsData[0]; + + return AspectRatio( + aspectRatio: 2.5, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 24.0, + vertical: 10, + ), + child: LayoutBuilder(builder: (context, constraints) { + return LineChart( + LineChartData( + showingTooltipIndicators: showingTooltipOnSpots.map((index) { + return ShowingTooltipIndicators([ + LineBarSpot( + tooltipsOnBar, + lineBarsData.indexOf(tooltipsOnBar), + tooltipsOnBar.spots[index], + ), + ]); + }).toList(), + lineTouchData: LineTouchData( + enabled: true, + handleBuiltInTouches: false, + touchCallback: + (FlTouchEvent event, LineTouchResponse? response) { + if (response == null || response.lineBarSpots == null) { + return; + } + if (event is FlTapUpEvent) { + final spotIndex = response.lineBarSpots!.first.spotIndex; + setState(() { + if (showingTooltipOnSpots.contains(spotIndex)) { + showingTooltipOnSpots.remove(spotIndex); + } else { + showingTooltipOnSpots.add(spotIndex); + } + }); + } + }, + mouseCursorResolver: + (FlTouchEvent event, LineTouchResponse? response) { + if (response == null || response.lineBarSpots == null) { + return SystemMouseCursors.basic; + } + return SystemMouseCursors.click; + }, + getTouchedSpotIndicator: + (LineChartBarData barData, List spotIndexes) { + return spotIndexes.map((index) { + return TouchedSpotIndicatorData( + const FlLine( + color: Colors.pink, + ), + FlDotData( + show: true, + getDotPainter: (spot, percent, barData, index) => + FlDotCirclePainter( + radius: 8, + color: lerpGradient( + barData.gradient!.colors, + barData.gradient!.stops!, + percent / 100, + ), + strokeWidth: 2, + strokeColor: widget.indicatorStrokeColor, + ), + ), + ); + }).toList(); + }, + touchTooltipData: LineTouchTooltipData( + getTooltipColor: (touchedSpot) => Colors.pink, + tooltipBorderRadius: BorderRadius.circular(8), + getTooltipItems: (List lineBarsSpot) { + return lineBarsSpot.map((lineBarSpot) { + return LineTooltipItem( + lineBarSpot.y.toString(), + const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ); + }).toList(); + }, + ), + ), + lineBarsData: lineBarsData, + minY: 0, + titlesData: FlTitlesData( + leftTitles: const AxisTitles( + axisNameWidget: Text('count'), + axisNameSize: 24, + sideTitles: SideTitles( + showTitles: false, + reservedSize: 0, + ), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + interval: 1, + getTitlesWidget: (value, meta) { + return bottomTitleWidgets( + value, + meta, + constraints.maxWidth, + ); + }, + reservedSize: 30, + ), + ), + rightTitles: const AxisTitles( + axisNameWidget: Text('count'), + sideTitles: SideTitles( + showTitles: false, + reservedSize: 0, + ), + ), + topTitles: const AxisTitles( + axisNameWidget: Text( + 'Wall clock', + textAlign: TextAlign.left, + ), + axisNameSize: 24, + sideTitles: SideTitles( + showTitles: true, + reservedSize: 0, + ), + ), + ), + gridData: const FlGridData(show: false), + borderData: FlBorderData( + show: true, + border: Border.all( + color: AppColors.borderColor, + ), + ), + ), + ); + }), + ), + ); + } +} + +/// Lerps between a [LinearGradient] colors, based on [t] +Color lerpGradient(List colors, List stops, double t) { + if (colors.isEmpty) { + throw ArgumentError('"colors" is empty.'); + } else if (colors.length == 1) { + return colors[0]; + } + + if (stops.length != colors.length) { + stops = []; + + /// provided gradientColorStops is invalid and we calculate it here + colors.asMap().forEach((index, color) { + final percent = 1.0 / (colors.length - 1); + stops.add(percent * index); + }); + } + + for (var s = 0; s < stops.length - 1; s++) { + final leftStop = stops[s]; + final rightStop = stops[s + 1]; + final leftColor = colors[s]; + final rightColor = colors[s + 1]; + if (t <= leftStop) { + return leftColor; + } else if (t < rightStop) { + final sectionT = (t - leftStop) / (rightStop - leftStop); + return Color.lerp(leftColor, rightColor, sectionT)!; + } + } + return colors.last; +} diff --git a/example/lib/presentation/samples/line/line_chart_sample6.dart b/example/lib/presentation/samples/line/line_chart_sample6.dart new file mode 100644 index 0000000..9f2f306 --- /dev/null +++ b/example/lib/presentation/samples/line/line_chart_sample6.dart @@ -0,0 +1,286 @@ +import 'package:fl_chart_app/presentation/resources/app_resources.dart'; +import 'package:fl_chart_app/util/extensions/color_extensions.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; + +// ignore: must_be_immutable +class LineChartSample6 extends StatelessWidget { + LineChartSample6({ + super.key, + Color? line1Color1, + Color? line1Color2, + Color? line2Color1, + Color? line2Color2, + }) : line1Color1 = line1Color1 ?? AppColors.contentColorOrange, + line1Color2 = line1Color2 ?? AppColors.contentColorOrange.darken(60), + line2Color1 = line2Color1 ?? AppColors.contentColorBlue.darken(60), + line2Color2 = line2Color2 ?? AppColors.contentColorBlue { + minSpotX = spots.first.x; + maxSpotX = spots.first.x; + minSpotY = spots.first.y; + maxSpotY = spots.first.y; + + for (final spot in spots) { + if (spot.x > maxSpotX) { + maxSpotX = spot.x; + } + + if (spot.x < minSpotX) { + minSpotX = spot.x; + } + + if (spot.y > maxSpotY) { + maxSpotY = spot.y; + } + + if (spot.y < minSpotY) { + minSpotY = spot.y; + } + } + } + + final Color line1Color1; + final Color line1Color2; + final Color line2Color1; + final Color line2Color2; + + final spots = const [ + FlSpot(0, 1), + FlSpot(2, 5), + FlSpot(4, 3), + FlSpot(6, 5), + ]; + + final spots2 = const [ + FlSpot(0, 3), + FlSpot(2, 1), + FlSpot(4, 2), + FlSpot(6, 1), + ]; + + late double minSpotX; + late double maxSpotX; + late double minSpotY; + late double maxSpotY; + + Widget leftTitleWidgets(double value, TitleMeta meta) { + final style = TextStyle( + color: line1Color1, + fontWeight: FontWeight.bold, + fontSize: 18, + ); + + final intValue = reverseY(value, minSpotY, maxSpotY).toInt(); + + if (intValue == (maxSpotY + minSpotY)) { + return Text('', style: style); + } + + return Padding( + padding: const EdgeInsets.only(right: 6), + child: Text( + intValue.toString(), + style: style, + textAlign: TextAlign.center, + ), + ); + } + + Widget rightTitleWidgets(double value, TitleMeta meta) { + final style = TextStyle( + color: line2Color2, + fontWeight: FontWeight.bold, + fontSize: 18, + ); + final intValue = reverseY(value, minSpotY, maxSpotY).toInt(); + + if (intValue == (maxSpotY + minSpotY)) { + return Text('', style: style); + } + + return Text(intValue.toString(), style: style, textAlign: TextAlign.right); + } + + Widget topTitleWidgets(double value, TitleMeta meta) { + if (value % 1 != 0) { + return Container(); + } + const style = TextStyle( + fontWeight: FontWeight.bold, + color: AppColors.mainTextColor2, + ); + return SideTitleWidget( + meta: meta, + child: Text(value.toInt().toString(), style: style), + ); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(right: 22, bottom: 20), + child: AspectRatio( + aspectRatio: 2, + child: LineChart( + LineChartData( + lineTouchData: LineTouchData( + touchTooltipData: LineTouchTooltipData( + tooltipBorderRadius: BorderRadius.zero, + getTooltipColor: (spot) => Colors.white, + getTooltipItems: (List touchedSpots) { + return touchedSpots.map((LineBarSpot touchedSpot) { + return LineTooltipItem( + touchedSpot.y.toString(), + TextStyle( + color: touchedSpot.bar.gradient!.colors.first, + fontWeight: FontWeight.bold, + fontSize: 20, + ), + ); + }).toList(); + }, + ), + getTouchedSpotIndicator: ( + _, + indicators, + ) { + return indicators + .map((int index) => const TouchedSpotIndicatorData( + FlLine(color: Colors.transparent), + FlDotData(show: false), + )) + .toList(); + }, + touchSpotThreshold: 12, + distanceCalculator: + (Offset touchPoint, Offset spotPixelCoordinates) => + (touchPoint - spotPixelCoordinates).distance, + ), + lineBarsData: [ + LineChartBarData( + gradient: LinearGradient( + colors: [ + line1Color1, + line1Color2, + ], + ), + spots: reverseSpots(spots, minSpotY, maxSpotY), + isCurved: true, + isStrokeCapRound: true, + barWidth: 10, + belowBarData: BarAreaData( + show: false, + ), + dotData: FlDotData( + show: true, + getDotPainter: (spot, percent, barData, index) { + return FlDotCirclePainter( + radius: 12, + color: Color.lerp( + line1Color1, + line1Color2, + percent / 100, + )!, + strokeColor: Colors.white, + strokeWidth: 1, + ); + }, + ), + ), + LineChartBarData( + gradient: LinearGradient( + colors: [ + line2Color1, + line2Color2, + ], + ), + spots: reverseSpots(spots2, minSpotY, maxSpotY), + isCurved: true, + isStrokeCapRound: true, + barWidth: 10, + belowBarData: BarAreaData( + show: false, + ), + dotData: FlDotData( + show: true, + getDotPainter: (spot, percent, barData, index) { + return FlDotCirclePainter( + radius: 12, + color: Color.lerp( + line2Color1, + line2Color2, + percent / 100, + )!, + strokeColor: Colors.white, + strokeWidth: 1, + ); + }, + ), + ), + ], + minY: 0, + maxY: maxSpotY + minSpotY, + titlesData: FlTitlesData( + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: leftTitleWidgets, + reservedSize: 38, + ), + ), + rightTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: rightTitleWidgets, + reservedSize: 30, + ), + ), + bottomTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + topTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 32, + getTitlesWidget: topTitleWidgets, + ), + ), + ), + gridData: FlGridData( + show: true, + drawVerticalLine: true, + checkToShowHorizontalLine: (value) { + final intValue = reverseY(value, minSpotY, maxSpotY).toInt(); + + if (intValue == (maxSpotY + minSpotY).toInt()) { + return false; + } + + return true; + }, + ), + borderData: FlBorderData( + show: true, + border: const Border( + left: BorderSide(color: AppColors.borderColor), + top: BorderSide(color: AppColors.borderColor), + bottom: BorderSide(color: Colors.transparent), + right: BorderSide(color: Colors.transparent), + ), + ), + ), + ), + ), + ); + } + + double reverseY(double y, double minX, double maxX) { + return (maxX + minX) - y; + } + + List reverseSpots(List inputSpots, double minY, double maxY) { + return inputSpots.map((spot) { + return spot.copyWith(y: (maxY + minY) - spot.y); + }).toList(); + } +} diff --git a/example/lib/presentation/samples/line/line_chart_sample7.dart b/example/lib/presentation/samples/line/line_chart_sample7.dart new file mode 100644 index 0000000..fa10b88 --- /dev/null +++ b/example/lib/presentation/samples/line/line_chart_sample7.dart @@ -0,0 +1,193 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart_app/presentation/resources/app_resources.dart'; +import 'package:flutter/material.dart'; + +class LineChartSample7 extends StatelessWidget { + LineChartSample7({ + super.key, + Color? line1Color, + Color? line2Color, + Color? betweenColor, + }) : line1Color = line1Color ?? AppColors.contentColorGreen, + line2Color = line2Color ?? AppColors.contentColorRed, + betweenColor = + betweenColor ?? AppColors.contentColorRed.withValues(alpha: 0.5); + + final Color line1Color; + final Color line2Color; + final Color betweenColor; + + Widget bottomTitleWidgets(double value, TitleMeta meta) { + const style = TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + ); + String text; + switch (value.toInt()) { + case 0: + text = 'Jan'; + break; + case 1: + text = 'Feb'; + break; + case 2: + text = 'Mar'; + break; + case 3: + text = 'Apr'; + break; + case 4: + text = 'May'; + break; + case 5: + text = 'Jun'; + break; + case 6: + text = 'Jul'; + break; + case 7: + text = 'Aug'; + break; + case 8: + text = 'Sep'; + break; + case 9: + text = 'Oct'; + break; + case 10: + text = 'Nov'; + break; + case 11: + text = 'Dec'; + break; + default: + return Container(); + } + + return SideTitleWidget( + meta: meta, + space: 4, + child: Text(text, style: style), + ); + } + + Widget leftTitleWidgets(double value, TitleMeta meta) { + const style = TextStyle(fontSize: 10); + + return SideTitleWidget( + meta: meta, + child: Text( + '\$ ${value + 0.5}', + style: style, + ), + ); + } + + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: 2, + child: Padding( + padding: const EdgeInsets.only( + left: 10, + right: 18, + top: 10, + bottom: 4, + ), + child: LineChart( + LineChartData( + lineTouchData: const LineTouchData(enabled: false), + lineBarsData: [ + LineChartBarData( + spots: const [ + FlSpot(0, 4), + FlSpot(1, 3.5), + FlSpot(2, 4.5), + FlSpot(3, 1), + FlSpot(4, 4), + FlSpot(5, 6), + FlSpot(6, 6.5), + FlSpot(7, 6), + FlSpot(8, 4), + FlSpot(9, 6), + FlSpot(10, 6), + FlSpot(11, 7), + ], + isCurved: true, + barWidth: 2, + color: line1Color, + dotData: const FlDotData( + show: false, + ), + ), + LineChartBarData( + spots: const [ + FlSpot(0, 7), + FlSpot(1, 3), + FlSpot(2, 4), + FlSpot(3, 2), + FlSpot(4, 3), + FlSpot(5, 4), + FlSpot(6, 5), + FlSpot(7, 3), + FlSpot(8, 1), + FlSpot(9, 8), + FlSpot(10, 1), + FlSpot(11, 3), + ], + isCurved: false, + barWidth: 2, + color: line2Color, + dotData: const FlDotData( + show: false, + ), + ), + ], + betweenBarsData: [ + BetweenBarsData( + fromIndex: 0, + toIndex: 1, + color: betweenColor, + ) + ], + minY: 0, + borderData: FlBorderData( + show: false, + ), + titlesData: FlTitlesData( + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + interval: 1, + getTitlesWidget: bottomTitleWidgets, + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: leftTitleWidgets, + interval: 1, + reservedSize: 36, + ), + ), + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + ), + gridData: FlGridData( + show: true, + drawVerticalLine: false, + horizontalInterval: 1, + checkToShowHorizontalLine: (double value) { + return value == 1 || value == 6 || value == 4 || value == 5; + }, + ), + ), + ), + ), + ); + } +} diff --git a/example/lib/presentation/samples/line/line_chart_sample8.dart b/example/lib/presentation/samples/line/line_chart_sample8.dart new file mode 100644 index 0000000..0a0383b --- /dev/null +++ b/example/lib/presentation/samples/line/line_chart_sample8.dart @@ -0,0 +1,291 @@ +import 'dart:async'; +import 'dart:ui' as ui; + +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart_app/presentation/resources/app_resources.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show rootBundle; +import 'package:flutter_svg/flutter_svg.dart'; + +class LineChartSample8 extends StatefulWidget { + const LineChartSample8({super.key}); + + @override + State createState() => _LineChartSample8State(); +} + +class _LineChartSample8State extends State { + List gradientColors = const [ + Color(0xffEEF3FE), + Color(0xffEEF3FE), + ]; + + bool showAvg = false; + + Future loadImage(String asset) async { + final data = await rootBundle.load(asset); + final codec = await ui.instantiateImageCodec(data.buffer.asUint8List()); + final fi = await codec.getNextFrame(); + return fi.image; + } + + Future loadSvg() async { + const rawSvg = + ''; + + final pictureInfo = + await vg.loadPicture(const SvgStringLoader(rawSvg), null); + + final sizedPicture = SizedPicture(pictureInfo.picture, 14, 14); + return sizedPicture; + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: loadSvg(), + builder: (BuildContext context, imageSnapshot) { + if (imageSnapshot.connectionState == ConnectionState.done) { + return Stack( + children: [ + AspectRatio( + aspectRatio: 1.70, + child: Padding( + padding: const EdgeInsets.only( + right: 18, + left: 12, + top: 24, + bottom: 12, + ), + child: LineChart( + mainData(imageSnapshot.data!), + ), + ), + ), + ], + ); + } else { + return const CircularProgressIndicator(); + } + }, + ); + } + + Widget bottomTitleWidgets(double value, TitleMeta meta) { + const style = TextStyle( + fontSize: 10, + color: AppColors.mainTextColor1, + ); + return SideTitleWidget( + meta: meta, + child: Text(meta.formattedValue, style: style), + ); + } + + Widget leftTitleWidgets(double value, TitleMeta meta) { + IconData icon; + Color color; + switch (value.toInt()) { + case 0: + icon = Icons.wb_sunny; + color = AppColors.contentColorYellow; + break; + case 2: + icon = Icons.wine_bar_sharp; + color = AppColors.contentColorRed; + break; + case 4: + icon = Icons.watch_later; + color = AppColors.contentColorGreen; + break; + case 6: + icon = Icons.whatshot; + color = AppColors.contentColorOrange; + break; + default: + throw StateError('Invalid'); + } + return SideTitleWidget( + meta: meta, + child: Icon( + icon, + color: color, + size: 32, + ), + ); + } + + LineChartData mainData(SizedPicture sizedPicture) { + return LineChartData( + rangeAnnotations: RangeAnnotations( + verticalRangeAnnotations: [ + VerticalRangeAnnotation( + x1: 2, + x2: 5, + color: AppColors.contentColorBlue.withValues(alpha: 0.2), + ), + VerticalRangeAnnotation( + x1: 8, + x2: 9, + color: AppColors.contentColorBlue.withValues(alpha: 0.2), + ), + ], + horizontalRangeAnnotations: [ + HorizontalRangeAnnotation( + y1: 2, + y2: 3, + color: AppColors.contentColorGreen.withValues(alpha: 0.3), + ), + ], + ), + // uncomment to see ExtraLines with RangeAnnotations + extraLinesData: ExtraLinesData( +// extraLinesOnTop: true, + horizontalLines: [ + HorizontalLine( + y: 5, + color: AppColors.contentColorGreen, + strokeWidth: 2, + dashArray: [5, 10], + label: HorizontalLineLabel( + show: true, + alignment: Alignment.topRight, + padding: const EdgeInsets.only(right: 5, bottom: 5), + style: const TextStyle( + fontSize: 9, + fontWeight: FontWeight.bold, + ), + labelResolver: (line) => 'H: ${line.y}', + ), + ), + ], + verticalLines: [ + VerticalLine( + x: 5.7, + color: AppColors.contentColorBlue, + strokeWidth: 2, + dashArray: [5, 10], + label: VerticalLineLabel( + show: true, + alignment: Alignment.bottomRight, + padding: const EdgeInsets.only(left: 5, bottom: 5), + style: const TextStyle( + fontSize: 9, + fontWeight: FontWeight.bold, + ), + direction: LabelDirection.vertical, + labelResolver: (line) => 'V: ${line.x}', + ), + ), + VerticalLine( + x: 8.5, + color: Colors.transparent, + sizedPicture: sizedPicture, + ), + VerticalLine( + x: 3.5, + color: Colors.transparent, + sizedPicture: sizedPicture, + ) + ], + ), + gridData: const FlGridData( + show: true, + drawVerticalLine: false, + drawHorizontalLine: false, + verticalInterval: 1, + ), + titlesData: FlTitlesData( + show: true, + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 30, + getTitlesWidget: bottomTitleWidgets, + interval: 4, + ), + ), + leftTitles: AxisTitles( + drawBelowEverything: true, + sideTitles: SideTitles( + interval: 2, + showTitles: true, + getTitlesWidget: leftTitleWidgets, + reservedSize: 40, + ), + ), + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + ), + lineTouchData: LineTouchData( + getTouchLineEnd: (data, index) => double.infinity, + getTouchedSpotIndicator: + (LineChartBarData barData, List spotIndexes) { + return spotIndexes.map((spotIndex) { + return TouchedSpotIndicatorData( + const FlLine(color: AppColors.contentColorOrange, strokeWidth: 3), + FlDotData( + getDotPainter: (spot, percent, barData, index) => + FlDotCirclePainter( + radius: 8, + color: AppColors.contentColorOrange, + ), + ), + ); + }).toList(); + }, + touchTooltipData: LineTouchTooltipData( + getTooltipColor: (touchedSpot) => AppColors.contentColorRed, + getTooltipItems: (List touchedSpots) => touchedSpots + .map((LineBarSpot touchedSpot) => LineTooltipItem( + touchedSpot.y.toString(), + const TextStyle( + color: AppColors.contentColorWhite, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + )) + .toList(), + ), + ), + borderData: FlBorderData( + show: true, + border: Border.all( + color: AppColors.borderColor, + ), + ), + minX: 0, + maxX: 11, + minY: 0, + maxY: 6, + lineBarsData: [ + LineChartBarData( + spots: const [ + FlSpot(0, 1), + FlSpot(2, 1), + FlSpot(4.9, 5), + FlSpot(6.8, 5), + FlSpot(7.5, 3.5), + FlSpot.nullSpot, + FlSpot(7.5, 2), + FlSpot(8, 1), + FlSpot(10, 2), + FlSpot(11, 2.5), + ], + dashArray: [10, 6], + isCurved: true, + color: AppColors.contentColorRed, + barWidth: 4, + isStrokeCapRound: true, + dotData: const FlDotData( + show: false, + ), + ), + ], + ); + } +} diff --git a/example/lib/presentation/samples/line/line_chart_sample9.dart b/example/lib/presentation/samples/line/line_chart_sample9.dart new file mode 100644 index 0000000..7143fbd --- /dev/null +++ b/example/lib/presentation/samples/line/line_chart_sample9.dart @@ -0,0 +1,154 @@ +import 'dart:math'; + +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart_app/presentation/resources/app_resources.dart'; +import 'package:flutter/material.dart'; + +// ignore: must_be_immutable +class LineChartSample9 extends StatelessWidget { + LineChartSample9({super.key}); + + final spots = List.generate(101, (i) => (i - 50) / 10) + .map((x) => FlSpot(x, cos(x))) + .toList(); + + Widget bottomTitleWidgets(double value, TitleMeta meta, double chartWidth) { + if (value % 1 != 0) { + return Container(); + } + final style = TextStyle( + color: AppColors.contentColorBlue, + fontWeight: FontWeight.bold, + fontSize: min(18, 18 * chartWidth / 300), + ); + return SideTitleWidget( + meta: meta, + space: 16, + child: Text(meta.formattedValue, style: style), + ); + } + + Widget leftTitleWidgets(double value, TitleMeta meta, double chartWidth) { + final style = TextStyle( + color: AppColors.contentColorYellow, + fontWeight: FontWeight.bold, + fontSize: min(18, 18 * chartWidth / 300), + ); + return SideTitleWidget( + meta: meta, + space: 16, + child: Text(meta.formattedValue, style: style), + ); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + bottom: 12, + right: 20, + top: 20, + ), + child: AspectRatio( + aspectRatio: 1, + child: LayoutBuilder( + builder: (context, constraints) { + return LineChart( + LineChartData( + lineTouchData: LineTouchData( + touchTooltipData: LineTouchTooltipData( + maxContentWidth: 100, + getTooltipColor: (touchedSpot) => Colors.black, + getTooltipItems: (touchedSpots) { + return touchedSpots.map((LineBarSpot touchedSpot) { + final textStyle = TextStyle( + color: touchedSpot.bar.gradient?.colors[0] ?? + touchedSpot.bar.color, + fontWeight: FontWeight.bold, + fontSize: 14, + ); + return LineTooltipItem( + '${touchedSpot.x}, ${touchedSpot.y.toStringAsFixed(2)}', + textStyle, + ); + }).toList(); + }, + ), + handleBuiltInTouches: true, + getTouchLineStart: (data, index) => 0, + ), + lineBarsData: [ + LineChartBarData( + color: AppColors.contentColorPink, + spots: spots, + isCurved: true, + isStrokeCapRound: true, + barWidth: 3, + belowBarData: BarAreaData( + show: false, + ), + dotData: const FlDotData(show: false), + ), + ], + minY: -1.5, + maxY: 1.5, + titlesData: FlTitlesData( + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, meta) => + leftTitleWidgets(value, meta, constraints.maxWidth), + reservedSize: 56, + ), + drawBelowEverything: true, + ), + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, meta) => + bottomTitleWidgets(value, meta, constraints.maxWidth), + reservedSize: 36, + interval: 1, + ), + drawBelowEverything: true, + ), + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + ), + gridData: FlGridData( + show: true, + drawHorizontalLine: true, + drawVerticalLine: true, + horizontalInterval: 1.5, + verticalInterval: 5, + checkToShowHorizontalLine: (value) { + return value.toInt() == 0; + }, + getDrawingHorizontalLine: (_) => FlLine( + color: AppColors.contentColorBlue.withValues(alpha: 1), + dashArray: [8, 2], + strokeWidth: 0.8, + ), + getDrawingVerticalLine: (_) => FlLine( + color: AppColors.contentColorYellow.withValues(alpha: 1), + dashArray: [8, 2], + strokeWidth: 0.8, + ), + checkToShowVerticalLine: (value) { + return value.toInt() == 0; + }, + ), + borderData: FlBorderData(show: false), + ), + ); + }, + ), + ), + ); + } +} diff --git a/example/lib/presentation/samples/pie/pie_chart_sample1.dart b/example/lib/presentation/samples/pie/pie_chart_sample1.dart new file mode 100644 index 0000000..31e64ed --- /dev/null +++ b/example/lib/presentation/samples/pie/pie_chart_sample1.dart @@ -0,0 +1,173 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart_app/presentation/resources/app_resources.dart'; +import 'package:fl_chart_app/presentation/widgets/indicator.dart'; +import 'package:flutter/material.dart'; + +class PieChartSample1 extends StatefulWidget { + const PieChartSample1({super.key}); + + @override + State createState() => PieChartSample1State(); +} + +class PieChartSample1State extends State { + int touchedIndex = -1; + + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: 1.3, + child: Column( + children: [ + const SizedBox( + height: 28, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Indicator( + color: AppColors.contentColorBlue, + text: 'One', + isSquare: false, + size: touchedIndex == 0 ? 18 : 16, + textColor: touchedIndex == 0 + ? AppColors.mainTextColor1 + : AppColors.mainTextColor3, + ), + Indicator( + color: AppColors.contentColorYellow, + text: 'Two', + isSquare: false, + size: touchedIndex == 1 ? 18 : 16, + textColor: touchedIndex == 1 + ? AppColors.mainTextColor1 + : AppColors.mainTextColor3, + ), + Indicator( + color: AppColors.contentColorPink, + text: 'Three', + isSquare: false, + size: touchedIndex == 2 ? 18 : 16, + textColor: touchedIndex == 2 + ? AppColors.mainTextColor1 + : AppColors.mainTextColor3, + ), + Indicator( + color: AppColors.contentColorGreen, + text: 'Four', + isSquare: false, + size: touchedIndex == 3 ? 18 : 16, + textColor: touchedIndex == 3 + ? AppColors.mainTextColor1 + : AppColors.mainTextColor3, + ), + ], + ), + const SizedBox( + height: 18, + ), + Expanded( + child: AspectRatio( + aspectRatio: 1, + child: PieChart( + PieChartData( + pieTouchData: PieTouchData( + touchCallback: (FlTouchEvent event, pieTouchResponse) { + setState(() { + if (!event.isInterestedForInteractions || + pieTouchResponse == null || + pieTouchResponse.touchedSection == null) { + touchedIndex = -1; + return; + } + touchedIndex = pieTouchResponse + .touchedSection!.touchedSectionIndex; + }); + }, + ), + startDegreeOffset: 180, + borderData: FlBorderData( + show: false, + ), + sectionsSpace: 1, + centerSpaceRadius: 0, + sections: showingSections(), + ), + ), + ), + ), + ], + ), + ); + } + + List showingSections() { + return List.generate( + 4, + (i) { + final isTouched = i == touchedIndex; + const color0 = AppColors.contentColorBlue; + const color1 = AppColors.contentColorYellow; + const color2 = AppColors.contentColorPink; + const color3 = AppColors.contentColorGreen; + + switch (i) { + case 0: + return PieChartSectionData( + color: color0, + value: 25, + title: '', + radius: 80, + titlePositionPercentageOffset: 0.55, + borderSide: isTouched + ? const BorderSide( + color: AppColors.contentColorWhite, width: 6) + : BorderSide( + color: AppColors.contentColorWhite.withValues(alpha: 0)), + ); + case 1: + return PieChartSectionData( + color: color1, + value: 25, + title: '', + radius: 65, + titlePositionPercentageOffset: 0.55, + borderSide: isTouched + ? const BorderSide( + color: AppColors.contentColorWhite, width: 6) + : BorderSide( + color: AppColors.contentColorWhite.withValues(alpha: 0)), + ); + case 2: + return PieChartSectionData( + color: color2, + value: 25, + title: '', + radius: 60, + titlePositionPercentageOffset: 0.6, + borderSide: isTouched + ? const BorderSide( + color: AppColors.contentColorWhite, width: 6) + : BorderSide( + color: AppColors.contentColorWhite.withValues(alpha: 0)), + ); + case 3: + return PieChartSectionData( + color: color3, + value: 25, + title: '', + radius: 70, + titlePositionPercentageOffset: 0.55, + borderSide: isTouched + ? const BorderSide( + color: AppColors.contentColorWhite, width: 6) + : BorderSide( + color: AppColors.contentColorWhite.withValues(alpha: 0)), + ); + default: + throw Error(); + } + }, + ); + } +} diff --git a/example/lib/presentation/samples/pie/pie_chart_sample2.dart b/example/lib/presentation/samples/pie/pie_chart_sample2.dart new file mode 100644 index 0000000..5b0b6cc --- /dev/null +++ b/example/lib/presentation/samples/pie/pie_chart_sample2.dart @@ -0,0 +1,164 @@ +import 'package:fl_chart_app/presentation/resources/app_resources.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart_app/presentation/widgets/indicator.dart'; +import 'package:flutter/material.dart'; + +class PieChartSample2 extends StatefulWidget { + const PieChartSample2({super.key}); + + @override + State createState() => PieChart2State(); +} + +class PieChart2State extends State { + int touchedIndex = -1; + + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: 1.3, + child: Row( + children: [ + const SizedBox( + height: 18, + ), + Expanded( + child: AspectRatio( + aspectRatio: 1, + child: PieChart( + PieChartData( + pieTouchData: PieTouchData( + touchCallback: (FlTouchEvent event, pieTouchResponse) { + setState(() { + if (!event.isInterestedForInteractions || + pieTouchResponse == null || + pieTouchResponse.touchedSection == null) { + touchedIndex = -1; + return; + } + touchedIndex = pieTouchResponse + .touchedSection!.touchedSectionIndex; + }); + }, + ), + borderData: FlBorderData( + show: false, + ), + sectionsSpace: 0, + centerSpaceRadius: 40, + sections: showingSections(), + ), + ), + ), + ), + const Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Indicator( + color: AppColors.contentColorBlue, + text: 'First', + isSquare: true, + ), + SizedBox( + height: 4, + ), + Indicator( + color: AppColors.contentColorYellow, + text: 'Second', + isSquare: true, + ), + SizedBox( + height: 4, + ), + Indicator( + color: AppColors.contentColorPurple, + text: 'Third', + isSquare: true, + ), + SizedBox( + height: 4, + ), + Indicator( + color: AppColors.contentColorGreen, + text: 'Fourth', + isSquare: true, + ), + SizedBox( + height: 18, + ), + ], + ), + const SizedBox( + width: 28, + ), + ], + ), + ); + } + + List showingSections() { + return List.generate(4, (i) { + final isTouched = i == touchedIndex; + final fontSize = isTouched ? 25.0 : 16.0; + final radius = isTouched ? 60.0 : 50.0; + const shadows = [Shadow(color: Colors.black, blurRadius: 2)]; + switch (i) { + case 0: + return PieChartSectionData( + color: AppColors.contentColorBlue, + value: 40, + title: '40%', + radius: radius, + titleStyle: TextStyle( + fontSize: fontSize, + fontWeight: FontWeight.bold, + color: AppColors.mainTextColor1, + shadows: shadows, + ), + ); + case 1: + return PieChartSectionData( + color: AppColors.contentColorYellow, + value: 30, + title: '30%', + radius: radius, + titleStyle: TextStyle( + fontSize: fontSize, + fontWeight: FontWeight.bold, + color: AppColors.mainTextColor1, + shadows: shadows, + ), + ); + case 2: + return PieChartSectionData( + color: AppColors.contentColorPurple, + value: 15, + title: '15%', + radius: radius, + titleStyle: TextStyle( + fontSize: fontSize, + fontWeight: FontWeight.bold, + color: AppColors.mainTextColor1, + shadows: shadows, + ), + ); + case 3: + return PieChartSectionData( + color: AppColors.contentColorGreen, + value: 15, + title: '15%', + radius: radius, + titleStyle: TextStyle( + fontSize: fontSize, + fontWeight: FontWeight.bold, + color: AppColors.mainTextColor1, + shadows: shadows, + ), + ); + default: + throw Error(); + } + }); + } +} diff --git a/example/lib/presentation/samples/pie/pie_chart_sample3.dart b/example/lib/presentation/samples/pie/pie_chart_sample3.dart new file mode 100644 index 0000000..5929514 --- /dev/null +++ b/example/lib/presentation/samples/pie/pie_chart_sample3.dart @@ -0,0 +1,181 @@ +import 'package:fl_chart_app/presentation/resources/app_resources.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class PieChartSample3 extends StatefulWidget { + const PieChartSample3({super.key}); + + @override + State createState() => PieChartSample3State(); +} + +class PieChartSample3State extends State { + int touchedIndex = 0; + + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: 1.3, + child: AspectRatio( + aspectRatio: 1, + child: PieChart( + PieChartData( + pieTouchData: PieTouchData( + touchCallback: (FlTouchEvent event, pieTouchResponse) { + setState(() { + if (!event.isInterestedForInteractions || + pieTouchResponse == null || + pieTouchResponse.touchedSection == null) { + touchedIndex = -1; + return; + } + touchedIndex = + pieTouchResponse.touchedSection!.touchedSectionIndex; + }); + }, + ), + borderData: FlBorderData( + show: false, + ), + sectionsSpace: 0, + centerSpaceRadius: 0, + sections: showingSections(), + ), + ), + ), + ); + } + + List showingSections() { + return List.generate(4, (i) { + final isTouched = i == touchedIndex; + final fontSize = isTouched ? 20.0 : 16.0; + final radius = isTouched ? 110.0 : 100.0; + final widgetSize = isTouched ? 55.0 : 40.0; + const shadows = [Shadow(color: Colors.black, blurRadius: 2)]; + + switch (i) { + case 0: + return PieChartSectionData( + color: AppColors.contentColorBlue, + value: 40, + title: '40%', + radius: radius, + titleStyle: TextStyle( + fontSize: fontSize, + fontWeight: FontWeight.bold, + color: const Color(0xffffffff), + shadows: shadows, + ), + badgeWidget: _Badge( + 'assets/icons/ophthalmology-svgrepo-com.svg', + size: widgetSize, + borderColor: AppColors.contentColorBlack, + ), + badgePositionPercentageOffset: .98, + ); + case 1: + return PieChartSectionData( + color: AppColors.contentColorYellow, + value: 30, + title: '30%', + radius: radius, + titleStyle: TextStyle( + fontSize: fontSize, + fontWeight: FontWeight.bold, + color: const Color(0xffffffff), + shadows: shadows, + ), + badgeWidget: _Badge( + 'assets/icons/librarian-svgrepo-com.svg', + size: widgetSize, + borderColor: AppColors.contentColorBlack, + ), + badgePositionPercentageOffset: .98, + ); + case 2: + return PieChartSectionData( + color: AppColors.contentColorPurple, + value: 16, + title: '16%', + radius: radius, + titleStyle: TextStyle( + fontSize: fontSize, + fontWeight: FontWeight.bold, + color: const Color(0xffffffff), + shadows: shadows, + ), + badgeWidget: _Badge( + 'assets/icons/fitness-svgrepo-com.svg', + size: widgetSize, + borderColor: AppColors.contentColorBlack, + ), + badgePositionPercentageOffset: .98, + ); + case 3: + return PieChartSectionData( + color: AppColors.contentColorGreen, + value: 15, + title: '15%', + radius: radius, + titleStyle: TextStyle( + fontSize: fontSize, + fontWeight: FontWeight.bold, + color: const Color(0xffffffff), + shadows: shadows, + ), + badgeWidget: _Badge( + 'assets/icons/worker-svgrepo-com.svg', + size: widgetSize, + borderColor: AppColors.contentColorBlack, + ), + badgePositionPercentageOffset: .98, + ); + default: + throw Exception('Oh no'); + } + }); + } +} + +class _Badge extends StatelessWidget { + const _Badge( + this.svgAsset, { + required this.size, + required this.borderColor, + }); + final String svgAsset; + final double size; + final Color borderColor; + + @override + Widget build(BuildContext context) { + return AnimatedContainer( + duration: PieChart.defaultDuration, + width: size, + height: size, + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + border: Border.all( + color: borderColor, + width: 2, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: .5), + offset: const Offset(3, 3), + blurRadius: 3, + ), + ], + ), + padding: EdgeInsets.all(size * .15), + child: Center( + child: SvgPicture.asset( + svgAsset, + ), + ), + ); + } +} diff --git a/example/lib/presentation/samples/radar/radar_chart_sample1.dart b/example/lib/presentation/samples/radar/radar_chart_sample1.dart new file mode 100644 index 0000000..64b8ee8 --- /dev/null +++ b/example/lib/presentation/samples/radar/radar_chart_sample1.dart @@ -0,0 +1,285 @@ +import 'package:fl_chart_app/presentation/resources/app_resources.dart'; +import 'package:fl_chart_app/util/extensions/color_extensions.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; + +class RadarChartSample1 extends StatefulWidget { + RadarChartSample1({super.key}); + + final gridColor = AppColors.contentColorPurple.lighten(80); + final titleColor = AppColors.contentColorPurple.lighten(80); + final fashionColor = AppColors.contentColorRed; + final artColor = AppColors.contentColorCyan; + final boxingColor = AppColors.contentColorGreen; + final entertainmentColor = AppColors.contentColorWhite; + final offRoadColor = AppColors.contentColorYellow; + + @override + State createState() => _RadarChartSample1State(); +} + +class _RadarChartSample1State extends State { + int selectedDataSetIndex = -1; + double angleValue = 0; + bool relativeAngleMode = true; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'Title configuration', + style: TextStyle( + color: AppColors.mainTextColor2, + ), + ), + Row( + children: [ + const Text( + 'Angle', + style: TextStyle( + color: AppColors.mainTextColor2, + ), + ), + Slider( + value: angleValue, + max: 360, + onChanged: (double value) => setState(() => angleValue = value), + ), + ], + ), + Row( + children: [ + Checkbox( + value: relativeAngleMode, + onChanged: (v) => setState(() => relativeAngleMode = v!), + ), + const Text('Relative'), + ], + ), + GestureDetector( + onTap: () { + setState(() { + selectedDataSetIndex = -1; + }); + }, + child: Text( + 'Categories'.toUpperCase(), + style: const TextStyle( + fontSize: 32, + fontWeight: FontWeight.w300, + color: AppColors.mainTextColor1, + ), + ), + ), + const SizedBox(height: 4), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: rawDataSets() + .asMap() + .map((index, value) { + final isSelected = index == selectedDataSetIndex; + return MapEntry( + index, + GestureDetector( + onTap: () { + setState(() { + selectedDataSetIndex = index; + }); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + margin: const EdgeInsets.symmetric(vertical: 2), + height: 26, + decoration: BoxDecoration( + color: isSelected + ? AppColors.pageBackground + : Colors.transparent, + borderRadius: BorderRadius.circular(46), + ), + padding: const EdgeInsets.symmetric( + vertical: 4, + horizontal: 6, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 400), + curve: Curves.easeInToLinear, + padding: EdgeInsets.all(isSelected ? 8 : 6), + decoration: BoxDecoration( + color: value.color, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 8), + AnimatedDefaultTextStyle( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInToLinear, + style: TextStyle( + color: + isSelected ? value.color : widget.gridColor, + ), + child: Text(value.title), + ), + ], + ), + ), + ), + ); + }) + .values + .toList(), + ), + AspectRatio( + aspectRatio: 1.3, + child: RadarChart( + RadarChartData( + radarTouchData: RadarTouchData( + touchCallback: (FlTouchEvent event, response) { + if (!event.isInterestedForInteractions) { + setState(() { + selectedDataSetIndex = -1; + }); + return; + } + setState(() { + selectedDataSetIndex = + response?.touchedSpot?.touchedDataSetIndex ?? -1; + }); + }, + ), + dataSets: showingDataSets(), + radarBackgroundColor: Colors.transparent, + borderData: FlBorderData(show: false), + radarBorderData: const BorderSide(color: Colors.transparent), + titlePositionPercentageOffset: 0.2, + titleTextStyle: + TextStyle(color: widget.titleColor, fontSize: 14), + getTitle: (index, angle) { + final usedAngle = + relativeAngleMode ? angle + angleValue : angleValue; + switch (index) { + case 0: + return RadarChartTitle( + text: 'Mobile or Tablet', + angle: usedAngle, + ); + case 2: + return RadarChartTitle( + text: 'Desktop', + angle: usedAngle, + ); + case 1: + return RadarChartTitle(text: 'TV', angle: usedAngle); + default: + return const RadarChartTitle(text: ''); + } + }, + tickCount: 1, + ticksTextStyle: + const TextStyle(color: Colors.transparent, fontSize: 10), + tickBorderData: const BorderSide(color: Colors.transparent), + gridBorderData: BorderSide(color: widget.gridColor, width: 2), + ), + duration: const Duration(milliseconds: 400), + ), + ), + ], + ), + ); + } + + List showingDataSets() { + return rawDataSets().asMap().entries.map((entry) { + final index = entry.key; + final rawDataSet = entry.value; + + final isSelected = index == selectedDataSetIndex + ? true + : selectedDataSetIndex == -1 + ? true + : false; + + return RadarDataSet( + fillColor: isSelected + ? rawDataSet.color.withValues(alpha: 0.2) + : rawDataSet.color.withValues(alpha: 0.05), + borderColor: isSelected + ? rawDataSet.color + : rawDataSet.color.withValues(alpha: 0.25), + entryRadius: isSelected ? 3 : 2, + dataEntries: + rawDataSet.values.map((e) => RadarEntry(value: e)).toList(), + borderWidth: isSelected ? 2.3 : 2, + ); + }).toList(); + } + + List rawDataSets() { + return [ + RawDataSet( + title: 'Fashion', + color: widget.fashionColor, + values: [ + 300, + 50, + 250, + ], + ), + RawDataSet( + title: 'Art & Tech', + color: widget.artColor, + values: [ + 250, + 100, + 200, + ], + ), + RawDataSet( + title: 'Entertainment', + color: widget.entertainmentColor, + values: [ + 200, + 150, + 50, + ], + ), + RawDataSet( + title: 'Off-road Vehicle', + color: widget.offRoadColor, + values: [ + 150, + 200, + 150, + ], + ), + RawDataSet( + title: 'Boxing', + color: widget.boxingColor, + values: [ + 100, + 250, + 100, + ], + ), + ]; + } +} + +class RawDataSet { + RawDataSet({ + required this.title, + required this.color, + required this.values, + }); + + final String title; + final Color color; + final List values; +} diff --git a/example/lib/presentation/samples/scatter/scatter_chart_sample1.dart b/example/lib/presentation/samples/scatter/scatter_chart_sample1.dart new file mode 100644 index 0000000..8e69e3e --- /dev/null +++ b/example/lib/presentation/samples/scatter/scatter_chart_sample1.dart @@ -0,0 +1,513 @@ +import 'dart:math'; + +import 'package:fl_chart_app/presentation/resources/app_resources.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; + +class ScatterChartSample1 extends StatefulWidget { + ScatterChartSample1({super.key}); + + final blue1 = AppColors.contentColorBlue.withValues(alpha: 0.5); + final blue2 = AppColors.contentColorBlue; + + @override + State createState() => ScatterChartSample1State(); +} + +class ScatterChartSample1State extends State { + final maxX = 50.0; + final maxY = 50.0; + final radius = 8.0; + + bool showFlutter = true; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + setState(() { + showFlutter = !showFlutter; + }); + }, + child: AspectRatio( + aspectRatio: 1, + child: ScatterChart( + ScatterChartData( + scatterSpots: showFlutter ? flutterLogoData() : randomData(), + minX: 0, + maxX: maxX, + minY: 0, + maxY: maxY, + borderData: FlBorderData( + show: false, + ), + gridData: const FlGridData( + show: false, + ), + titlesData: const FlTitlesData( + show: false, + ), + scatterTouchData: ScatterTouchData( + enabled: false, + ), + ), + duration: const Duration(milliseconds: 600), + curve: Curves.fastOutSlowIn, + ), + ), + ); + } + + List flutterLogoData() { + return [ + /// section 1 + ScatterSpot( + 20, + 14.5, + dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius), + ), + ScatterSpot( + 20, + 14.5, + dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius), + ), + ScatterSpot( + 22, + 16.5, + dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius), + ), + ScatterSpot( + 24, + 18.5, + dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius), + ), + + ScatterSpot( + 22, + 12.5, + dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius), + ), + ScatterSpot( + 24, + 14.5, + dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius), + ), + ScatterSpot( + 26, + 16.5, + dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius), + ), + + ScatterSpot( + 24, + 10.5, + dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius), + ), + ScatterSpot( + 26, + 12.5, + dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius), + ), + ScatterSpot( + 28, + 14.5, + dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius), + ), + + ScatterSpot( + 26, + 8.5, + dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius), + ), + ScatterSpot( + 28, + 10.5, + dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius), + ), + ScatterSpot( + 30, + 12.5, + dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius), + ), + + ScatterSpot( + 28, + 6.5, + dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius), + ), + ScatterSpot( + 30, + 8.5, + dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius), + ), + ScatterSpot( + 32, + 10.5, + dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius), + ), + + ScatterSpot( + 30, + 4.5, + dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius), + ), + ScatterSpot( + 32, + 6.5, + dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius), + ), + ScatterSpot( + 34, + 8.5, + dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius), + ), + + ScatterSpot( + 34, + 4.5, + dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius), + ), + ScatterSpot( + 36, + 6.5, + dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius), + ), + + ScatterSpot( + 38, + 4.5, + dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius), + ), + + /// section 2 + ScatterSpot( + 20, + 14.5, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + ScatterSpot( + 22, + 12.5, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + ScatterSpot( + 24, + 10.5, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + + ScatterSpot( + 22, + 16.5, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + ScatterSpot( + 24, + 14.5, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + ScatterSpot( + 26, + 12.5, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + + ScatterSpot( + 24, + 18.5, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + ScatterSpot( + 26, + 16.5, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + ScatterSpot( + 28, + 14.5, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + + ScatterSpot( + 26, + 20.5, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + ScatterSpot( + 28, + 18.5, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + ScatterSpot( + 30, + 16.5, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + + ScatterSpot( + 28, + 22.5, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + ScatterSpot( + 30, + 20.5, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + ScatterSpot( + 32, + 18.5, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + + ScatterSpot( + 30, + 24.5, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + ScatterSpot( + 32, + 22.5, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + ScatterSpot( + 34, + 20.5, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + + ScatterSpot( + 34, + 24.5, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + ScatterSpot( + 36, + 22.5, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + + ScatterSpot( + 38, + 24.5, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + + /// section 3 + ScatterSpot( + 10, + 25, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + ScatterSpot( + 12, + 23, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + ScatterSpot( + 14, + 21, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + + ScatterSpot( + 12, + 27, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + ScatterSpot( + 14, + 25, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + ScatterSpot( + 16, + 23, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + + ScatterSpot( + 14, + 29, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + ScatterSpot( + 16, + 27, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + ScatterSpot( + 18, + 25, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + + ScatterSpot( + 16, + 31, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + ScatterSpot( + 18, + 29, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + ScatterSpot( + 20, + 27, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + + ScatterSpot( + 18, + 33, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + ScatterSpot( + 20, + 31, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + ScatterSpot( + 22, + 29, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + + ScatterSpot( + 20, + 35, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + ScatterSpot( + 22, + 33, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + ScatterSpot( + 24, + 31, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + + ScatterSpot( + 22, + 37, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + ScatterSpot( + 24, + 35, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + ScatterSpot( + 26, + 33, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + + ScatterSpot( + 24, + 39, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + ScatterSpot( + 26, + 37, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + ScatterSpot( + 28, + 35, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + + ScatterSpot( + 26, + 41, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + ScatterSpot( + 28, + 39, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + ScatterSpot( + 30, + 37, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + + ScatterSpot( + 28, + 43, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + ScatterSpot( + 30, + 41, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + ScatterSpot( + 32, + 39, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + + ScatterSpot( + 30, + 45, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + ScatterSpot( + 32, + 43, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + ScatterSpot( + 34, + 41, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + + ScatterSpot( + 34, + 45, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + ScatterSpot( + 36, + 43, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + + ScatterSpot( + 38, + 45, + dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius), + ), + ]; + } + + List randomData() { + const blue1Count = 21; + const blue2Count = 57; + return List.generate(blue1Count + blue2Count, (i) { + Color color; + if (i < blue1Count) { + color = widget.blue1; + } else { + color = widget.blue2; + } + + return ScatterSpot( + (Random().nextDouble() * (maxX - 8)) + 4, + (Random().nextDouble() * (maxY - 8)) + 4, + dotPainter: FlDotCirclePainter( + color: color, + radius: (Random().nextDouble() * 16) + 4, + ), + ); + }); + } +} diff --git a/example/lib/presentation/samples/scatter/scatter_chart_sample2.dart b/example/lib/presentation/samples/scatter/scatter_chart_sample2.dart new file mode 100644 index 0000000..c3df0a0 --- /dev/null +++ b/example/lib/presentation/samples/scatter/scatter_chart_sample2.dart @@ -0,0 +1,238 @@ +import 'dart:math'; + +import 'package:fl_chart_app/presentation/resources/app_resources.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; + +class ScatterChartSample2 extends StatefulWidget { + const ScatterChartSample2({super.key}); + + @override + State createState() => _ScatterChartSample2State(); +} + +class _ScatterChartSample2State extends State { + int touchedIndex = -1; + + Color greyColor = Colors.grey; + final _availableColors = [ + AppColors.contentColorGreen, + AppColors.contentColorYellow, + AppColors.contentColorPink, + AppColors.contentColorOrange, + AppColors.contentColorPurple, + AppColors.contentColorBlue, + AppColors.contentColorRed, + AppColors.contentColorCyan, + AppColors.contentColorBlue, + AppColors.contentColorGreen, + AppColors.contentColorPink, + ]; + + List selectedSpots = []; + + PainterType _currentPaintType = PainterType.circle; + + static FlDotPainter _getPaint(PainterType type, double size, Color color) { + switch (type) { + case PainterType.circle: + return FlDotCirclePainter( + color: color, + radius: size, + ); + case PainterType.square: + return FlDotSquarePainter( + color: color, + size: size * 2, + strokeWidth: 0, + ); + case PainterType.cross: + return FlDotCrossPainter( + color: color, + size: size * 2, + width: max(size / 5, 2), + ); + } + } + + @override + Widget build(BuildContext context) { + // (x, y, size) + final data = [ + (4.0, 4.0, 4.0), + (2.0, 5.0, 12.0), + (4.0, 5.0, 8.0), + (8.0, 6.0, 20.0), + (5.0, 7.0, 14.0), + (7.0, 2.0, 18.0), + (3.0, 2.0, 36.0), + (2.0, 8.0, 22.0), + (8.0, 8.0, 32.0), + (5.0, 2.5, 24.0), + (3.0, 7.0, 18.0), + ]; + return AspectRatio( + aspectRatio: 1, + child: Stack( + children: [ + ScatterChart( + ScatterChartData( + scatterSpots: data.asMap().entries.map((e) { + final index = e.key; + final (double x, double y, double size) = e.value; + return ScatterSpot( + x, + y, + dotPainter: _getPaint( + _currentPaintType, + size, + selectedSpots.contains(index) + ? _availableColors[index % _availableColors.length] + : AppColors.contentColorWhite.withValues(alpha: 0.5), + ), + ); + }).toList(), + minX: 0, + maxX: 10, + minY: 0, + maxY: 10, + borderData: FlBorderData( + show: false, + ), + gridData: FlGridData( + show: true, + drawHorizontalLine: true, + checkToShowHorizontalLine: (value) => true, + getDrawingHorizontalLine: (value) => const FlLine( + color: AppColors.gridLinesColor, + ), + drawVerticalLine: true, + checkToShowVerticalLine: (value) => true, + getDrawingVerticalLine: (value) => const FlLine( + color: AppColors.gridLinesColor, + ), + ), + titlesData: const FlTitlesData( + show: false, + ), + showingTooltipIndicators: selectedSpots, + scatterTouchData: ScatterTouchData( + enabled: true, + handleBuiltInTouches: false, + mouseCursorResolver: + (FlTouchEvent touchEvent, ScatterTouchResponse? response) { + return response == null || response.touchedSpot == null + ? MouseCursor.defer + : SystemMouseCursors.click; + }, + touchTooltipData: ScatterTouchTooltipData( + getTooltipColor: (ScatterSpot touchedBarSpot) { + return touchedBarSpot.dotPainter.mainColor; + }, + getTooltipItems: (ScatterSpot touchedBarSpot) { + final bool isBgDark = + switch ((touchedBarSpot.x, touchedBarSpot.y)) { + (4.0, 4.0) => false, + (2.0, 5.0) => false, + (4.0, 5.0) => true, + (8.0, 6.0) => true, + (5.0, 7.0) => true, + (7.0, 2.0) => true, + (3.0, 2.0) => true, + (2.0, 8.0) => false, + (8.0, 8.0) => true, + (5.0, 2.5) => false, + (3.0, 7.0) => true, + _ => false, + }; + + final color1 = isBgDark ? Colors.grey[100] : Colors.black87; + final color2 = isBgDark ? Colors.white : Colors.black; + return ScatterTooltipItem( + 'X: ', + textStyle: TextStyle( + height: 1.2, + color: color1, + fontStyle: FontStyle.italic, + ), + bottomMargin: 10, + children: [ + TextSpan( + text: '${touchedBarSpot.x.toInt()} \n', + style: TextStyle( + color: color2, + fontStyle: FontStyle.normal, + fontWeight: FontWeight.bold, + ), + ), + TextSpan( + text: 'Y: ', + style: TextStyle( + height: 1.2, + color: color1, + fontStyle: FontStyle.italic, + ), + ), + TextSpan( + text: touchedBarSpot.y.toInt().toString(), + style: TextStyle( + color: color2, + fontStyle: FontStyle.normal, + fontWeight: FontWeight.bold, + ), + ), + ], + ); + }, + ), + touchCallback: + (FlTouchEvent event, ScatterTouchResponse? touchResponse) { + if (touchResponse == null || + touchResponse.touchedSpot == null) { + return; + } + if (event is FlTapUpEvent) { + final sectionIndex = touchResponse.touchedSpot!.spotIndex; + setState(() { + if (selectedSpots.contains(sectionIndex)) { + selectedSpots.remove(sectionIndex); + } else { + selectedSpots.add(sectionIndex); + } + }); + } + }, + ), + ), + ), + Align( + alignment: Alignment.topLeft, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: DropdownButton( + value: _currentPaintType, + items: PainterType.values + .map((e) => DropdownMenuItem( + value: e, + child: Text(e.name), + )) + .toList(), + onChanged: (PainterType? value) { + setState(() { + _currentPaintType = value!; + }); + }, + ), + ), + ), + ], + ), + ); + } +} + +enum PainterType { + circle, + square, + cross, +} diff --git a/example/lib/presentation/widgets/chart_holder.dart b/example/lib/presentation/widgets/chart_holder.dart new file mode 100644 index 0000000..93a4a39 --- /dev/null +++ b/example/lib/presentation/widgets/chart_holder.dart @@ -0,0 +1,54 @@ +import 'package:fl_chart_app/presentation/resources/app_resources.dart'; +import 'package:fl_chart_app/presentation/samples/chart_sample.dart'; +import 'package:fl_chart_app/util/app_utils.dart'; +import 'package:flutter/material.dart'; + +class ChartHolder extends StatelessWidget { + final ChartSample chartSample; + + const ChartHolder({ + super.key, + required this.chartSample, + }); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + const SizedBox(width: 6), + Text( + chartSample.name, + style: const TextStyle( + color: AppColors.primary, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + Expanded(child: Container()), + IconButton( + onPressed: () => AppUtils().tryToLaunchUrl(chartSample.url), + icon: const Icon( + Icons.code, + color: AppColors.primary, + ), + tooltip: 'Source code', + ), + ], + ), + const SizedBox(height: 2), + Container( + decoration: const BoxDecoration( + color: AppColors.itemsBackground, + borderRadius: + BorderRadius.all(Radius.circular(AppDimens.defaultRadius)), + ), + child: chartSample.builder(context), + ), + ], + ); + } +} diff --git a/example/lib/presentation/widgets/download_native_app_button.dart b/example/lib/presentation/widgets/download_native_app_button.dart new file mode 100644 index 0000000..679b4a7 --- /dev/null +++ b/example/lib/presentation/widgets/download_native_app_button.dart @@ -0,0 +1,64 @@ +import 'package:fl_chart_app/presentation/resources/app_colors.dart'; +import 'package:flutter/material.dart'; + +class DownloadNativeAppButton extends StatelessWidget { + const DownloadNativeAppButton({ + super.key, + required this.onClose, + required this.onDownload, + }); + + final VoidCallback onClose; + final VoidCallback onDownload; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: AppColors.contentColorYellow, + borderRadius: BorderRadius.circular(999), + ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(999), + onTap: onDownload, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 4, + horizontal: 8, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(width: 4), + const Icon( + size: 28, + Icons.download_for_offline, + color: AppColors.contentColorBlack, + ), + const SizedBox(width: 4), + const Text( + 'Download Native App', + style: TextStyle( + color: AppColors.contentColorBlack, + fontSize: 16, + fontWeight: FontWeight.w900, + ), + ), + IconButton( + onPressed: onClose, + icon: const Icon( + size: 16, + Icons.close, + color: AppColors.contentColorBlack, + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/example/lib/presentation/widgets/indicator.dart b/example/lib/presentation/widgets/indicator.dart new file mode 100644 index 0000000..4a8ae9f --- /dev/null +++ b/example/lib/presentation/widgets/indicator.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; + +class Indicator extends StatelessWidget { + const Indicator({ + super.key, + required this.color, + required this.text, + required this.isSquare, + this.size = 16, + this.textColor, + }); + final Color color; + final String text; + final bool isSquare; + final double size; + final Color? textColor; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Container( + width: size, + height: size, + decoration: BoxDecoration( + shape: isSquare ? BoxShape.rectangle : BoxShape.circle, + color: color, + ), + ), + const SizedBox( + width: 4, + ), + Text( + text, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: textColor, + ), + ) + ], + ); + } +} diff --git a/example/lib/presentation/widgets/legend_widget.dart b/example/lib/presentation/widgets/legend_widget.dart new file mode 100644 index 0000000..fcc5c9e --- /dev/null +++ b/example/lib/presentation/widgets/legend_widget.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; + +class LegendWidget extends StatelessWidget { + const LegendWidget({ + super.key, + required this.name, + required this.color, + }); + final String name; + final Color color; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 10, + height: 10, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: color, + ), + ), + const SizedBox(width: 6), + Text( + name, + style: const TextStyle( + color: Color(0xff757391), + fontSize: 12, + ), + ), + ], + ); + } +} + +class LegendsListWidget extends StatelessWidget { + const LegendsListWidget({ + super.key, + required this.legends, + }); + final List legends; + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 16, + children: legends + .map( + (e) => LegendWidget( + name: e.name, + color: e.color, + ), + ) + .toList(), + ); + } +} + +class Legend { + Legend(this.name, this.color); + final String name; + final Color color; +} diff --git a/example/lib/urls.dart b/example/lib/urls.dart new file mode 100644 index 0000000..bac81ef --- /dev/null +++ b/example/lib/urls.dart @@ -0,0 +1,23 @@ +import 'package:fl_chart_app/util/app_helper.dart'; + +class Urls { + static const flChartUrl = 'https://flchart.dev'; + static const flChartGithubUrl = 'https://github.com/imaNNeo/fl_chart'; + + static String get aboutUrl => '$flChartUrl/about'; + + static String get downloadUrl => '$flChartUrl/download'; + + static String getChartSourceCodeUrl(ChartType chartType, int sampleNumber) { + final chartDir = chartType.name.toLowerCase(); + return 'https://github.com/imaNNeo/fl_chart/blob/main/example/lib/presentation/samples/$chartDir/${chartDir}_chart_sample$sampleNumber.dart'; + } + + static String getChartDocumentationUrl(ChartType chartType) { + final chartDir = chartType.name.toLowerCase(); + return 'https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/${chartDir}_chart.md'; + } + + static String getVersionReleaseUrl(String version) => + '$flChartGithubUrl/releases/tag/$version'; +} diff --git a/example/lib/util/app_helper.dart b/example/lib/util/app_helper.dart new file mode 100644 index 0000000..02b29e8 --- /dev/null +++ b/example/lib/util/app_helper.dart @@ -0,0 +1,21 @@ +import 'package:fl_chart_app/presentation/resources/app_resources.dart'; +import 'package:fl_chart_app/urls.dart'; + +enum ChartType { line, bar, pie, scatter, radar, candlestick } + +extension ChartTypeExtension on ChartType { + String get displayName => '$simpleName Chart'; + + String get simpleName => switch (this) { + ChartType.line => 'Line', + ChartType.bar => 'Bar', + ChartType.pie => 'Pie', + ChartType.scatter => 'Scatter', + ChartType.radar => 'Radar', + ChartType.candlestick => 'Candlestick', + }; + + String get documentationUrl => Urls.getChartDocumentationUrl(this); + + String get assetIcon => AppAssets.getChartIcon(this); +} diff --git a/example/lib/util/app_utils.dart b/example/lib/util/app_utils.dart new file mode 100644 index 0000000..51e2095 --- /dev/null +++ b/example/lib/util/app_utils.dart @@ -0,0 +1,28 @@ +import 'dart:math' as math; + +import 'package:url_launcher/url_launcher.dart'; + +class AppUtils { + factory AppUtils() { + return _singleton; + } + + AppUtils._internal(); + static final AppUtils _singleton = AppUtils._internal(); + + double degreeToRadian(double degree) { + return degree * math.pi / 180; + } + + double radianToDegree(double radian) { + return radian * 180 / math.pi; + } + + Future tryToLaunchUrl(String url) async { + final uri = Uri.parse(url); + if (await canLaunchUrl(uri)) { + return await launchUrl(uri); + } + return false; + } +} diff --git a/example/lib/util/csv_parser.dart b/example/lib/util/csv_parser.dart new file mode 100644 index 0000000..58911b5 --- /dev/null +++ b/example/lib/util/csv_parser.dart @@ -0,0 +1,45 @@ +class CsvParser { + static List> parse(String rawCsvData) { + final lines = + rawCsvData.split('\n').where((line) => line.isNotEmpty).toList(); + final headers = _parseCsvLine(lines.first); + + return [ + headers, + ...lines.skip(1).map((line) => _parseCsvLine(line)), + ]; + } + + static List _parseCsvLine(String line) { + final values = []; + final buffer = StringBuffer(); + bool insideQuotes = false; + + for (int i = 0; i < line.length; i++) { + final char = line[i]; + + if (char == '"') { + if (insideQuotes && i + 1 < line.length && line[i + 1] == '"') { + // Handle escaped quotes + buffer.write('"'); + i++; // Skip the next quote + } else { + // Toggle the insideQuotes flag + insideQuotes = !insideQuotes; + } + } else if (char == ',' && !insideQuotes) { + // End of value + values.add(buffer.toString()); + buffer.clear(); + } else { + // Normal character + buffer.write(char); + } + } + + // Add the last value + values.add(buffer.toString()); + + return values; + } +} diff --git a/example/lib/util/device_info.dart b/example/lib/util/device_info.dart new file mode 100644 index 0000000..1a71495 --- /dev/null +++ b/example/lib/util/device_info.dart @@ -0,0 +1,46 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:universal_platform/universal_platform.dart'; + +enum FormFactorType { monitor, smallPhone, largePhone, tablet } + +// Copied from https://github.com/gskinnerTeam/flutter-folio/blob/main/lib/_utils/device_info.dart +class DeviceOS { + // Syntax sugar, proxy the UniversalPlatform methods so our views can reference a single class + static bool isIOS = UniversalPlatform.isIOS; + static bool isAndroid = UniversalPlatform.isAndroid; + static bool isMacOS = UniversalPlatform.isMacOS; + static bool isLinux = UniversalPlatform.isLinux; + static bool isWindows = UniversalPlatform.isWindows; + + // Higher level device class abstractions (more syntax sugar for the views) + static bool isWeb = kIsWeb; + static bool get isDesktop => isWindows || isMacOS || isLinux; + static bool get isMobile => isAndroid || isIOS; + static bool get isDesktopOrWeb => isDesktop || isWeb; + static bool get isMobileOrWeb => isMobile || isWeb; +} + +class DeviceScreen { + // Get the device form factor as best we can. + // Otherwise we will use the screen size to determine which class we fall into. + static FormFactorType get(BuildContext context) { + var shortestSide = MediaQuery.of(context).size.shortestSide; + if (shortestSide <= 300) return FormFactorType.smallPhone; + if (shortestSide <= 600) return FormFactorType.largePhone; + if (shortestSide <= 900) return FormFactorType.tablet; + return FormFactorType.monitor; + } + + // Shortcuts for various mobile device types + static bool isPhone(BuildContext context) => + isSmallPhone(context) || isLargePhone(context); + static bool isTablet(BuildContext context) => + get(context) == FormFactorType.tablet; + static bool isMonitor(BuildContext context) => + get(context) == FormFactorType.monitor; + static bool isSmallPhone(BuildContext context) => + get(context) == FormFactorType.smallPhone; + static bool isLargePhone(BuildContext context) => + get(context) == FormFactorType.largePhone; +} diff --git a/example/lib/util/extensions/color_extensions.dart b/example/lib/util/extensions/color_extensions.dart new file mode 100644 index 0000000..13539da --- /dev/null +++ b/example/lib/util/extensions/color_extensions.dart @@ -0,0 +1,43 @@ +import 'dart:ui'; + +extension ColorExtension on Color { + /// Convert the color to a darken color based on the [percent] + Color darken([int percent = 40]) { + assert(1 <= percent && percent <= 100); + final value = 1 - percent / 100; + return Color.fromARGB( + _floatToInt8(a), + (_floatToInt8(r) * value).round(), + (_floatToInt8(g) * value).round(), + (_floatToInt8(b) * value).round(), + ); + } + + Color lighten([int percent = 40]) { + assert(1 <= percent && percent <= 100); + final value = percent / 100; + return Color.fromARGB( + _floatToInt8(a), + (_floatToInt8(r) + ((255 - _floatToInt8(r)) * value)).round(), + (_floatToInt8(g) + ((255 - _floatToInt8(g)) * value)).round(), + (_floatToInt8(b) + ((255 - _floatToInt8(b)) * value)).round(), + ); + } + + Color avg(Color other) { + final red = (_floatToInt8(r) + _floatToInt8(other.r)) ~/ 2; + final green = (_floatToInt8(g) + _floatToInt8(other.g)) ~/ 2; + final blue = (_floatToInt8(b) + _floatToInt8(other.b)) ~/ 2; + final alpha = (_floatToInt8(a) + _floatToInt8(other.a)) ~/ 2; + return Color.fromARGB(alpha, red, green, blue); + } + + // Int color components were deprecated in Flutter 3.27.0. + // This method is used to convert the new double color components to the + // old int color components. + // + // Taken from the Color class. + int _floatToInt8(double x) { + return (x * 255.0).round() & 0xff; + } +} diff --git a/example/lib/util/extensions/iterable_extensions.dart b/example/lib/util/extensions/iterable_extensions.dart new file mode 100644 index 0000000..fe4598f --- /dev/null +++ b/example/lib/util/extensions/iterable_extensions.dart @@ -0,0 +1,3 @@ +extension IterableToMapExtension on Iterable> { + Map get asMap => Map.fromEntries(this); +} diff --git a/example/lib/util/extensions/list_extensions.dart b/example/lib/util/extensions/list_extensions.dart new file mode 100644 index 0000000..0073298 --- /dev/null +++ b/example/lib/util/extensions/list_extensions.dart @@ -0,0 +1,3 @@ +extension ListToMapExtension on List> { + Map get asMap => Map.fromEntries(this); +} diff --git a/example/linux/.gitignore b/example/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/example/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/example/linux/CMakeLists.txt b/example/linux/CMakeLists.txt new file mode 100644 index 0000000..228a43f --- /dev/null +++ b/example/linux/CMakeLists.txt @@ -0,0 +1,138 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "fLChartApp") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "dev.flchart.app") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/example/linux/flutter/CMakeLists.txt b/example/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/example/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/example/linux/flutter/generated_plugin_registrant.cc b/example/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..f6f23bf --- /dev/null +++ b/example/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); +} diff --git a/example/linux/flutter/generated_plugin_registrant.h b/example/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/example/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/example/linux/flutter/generated_plugins.cmake b/example/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..f16b4c3 --- /dev/null +++ b/example/linux/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_linux +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/example/linux/main.cc b/example/linux/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/example/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/example/linux/my_application.cc b/example/linux/my_application.cc new file mode 100644 index 0000000..235cbbb --- /dev/null +++ b/example/linux/my_application.cc @@ -0,0 +1,104 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "FL Chart App"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "FL Chart App"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/example/linux/my_application.h b/example/linux/my_application.h new file mode 100644 index 0000000..72271d5 --- /dev/null +++ b/example/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/example/macos/.gitignore b/example/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/example/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/example/macos/Flutter/Flutter-Debug.xcconfig b/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..4b81f9b --- /dev/null +++ b/example/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/example/macos/Flutter/Flutter-Release.xcconfig b/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..5caa9d1 --- /dev/null +++ b/example/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..9506405 --- /dev/null +++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,18 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import package_info_plus +import path_provider_foundation +import shared_preferences_foundation +import url_launcher_macos + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) +} diff --git a/example/macos/Podfile b/example/macos/Podfile new file mode 100644 index 0000000..049abe2 --- /dev/null +++ b/example/macos/Podfile @@ -0,0 +1,40 @@ +platform :osx, '10.14' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/example/macos/Podfile.lock b/example/macos/Podfile.lock new file mode 100644 index 0000000..d2d4353 --- /dev/null +++ b/example/macos/Podfile.lock @@ -0,0 +1,35 @@ +PODS: + - FlutterMacOS (1.0.0) + - package_info_plus (0.0.1): + - FlutterMacOS + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - url_launcher_macos (0.0.1): + - FlutterMacOS + +DEPENDENCIES: + - FlutterMacOS (from `Flutter/ephemeral`) + - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) + - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + +EXTERNAL SOURCES: + FlutterMacOS: + :path: Flutter/ephemeral + package_info_plus: + :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos + path_provider_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + +SPEC CHECKSUMS: + FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 + package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 + +PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7 + +COCOAPODS: 1.16.2 diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..d31292a --- /dev/null +++ b/example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,633 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + CD3C0E2CB86F97227A57A462 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0A6D7D2B0395281ED83347AF /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0A6D7D2B0395281ED83347AF /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 290F1FF0B02A72A883051281 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* fl_chart.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = fl_chart.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 4FD09C27857801C704F06757 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + A0C43ED3CC8F9FE13408EE45 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + CD3C0E2CB86F97227A57A462 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + F4A78FA3CEF98654E14A7A43 /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* fl_chart.app */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 0A6D7D2B0395281ED83347AF /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + F4A78FA3CEF98654E14A7A43 /* Pods */ = { + isa = PBXGroup; + children = ( + 290F1FF0B02A72A883051281 /* Pods-Runner.debug.xcconfig */, + 4FD09C27857801C704F06757 /* Pods-Runner.release.xcconfig */, + A0C43ED3CC8F9FE13408EE45 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 1D2964E42B7E6986BA4115FC /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 06400C1AB8DC605AC029909F /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* fl_chart.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 06400C1AB8DC605AC029909F /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 1D2964E42B7E6986BA4115FC /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..f3f56d1 --- /dev/null +++ b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/example/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/macos/Runner/AppDelegate.swift b/example/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..b3c1761 --- /dev/null +++ b/example/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..90e443f Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..baafe3a Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..3b1acbe Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..e0931cb Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..29f0f08 Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..e228403 Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..89a755f Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/example/macos/Runner/Base.lproj/MainMenu.xib b/example/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/example/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/macos/Runner/Configs/AppInfo.xcconfig b/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..485bf18 --- /dev/null +++ b/example/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = FL Chart App + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = dev.flchart.app.exampleNew + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2023 dev.flchart.app. All rights reserved. diff --git a/example/macos/Runner/Configs/Debug.xcconfig b/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/example/macos/Runner/Configs/Release.xcconfig b/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/example/macos/Runner/Configs/Warnings.xcconfig b/example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/example/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/example/macos/Runner/DebugProfile.entitlements b/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..c946719 --- /dev/null +++ b/example/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + com.apple.security.network.client + + + diff --git a/example/macos/Runner/Info.plist b/example/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/example/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/example/macos/Runner/MainFlutterWindow.swift b/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..2722837 --- /dev/null +++ b/example/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController.init() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/example/macos/Runner/Release.entitlements b/example/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/example/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/example/pubspec.yaml b/example/pubspec.yaml new file mode 100644 index 0000000..216b01c --- /dev/null +++ b/example/pubspec.yaml @@ -0,0 +1,43 @@ +name: fl_chart_app +description: FL Chart App is an application to demonstrate samples of the fl_chart (A Flutter package to draw charts). +publish_to: 'none' +version: 1.2.0+10200 + +environment: + sdk: ^3.0.0 +dependencies: + flutter: + sdk: flutter + flutter_web_plugins: + sdk: flutter + cupertino_icons: ^1.0.8 + google_fonts: ^6.2.1 + flutter_svg: ^2.0.10+1 + universal_platform: ^1.1.0 + flutter_staggered_grid_view: ^0.7.0 + url_launcher: ^6.3.0 + go_router: ^14.2.7 + dartx: ^1.2.0 + fl_chart: + path: ../ + flutter_bloc: ^8.1.6 + package_info_plus: ^8.0.2 + equatable: ^2.0.5 + intl: ^0.20.1 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^4.0.0 + +flutter: + uses-material-design: true + fonts: + - family: Digital + fonts: + - asset: assets/fonts/digital-7.ttf + + assets: + - assets/icons/ + - assets/fonts/ + - assets/data/ diff --git a/example/web/CNAME b/example/web/CNAME new file mode 100644 index 0000000..b35e756 --- /dev/null +++ b/example/web/CNAME @@ -0,0 +1 @@ +app.flchart.dev \ No newline at end of file diff --git a/example/web/favicon.png b/example/web/favicon.png new file mode 100644 index 0000000..164030f Binary files /dev/null and b/example/web/favicon.png differ diff --git a/example/web/icons/Icon-192.png b/example/web/icons/Icon-192.png new file mode 100644 index 0000000..67a9de0 Binary files /dev/null and b/example/web/icons/Icon-192.png differ diff --git a/example/web/icons/Icon-512.png b/example/web/icons/Icon-512.png new file mode 100644 index 0000000..08baf75 Binary files /dev/null and b/example/web/icons/Icon-512.png differ diff --git a/example/web/index.html b/example/web/index.html new file mode 100644 index 0000000..b094c6a --- /dev/null +++ b/example/web/index.html @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + FL Chart App + + + + + + +
+
+
+ + + + diff --git a/example/web/manifest.json b/example/web/manifest.json new file mode 100644 index 0000000..a3f018e --- /dev/null +++ b/example/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "FL Chart App", + "short_name": "FL Chart App", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/example/windows/.gitignore b/example/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/example/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/example/windows/CMakeLists.txt b/example/windows/CMakeLists.txt new file mode 100644 index 0000000..f26c610 --- /dev/null +++ b/example/windows/CMakeLists.txt @@ -0,0 +1,101 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(fl_chart_app LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "fl_chart_app") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/example/windows/flutter/CMakeLists.txt b/example/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/example/windows/flutter/generated_plugin_registrant.cc b/example/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..4f78848 --- /dev/null +++ b/example/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,14 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); +} diff --git a/example/windows/flutter/generated_plugin_registrant.h b/example/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/example/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/example/windows/flutter/generated_plugins.cmake b/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..88b22e5 --- /dev/null +++ b/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/example/windows/runner/CMakeLists.txt b/example/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..2041a04 --- /dev/null +++ b/example/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/example/windows/runner/Runner.rc b/example/windows/runner/Runner.rc new file mode 100644 index 0000000..67a802a --- /dev/null +++ b/example/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "dev.flchart.app" "\0" + VALUE "FileDescription", "FL Chart App" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "FL Chart App" "\0" + VALUE "LegalCopyright", "Copyright (C) 2023 dev.flchart.app. All rights reserved." "\0" + VALUE "OriginalFilename", "fl_chart_app.exe" "\0" + VALUE "ProductName", "fl_chart_app" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/example/windows/runner/flutter_window.cpp b/example/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..c819cb0 --- /dev/null +++ b/example/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/example/windows/runner/flutter_window.h b/example/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/example/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/example/windows/runner/main.cpp b/example/windows/runner/main.cpp new file mode 100644 index 0000000..7a1b020 --- /dev/null +++ b/example/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"FL Chart App", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/example/windows/runner/resource.h b/example/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/example/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/example/windows/runner/resources/app_icon.ico b/example/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/example/windows/runner/resources/app_icon.ico differ diff --git a/example/windows/runner/runner.exe.manifest b/example/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..a42ea76 --- /dev/null +++ b/example/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/example/windows/runner/utils.cpp b/example/windows/runner/utils.cpp new file mode 100644 index 0000000..f5bf9fa --- /dev/null +++ b/example/windows/runner/utils.cpp @@ -0,0 +1,64 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/example/windows/runner/utils.h b/example/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/example/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/example/windows/runner/win32_window.cpp b/example/windows/runner/win32_window.cpp new file mode 100644 index 0000000..2f91cbb --- /dev/null +++ b/example/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/example/windows/runner/win32_window.h b/example/windows/runner/win32_window.h new file mode 100644 index 0000000..90a7043 --- /dev/null +++ b/example/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/fl_chart.iml b/fl_chart.iml new file mode 100644 index 0000000..97ae688 --- /dev/null +++ b/fl_chart.iml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib/fl_chart.dart b/lib/fl_chart.dart new file mode 100644 index 0000000..efc2e1e --- /dev/null +++ b/lib/fl_chart.dart @@ -0,0 +1,18 @@ +export 'src/chart/bar_chart/bar_chart.dart'; +export 'src/chart/bar_chart/bar_chart_data.dart'; +export 'src/chart/base/axis_chart/axis_chart_data.dart'; +export 'src/chart/base/axis_chart/axis_chart_widgets.dart'; +export 'src/chart/base/axis_chart/scale_axis.dart'; +export 'src/chart/base/axis_chart/transformation_config.dart'; +export 'src/chart/base/base_chart/base_chart_data.dart'; +export 'src/chart/base/base_chart/fl_touch_event.dart'; +export 'src/chart/candlestick_chart/candlestick_chart.dart'; +export 'src/chart/candlestick_chart/candlestick_chart_data.dart'; +export 'src/chart/line_chart/line_chart.dart'; +export 'src/chart/line_chart/line_chart_data.dart'; +export 'src/chart/pie_chart/pie_chart.dart'; +export 'src/chart/pie_chart/pie_chart_data.dart'; +export 'src/chart/radar_chart/radar_chart.dart'; +export 'src/chart/radar_chart/radar_chart_data.dart'; +export 'src/chart/scatter_chart/scatter_chart.dart'; +export 'src/chart/scatter_chart/scatter_chart_data.dart'; diff --git a/lib/src/chart/bar_chart/bar_chart.dart b/lib/src/chart/bar_chart/bar_chart.dart new file mode 100644 index 0000000..a9c12f3 --- /dev/null +++ b/lib/src/chart/bar_chart/bar_chart.dart @@ -0,0 +1,168 @@ +import 'package:fl_chart/src/chart/bar_chart/bar_chart_data.dart'; +import 'package:fl_chart/src/chart/bar_chart/bar_chart_helper.dart'; +import 'package:fl_chart/src/chart/bar_chart/bar_chart_renderer.dart'; +import 'package:fl_chart/src/chart/base/axis_chart/axis_chart_scaffold_widget.dart'; +import 'package:fl_chart/src/chart/base/axis_chart/scale_axis.dart'; +import 'package:fl_chart/src/chart/base/axis_chart/transformation_config.dart'; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_data.dart'; +import 'package:fl_chart/src/chart/base/base_chart/fl_touch_event.dart'; +import 'package:flutter/cupertino.dart'; + +/// Renders a bar chart as a widget, using provided [BarChartData]. +class BarChart extends ImplicitlyAnimatedWidget { + /// [data] determines how the [BarChart] should be look like, + /// when you make any change in the [BarChartData], it updates + /// new values with animation, and duration is [duration]. + /// also you can change the [curve] + /// which default is [Curves.linear]. + BarChart( + this.data, { + this.chartRendererKey, + super.key, + @Deprecated('Please use [duration] instead') + Duration? swapAnimationDuration, + Duration duration = const Duration(milliseconds: 150), + @Deprecated('Please use [curve] instead') Curve? swapAnimationCurve, + Curve curve = Curves.linear, + this.transformationConfig = const FlTransformationConfig(), + }) : assert( + switch (data.alignment) { + BarChartAlignment.center || + BarChartAlignment.end || + BarChartAlignment.start => + transformationConfig.scaleAxis != FlScaleAxis.horizontal && + transformationConfig.scaleAxis != FlScaleAxis.free, + _ => true, + }, + 'Can not scale horizontally when BarChartAlignment is center, ' + 'end or start', + ), + super( + duration: swapAnimationDuration ?? duration, + curve: swapAnimationCurve ?? curve, + ); + + /// Determines how the [BarChart] should be look like. + final BarChartData data; + + /// {@macro fl_chart.AxisChartScaffoldWidget.transformationConfig} + final FlTransformationConfig transformationConfig; + + /// We pass this key to our renderers which are supposed to + /// render the chart itself (without anything around the chart). + final Key? chartRendererKey; + + /// Creates a [_BarChartState] + @override + _BarChartState createState() => _BarChartState(); +} + +class _BarChartState extends AnimatedWidgetBaseState { + /// we handle under the hood animations (implicit animations) via this tween, + /// it lerps between the old [BarChartData] to the new one. + BarChartDataTween? _barChartDataTween; + + /// If [BarTouchData.handleBuiltInTouches] is true, we override the callback to handle touches internally, + /// but we need to keep the provided callback to notify it too. + BaseTouchCallback? _providedTouchCallback; + + final Map> _showingTouchedTooltips = {}; + + final _barChartHelper = BarChartHelper(); + + @override + Widget build(BuildContext context) { + final showingData = _getData(); + + return AxisChartScaffoldWidget( + data: showingData, + transformationConfig: widget.transformationConfig, + chartBuilder: (context, chartVirtualRect) => BarChartLeaf( + data: _withTouchedIndicators(_barChartDataTween!.evaluate(animation)), + targetData: _withTouchedIndicators(showingData), + key: widget.chartRendererKey, + chartVirtualRect: chartVirtualRect, + canBeScaled: widget.transformationConfig.scaleAxis != FlScaleAxis.none, + ), + ); + } + + BarChartData _withTouchedIndicators(BarChartData barChartData) { + if (!barChartData.barTouchData.enabled || + !barChartData.barTouchData.handleBuiltInTouches) { + return barChartData; + } + + final newGroups = []; + for (var i = 0; i < barChartData.barGroups.length; i++) { + final group = barChartData.barGroups[i]; + + newGroups.add( + group.copyWith( + showingTooltipIndicators: _showingTouchedTooltips[i], + ), + ); + } + + return barChartData.copyWith( + barGroups: newGroups, + ); + } + + BarChartData _getData() { + var newData = widget.data; + if (newData.minY.isNaN || newData.maxY.isNaN) { + final (minY, maxY) = + _barChartHelper.calculateMaxAxisValues(newData.barGroups); + newData = newData.copyWith( + minY: newData.minY.isNaN ? minY : newData.minY, + maxY: newData.maxY.isNaN ? maxY : newData.maxY, + ); + } + + final barTouchData = newData.barTouchData; + if (barTouchData.enabled && barTouchData.handleBuiltInTouches) { + _providedTouchCallback = barTouchData.touchCallback; + return newData.copyWith( + barTouchData: + newData.barTouchData.copyWith(touchCallback: _handleBuiltInTouch), + ); + } + return newData; + } + + void _handleBuiltInTouch( + FlTouchEvent event, + BarTouchResponse? touchResponse, + ) { + if (!mounted) { + return; + } + _providedTouchCallback?.call(event, touchResponse); + + if (!event.isInterestedForInteractions || + touchResponse == null || + touchResponse.spot == null) { + setState(_showingTouchedTooltips.clear); + return; + } + setState(() { + final spot = touchResponse.spot!; + final groupIndex = spot.touchedBarGroupIndex; + final rodIndex = spot.touchedRodDataIndex; + + _showingTouchedTooltips.clear(); + _showingTouchedTooltips[groupIndex] = [rodIndex]; + }); + } + + @override + void forEachTween(TweenVisitor visitor) { + _barChartDataTween = visitor( + _barChartDataTween, + _getData(), + (dynamic value) => + BarChartDataTween(begin: value as BarChartData, end: widget.data), + ) as BarChartDataTween?; + } +} diff --git a/lib/src/chart/bar_chart/bar_chart_data.dart b/lib/src/chart/bar_chart/bar_chart_data.dart new file mode 100644 index 0000000..82e8752 --- /dev/null +++ b/lib/src/chart/bar_chart/bar_chart_data.dart @@ -0,0 +1,1020 @@ +// coverage:ignore-file +import 'dart:math'; +import 'dart:ui'; + +import 'package:equatable/equatable.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/extensions/color_extension.dart'; +import 'package:fl_chart/src/utils/lerp.dart'; +import 'package:fl_chart/src/utils/utils.dart'; +import 'package:flutter/material.dart'; + +/// [BarChart] needs this class to render itself. +/// +/// It holds data needed to draw a bar chart, +/// including bar lines, colors, spaces, touches, ... +class BarChartData extends AxisChartData with EquatableMixin { + /// [BarChart] draws some [barGroups] and aligns them using [alignment], + /// if [alignment] is [BarChartAlignment.center], you can define [groupsSpace] + /// to apply space between them. + /// + /// It draws some titles on left, top, right, bottom sides per each axis number, + /// you can modify [titlesData] to have your custom titles, + /// also you can define the axis title (one text per axis) for each side + /// using [axisTitleData], you can restrict the y axis using [minY], and [maxY] values. + /// + /// It draws a color as a background behind everything you can set it using [backgroundColor], + /// then a grid over it, you can customize it using [gridData], + /// and it draws 4 borders around your chart, you can customize it using [borderData]. + /// + /// You can annotate some regions with a highlight color using [rangeAnnotations]. + /// + /// You can modify [barTouchData] to customize touch behaviors and responses. + /// + /// Horizontal lines are drawn with [extraLinesData]. Vertical lines will not be painted if received. + /// Please see issue #1149 (https://github.com/imaNNeo/fl_chart/issues/1149) for vertical lines. + BarChartData({ + List? barGroups, + double? groupsSpace, + BarChartAlignment? alignment, + FlTitlesData? titlesData, + BarTouchData? barTouchData, + double? maxY, + double? minY, + super.baselineY, + FlGridData? gridData, + super.borderData, + RangeAnnotations? rangeAnnotations, + super.backgroundColor, + ExtraLinesData? extraLinesData, + super.rotationQuarterTurns, + this.errorIndicatorData = const FlErrorIndicatorData(), + }) : barGroups = barGroups ?? const [], + groupsSpace = groupsSpace ?? 16, + alignment = alignment ?? BarChartAlignment.spaceEvenly, + barTouchData = barTouchData ?? const BarTouchData(), + super( + titlesData: titlesData ?? + const FlTitlesData( + topTitles: AxisTitles(), + ), + gridData: gridData ?? const FlGridData(), + rangeAnnotations: rangeAnnotations ?? const RangeAnnotations(), + extraLinesData: extraLinesData ?? const ExtraLinesData(), + minX: 0, + maxX: 1, + maxY: maxY ?? double.nan, + minY: minY ?? double.nan, + ); + + /// [BarChart] draws [barGroups] that each of them contains a list of [BarChartRodData]. + final List barGroups; + + /// Apply space between the [barGroups]. + final double groupsSpace; + + /// Arrange the [barGroups], see [BarChartAlignment]. + final BarChartAlignment alignment; + + /// Handles touch behaviors and responses. + final BarTouchData barTouchData; + + /// Holds data for showing error (threshold) indicators on the spots in + /// the different [BarChartGroupData.barRods] + final FlErrorIndicatorData + errorIndicatorData; + + /// Copies current [BarChartData] to a new [BarChartData], + /// and replaces provided values. + BarChartData copyWith({ + List? barGroups, + double? groupsSpace, + BarChartAlignment? alignment, + FlTitlesData? titlesData, + RangeAnnotations? rangeAnnotations, + BarTouchData? barTouchData, + FlGridData? gridData, + FlBorderData? borderData, + double? maxY, + double? minY, + double? baselineY, + Color? backgroundColor, + ExtraLinesData? extraLinesData, + int? rotationQuarterTurns, + FlErrorIndicatorData? + errorIndicatorData, + }) => + BarChartData( + barGroups: barGroups ?? this.barGroups, + groupsSpace: groupsSpace ?? this.groupsSpace, + alignment: alignment ?? this.alignment, + titlesData: titlesData ?? this.titlesData, + rangeAnnotations: rangeAnnotations ?? this.rangeAnnotations, + barTouchData: barTouchData ?? this.barTouchData, + gridData: gridData ?? this.gridData, + borderData: borderData ?? this.borderData, + maxY: maxY ?? this.maxY, + minY: minY ?? this.minY, + baselineY: baselineY ?? this.baselineY, + backgroundColor: backgroundColor ?? this.backgroundColor, + extraLinesData: extraLinesData ?? this.extraLinesData, + rotationQuarterTurns: rotationQuarterTurns ?? this.rotationQuarterTurns, + errorIndicatorData: errorIndicatorData ?? this.errorIndicatorData, + ); + + /// Lerps a [BaseChartData] based on [t] value, check [Tween.lerp]. + @override + BarChartData lerp(BaseChartData a, BaseChartData b, double t) { + if (a is BarChartData && b is BarChartData) { + return BarChartData( + barGroups: lerpBarChartGroupDataList(a.barGroups, b.barGroups, t), + groupsSpace: lerpDouble(a.groupsSpace, b.groupsSpace, t), + alignment: b.alignment, + titlesData: FlTitlesData.lerp(a.titlesData, b.titlesData, t), + rangeAnnotations: + RangeAnnotations.lerp(a.rangeAnnotations, b.rangeAnnotations, t), + barTouchData: b.barTouchData, + gridData: FlGridData.lerp(a.gridData, b.gridData, t), + borderData: FlBorderData.lerp(a.borderData, b.borderData, t), + maxY: lerpDouble(a.maxY, b.maxY, t), + minY: lerpDouble(a.minY, b.minY, t), + baselineY: lerpDouble(a.baselineY, b.baselineY, t), + backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), + extraLinesData: + ExtraLinesData.lerp(a.extraLinesData, b.extraLinesData, t), + rotationQuarterTurns: b.rotationQuarterTurns, + errorIndicatorData: FlErrorIndicatorData.lerp( + a.errorIndicatorData, + b.errorIndicatorData, + t, + ), + ); + } else { + throw Exception('Illegal State'); + } + } + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + barGroups, + groupsSpace, + alignment, + titlesData, + barTouchData, + maxY, + minY, + baselineY, + gridData, + borderData, + rangeAnnotations, + backgroundColor, + extraLinesData, + rotationQuarterTurns, + errorIndicatorData, + ]; +} + +/// defines arrangement of [barGroups], check [MainAxisAlignment] for more details. +enum BarChartAlignment { + start, + end, + center, + spaceEvenly, + spaceAround, + spaceBetween, +} + +/// Represents a group of rods (or bars) inside the [BarChart]. +/// +/// in the [BarChart] we have some rods, they can be grouped or not, +/// if you want to have grouped bars, simply put them in each group, +/// otherwise just pass one of them in each group. +class BarChartGroupData with EquatableMixin { + /// [BarChart] renders groups, and arrange them using [alignment], + /// [x] value defines the group's value in the x axis (set them incrementally). + /// it renders a list of [BarChartRodData] that represents a rod (or a bar) in the bar chart, + /// and applies [barsSpace] between them. + /// + /// you can show some tooltipIndicators (a popup with an information) + /// on top of each [BarChartRodData] using [showingTooltipIndicators], + /// just put indices you want to show it on top of them. + BarChartGroupData({ + required this.x, + bool? groupVertically, + List? barRods, + double? barsSpace, + List? showingTooltipIndicators, + }) : groupVertically = groupVertically ?? false, + barRods = barRods ?? const [], + barsSpace = barsSpace ?? 2, + showingTooltipIndicators = showingTooltipIndicators ?? const []; + + /// Order along the x axis in which titles, and titles only, will be shown. + /// + /// Note [x] does not reorder bars from [barRods]; instead, it gets the title + /// in [x] position through [SideTitles.getTitlesWidget] function. + @required + final int x; + + /// If set true, it will show bars below/above each other. + /// Otherwise, it will show bars beside each other. + final bool groupVertically; + + /// [BarChart] renders [barRods] that represents a rod (or a bar) in the bar chart. + final List barRods; + + /// [BarChart] applies [barsSpace] between [barRods] if [groupVertically] is false. + final double barsSpace; + + /// you can show some tooltipIndicators (a popup with an information) + /// on top of each [BarChartRodData] using [showingTooltipIndicators], + /// just put indices you want to show it on top of them. + /// + /// An important point is that you have to disable the default touch behaviour + /// to show the tooltip manually, see [BarTouchData.handleBuiltInTouches]. + final List showingTooltipIndicators; + + /// width of the group (sum of all [BarChartRodData]'s width and spaces) + double get width { + if (barRods.isEmpty) { + return 0; + } + + if (groupVertically) { + return barRods.map((rodData) => rodData.width).reduce(max); + } else { + final sumWidth = barRods + .map((rodData) => rodData.width) + .reduce((first, second) => first + second); + final spaces = (barRods.length - 1) * barsSpace; + + return sumWidth + spaces; + } + } + + /// Copies current [BarChartGroupData] to a new [BarChartGroupData], + /// and replaces provided values. + BarChartGroupData copyWith({ + int? x, + bool? groupVertically, + List? barRods, + double? barsSpace, + List? showingTooltipIndicators, + }) => + BarChartGroupData( + x: x ?? this.x, + groupVertically: groupVertically ?? this.groupVertically, + barRods: barRods ?? this.barRods, + barsSpace: barsSpace ?? this.barsSpace, + showingTooltipIndicators: + showingTooltipIndicators ?? this.showingTooltipIndicators, + ); + + /// Lerps a [BarChartGroupData] based on [t] value, check [Tween.lerp]. + static BarChartGroupData lerp( + BarChartGroupData a, + BarChartGroupData b, + double t, + ) => + BarChartGroupData( + x: (a.x + (b.x - a.x) * t).round(), + groupVertically: b.groupVertically, + barRods: lerpBarChartRodDataList(a.barRods, b.barRods, t), + barsSpace: lerpDouble(a.barsSpace, b.barsSpace, t), + showingTooltipIndicators: lerpIntList( + a.showingTooltipIndicators, + b.showingTooltipIndicators, + t, + ), + ); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + x, + groupVertically, + barRods, + barsSpace, + showingTooltipIndicators, + ]; +} + +/// Holds data about rendering each rod (or bar) in the [BarChart]. +class BarChartRodData with EquatableMixin { + /// [BarChart] renders rods vertically from zero to [toY], + /// and the x is equivalent to the [BarChartGroupData.x] value. + /// + /// It renders each rod using [color], [width], and [borderRadius] for rounding corners and also [borderSide] for stroke border. + /// Optionally you can use [borderDashArray] if you want your borders to have dashed lines. + /// + /// This bar draws with provided [color] or [gradient]. + /// You must provide one of them. + /// + /// If you want to have a bar drawn in rear of this rod, use [backDrawRodData], + /// it uses to have a bar with a passive color in rear of the rod, + /// for example you can use it as the maximum value place holder. + /// + /// If you are a fan of stacked charts (If you don't know what is it, google it), + /// you can fill up the [rodStackItems] to have a Stacked Chart. + /// for example if you want to have a Stacked Chart with three colors: + /// ```dart + /// BarChartRodData( + /// y: 9, + /// color: Colors.grey, + /// rodStackItems: [ + /// BarChartRodStackItem(0, 3, Colors.red), + /// BarChartRodStackItem(3, 6, Colors.green), + /// BarChartRodStackItem(6, 9, Colors.blue), + /// ] + /// ) + /// ``` + BarChartRodData({ + double? fromY, + required this.toY, + this.toYErrorRange, + Color? color, + this.gradient, + double? width, + BorderRadius? borderRadius, + this.borderDashArray, + BorderSide? borderSide, + BackgroundBarChartRodData? backDrawRodData, + List? rodStackItems, + }) : fromY = fromY ?? 0, + color = + color ?? ((color == null && gradient == null) ? Colors.cyan : null), + width = width ?? 8, + borderRadius = Utils().normalizeBorderRadius(borderRadius, width ?? 8), + borderSide = Utils().normalizeBorderSide(borderSide, width ?? 8), + backDrawRodData = backDrawRodData ?? BackgroundBarChartRodData(), + rodStackItems = rodStackItems ?? const []; + + /// [BarChart] renders rods vertically from [fromY]. + final double fromY; + + /// [BarChart] renders rods vertically from [fromY] to [toY]. + final double toY; + + /// If the data has error range/threshold, it will be rendered + /// with this error range. So you can provide the + /// [FlErrorRange.lowerBy] and [FlErrorRange.upperBy] that is relative to + /// the [toY] property. + /// + /// If you want to customize the visual representation of the error range, + /// you can use [BarChartData.errorIndicatorData] to customize the error range + final FlErrorRange? toYErrorRange; + + /// If provided, this [BarChartRodData] draws with this [color] + /// Otherwise we use [gradient] to draw the background. + /// It throws an exception if you provide both [color] and [gradient] + final Color? color; + + /// If provided, this [BarChartRodData] draws with this [gradient]. + /// Otherwise we use [color] to draw the background. + /// It throws an exception if you provide both [color] and [gradient] + final Gradient? gradient; + + /// [BarChart] renders each rods with this value. + final double width; + + /// If you want to have a rounded rod, set this value. + final BorderRadius? borderRadius; + + /// If you want to have dashed border, set this value. + final List? borderDashArray; + + /// If you want to have a border for rod, set this value. + final BorderSide borderSide; + + /// If you want to have a bar drawn in rear of this rod, use [backDrawRodData], + /// it uses to have a bar with a passive color in rear of the rod, + /// for example you can use it as the maximum value place holder. + final BackgroundBarChartRodData backDrawRodData; + + /// If you are a fan of stacked charts (If you don't know what is it, google it), + /// you can fill up the [rodStackItems] to have a Stacked Chart. + final List rodStackItems; + + /// Determines the upward or downward direction + bool isUpward() => toY >= fromY; + + /// Copies current [BarChartRodData] to a new [BarChartRodData], + /// and replaces provided values. + BarChartRodData copyWith({ + double? fromY, + double? toY, + FlErrorRange? toYErrorRange, + Color? color, + Gradient? gradient, + double? width, + BorderRadius? borderRadius, + List? dashArray, + BorderSide? borderSide, + BackgroundBarChartRodData? backDrawRodData, + List? rodStackItems, + }) => + BarChartRodData( + fromY: fromY ?? this.fromY, + toY: toY ?? this.toY, + toYErrorRange: toYErrorRange ?? this.toYErrorRange, + color: color ?? this.color, + gradient: gradient ?? this.gradient, + width: width ?? this.width, + borderRadius: borderRadius ?? this.borderRadius, + borderDashArray: borderDashArray, + borderSide: borderSide ?? this.borderSide, + backDrawRodData: backDrawRodData ?? this.backDrawRodData, + rodStackItems: rodStackItems ?? this.rodStackItems, + ); + + /// Lerps a [BarChartRodData] based on [t] value, check [Tween.lerp]. + static BarChartRodData lerp(BarChartRodData a, BarChartRodData b, double t) => + BarChartRodData( + gradient: Gradient.lerp(a.gradient, b.gradient, t), + color: Color.lerp(a.color, b.color, t), + width: lerpDouble(a.width, b.width, t), + borderRadius: BorderRadius.lerp(a.borderRadius, b.borderRadius, t), + borderDashArray: lerpIntList(a.borderDashArray, b.borderDashArray, t), + borderSide: BorderSide.lerp(a.borderSide, b.borderSide, t), + fromY: lerpDouble(a.fromY, b.fromY, t), + toY: lerpDouble(a.toY, b.toY, t)!, + toYErrorRange: FlErrorRange.lerp(a.toYErrorRange, b.toYErrorRange, t), + backDrawRodData: BackgroundBarChartRodData.lerp( + a.backDrawRodData, + b.backDrawRodData, + t, + ), + rodStackItems: + lerpBarChartRodStackList(a.rodStackItems, b.rodStackItems, t), + ); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + fromY, + toY, + toYErrorRange, + width, + borderRadius, + borderDashArray, + borderSide, + backDrawRodData, + rodStackItems, + color, + gradient, + ]; +} + +/// A colored section of Stacked Chart rod item +/// +/// Each [BarChartRodData] can have a list of [BarChartRodStackItem] (with different colors +/// and position) to represent a Stacked Chart rod, +class BarChartRodStackItem with EquatableMixin { + /// Renders a section of Stacked Chart from [fromY] to [toY] with [color] + /// for example if you want to have a Stacked Chart with three colors: + /// ```dart + /// BarChartRodData( + /// y: 9, + /// color: Colors.grey, + /// rodStackItems: [ + /// BarChartRodStackItem(0, 3, Colors.red), + /// BarChartRodStackItem(3, 6, Colors.green), + /// BarChartRodStackItem(6, 9, Colors.blue), + /// ] + /// ) + /// ``` + BarChartRodStackItem( + this.fromY, + this.toY, + this.color, [ + this.borderSide = Utils.defaultBorderSide, + ]); + + /// Renders a Stacked Chart section from [fromY] + final double fromY; + + /// Renders a Stacked Chart section to [toY] + final double toY; + + /// Renders a Stacked Chart section with [color] + final Color color; + + /// Renders border stroke for a Stacked Chart section + final BorderSide borderSide; + + /// Copies current [BarChartRodStackItem] to a new [BarChartRodStackItem], + /// and replaces provided values. + BarChartRodStackItem copyWith({ + double? fromY, + double? toY, + Color? color, + BorderSide? borderSide, + }) => + BarChartRodStackItem( + fromY ?? this.fromY, + toY ?? this.toY, + color ?? this.color, + borderSide ?? this.borderSide, + ); + + /// Lerps a [BarChartRodStackItem] based on [t] value, check [Tween.lerp]. + static BarChartRodStackItem lerp( + BarChartRodStackItem a, + BarChartRodStackItem b, + double t, + ) => + BarChartRodStackItem( + lerpDouble(a.fromY, b.fromY, t)!, + lerpDouble(a.toY, b.toY, t)!, + Color.lerp(a.color, b.color, t)!, + BorderSide.lerp(a.borderSide, b.borderSide, t), + ); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [fromY, toY, color, borderSide]; +} + +/// Holds values to draw a rod in rear of the main rod. +/// +/// If you want to have a bar drawn in rear of the main rod, use [BarChartRodData.backDrawRodData], +/// it uses to have a bar with a passive color in rear of the rod, +/// for example you can use it as the maximum value place holder in rear of your rod. +class BackgroundBarChartRodData with EquatableMixin { + /// It will be rendered in rear of the main rod, + /// background starts to show from [fromY] to [toY], + /// It draws with [color] or [gradient]. You must provide one of them, + /// you prevent to show it, using [show] property. + BackgroundBarChartRodData({ + double? fromY, + double? toY, + bool? show, + Color? color, + this.gradient, + }) : fromY = fromY ?? 0, + toY = toY ?? 0, + show = show ?? false, + color = color ?? + ((color == null && gradient == null) ? Colors.blueGrey : null); + + /// Determines to show or hide this + final bool show; + + /// [fromY] is where background starts to show + final double fromY; + + /// background starts to show from [fromY] to [toY] + final double toY; + + /// If provided, Background draws with this [color] + /// Otherwise we use [gradient] to draw the background. + /// It throws an exception if you provide both [color] and [gradient] + final Color? color; + + /// If provided, background draws with this [gradient]. + /// Otherwise we use [color] to draw the background. + /// It throws an exception if you provide both [color] and [gradient] + final Gradient? gradient; + + /// Lerps a [BackgroundBarChartRodData] based on [t] value, check [Tween.lerp]. + static BackgroundBarChartRodData lerp( + BackgroundBarChartRodData a, + BackgroundBarChartRodData b, + double t, + ) => + BackgroundBarChartRodData( + fromY: lerpDouble(a.fromY, b.fromY, t), + toY: lerpDouble(a.toY, b.toY, t), + color: Color.lerp(a.color, b.color, t), + gradient: Gradient.lerp(a.gradient, b.gradient, t), + show: b.show, + ); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + show, + fromY, + toY, + color, + gradient, + ]; +} + +/// Holds data to handle touch events, and touch responses in the [BarChart]. +/// +/// There is a touch flow, explained [here](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/handle_touches.md) +/// in a simple way, each chart's renderer captures the touch events, and passes the pointerEvent +/// to the painter, and gets touched spot, and wraps it into a concrete [BarTouchResponse]. +class BarTouchData extends FlTouchData with EquatableMixin { + /// You can disable or enable the touch system using [enabled] flag, + /// + /// [touchCallback] notifies you about the happened touch/pointer events. + /// It gives you a [FlTouchEvent] which is the happened event such as [FlPointerHoverEvent], [FlTapUpEvent], ... + /// It also gives you a [BarTouchResponse] which contains information + /// about the elements that has touched. + /// + /// Using [mouseCursorResolver] you can change the mouse cursor + /// based on the provided [FlTouchEvent] and [BarTouchResponse] + /// + /// if [handleBuiltInTouches] is true, [BarChart] shows a tooltip popup on top of the bars if + /// touch occurs (or you can show it manually using, [BarChartGroupData.showingTooltipIndicators]), + /// You can customize this tooltip using [touchTooltipData]. + /// If you need to have a distance threshold for handling touches, use [touchExtraThreshold]. + /// If [allowTouchBarBackDraw] sets to true, touches will work + /// on [BarChartRodData.backDrawRodData] too (by default it only works on the main rods). + const BarTouchData({ + bool? enabled, + BaseTouchCallback? touchCallback, + MouseCursorResolver? mouseCursorResolver, + Duration? longPressDuration, + BarTouchTooltipData? touchTooltipData, + EdgeInsets? touchExtraThreshold, + bool? allowTouchBarBackDraw, + bool? handleBuiltInTouches, + }) : touchTooltipData = touchTooltipData ?? const BarTouchTooltipData(), + touchExtraThreshold = touchExtraThreshold ?? const EdgeInsets.all(4), + allowTouchBarBackDraw = allowTouchBarBackDraw ?? false, + handleBuiltInTouches = handleBuiltInTouches ?? true, + super( + enabled ?? true, + touchCallback, + mouseCursorResolver, + longPressDuration, + ); + + /// Configs of how touch tooltip popup. + final BarTouchTooltipData touchTooltipData; + + /// Distance threshold to handle the touch event. + final EdgeInsets touchExtraThreshold; + + /// Determines to handle touches on the back draw bar. + final bool allowTouchBarBackDraw; + + /// Determines to handle default built-in touch responses, + /// [BarTouchResponse] shows a tooltip popup above the touched spot. + final bool handleBuiltInTouches; + + /// Copies current [BarTouchData] to a new [BarTouchData], + /// and replaces provided values. + BarTouchData copyWith({ + bool? enabled, + BaseTouchCallback? touchCallback, + MouseCursorResolver? mouseCursorResolver, + Duration? longPressDuration, + BarTouchTooltipData? touchTooltipData, + EdgeInsets? touchExtraThreshold, + bool? allowTouchBarBackDraw, + bool? handleBuiltInTouches, + }) => + BarTouchData( + enabled: enabled ?? this.enabled, + touchCallback: touchCallback ?? this.touchCallback, + mouseCursorResolver: mouseCursorResolver ?? this.mouseCursorResolver, + longPressDuration: longPressDuration ?? this.longPressDuration, + touchTooltipData: touchTooltipData ?? this.touchTooltipData, + touchExtraThreshold: touchExtraThreshold ?? this.touchExtraThreshold, + allowTouchBarBackDraw: + allowTouchBarBackDraw ?? this.allowTouchBarBackDraw, + handleBuiltInTouches: handleBuiltInTouches ?? this.handleBuiltInTouches, + ); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + enabled, + touchCallback, + mouseCursorResolver, + longPressDuration, + touchTooltipData, + touchExtraThreshold, + allowTouchBarBackDraw, + handleBuiltInTouches, + ]; +} + +/// Controls showing tooltip on top or bottom. +enum TooltipDirection { + /// Tooltip shows on top if value is positive, on bottom if value is negative. + auto, + + /// Tooltip always shows on top. + top, + + /// Tooltip always shows on bottom. + bottom, +} + +/// Holds representation data for showing tooltip popup on top of rods. +class BarTouchTooltipData with EquatableMixin { + /// if [BarTouchData.handleBuiltInTouches] is true, + /// [BarChart] shows a tooltip popup on top of rods automatically when touch happens, + /// otherwise you can show it manually using [BarChartGroupData.showingTooltipIndicators]. + /// Tooltip shows on top of rods, with [getTooltipColor] as a background color. + /// You can set the corner radius using [tooltipBorderRadius], + /// If you want to have a padding inside the tooltip, fill [tooltipPadding], + /// or If you want to have a bottom margin, set [tooltipMargin]. + /// Content of the tooltip will provide using [getTooltipItem] callback, you can override it + /// and pass your custom data to show in the tooltip. + /// You can restrict the tooltip's width using [maxContentWidth]. + /// Sometimes, [BarChart] shows the tooltip outside of the chart, + /// you can set [fitInsideHorizontally] true to force it to shift inside the chart horizontally, + /// also you can set [fitInsideVertically] true to force it to shift inside the chart vertically. + const BarTouchTooltipData({ + BorderRadius? tooltipBorderRadius, + EdgeInsets? tooltipPadding, + double? tooltipMargin, + FLHorizontalAlignment? tooltipHorizontalAlignment, + double? tooltipHorizontalOffset, + double? maxContentWidth, + GetBarTooltipItem? getTooltipItem, + GetBarTooltipColor? getTooltipColor, + bool? fitInsideHorizontally, + bool? fitInsideVertically, + TooltipDirection? direction, + double? rotateAngle, + BorderSide? tooltipBorder, + }) : _tooltipBorderRadius = tooltipBorderRadius, + tooltipPadding = tooltipPadding ?? + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + tooltipMargin = tooltipMargin ?? 16, + tooltipHorizontalAlignment = + tooltipHorizontalAlignment ?? FLHorizontalAlignment.center, + tooltipHorizontalOffset = tooltipHorizontalOffset ?? 0, + maxContentWidth = maxContentWidth ?? 120, + getTooltipItem = getTooltipItem ?? defaultBarTooltipItem, + getTooltipColor = getTooltipColor ?? defaultBarTooltipColor, + fitInsideHorizontally = fitInsideHorizontally ?? false, + fitInsideVertically = fitInsideVertically ?? false, + direction = direction ?? TooltipDirection.auto, + rotateAngle = rotateAngle ?? 0.0, + tooltipBorder = tooltipBorder ?? BorderSide.none, + super(); + + /// Sets a rounded radius for the tooltip. + final BorderRadius? _tooltipBorderRadius; + + /// Sets a rounded radius for the tooltip. + BorderRadius get tooltipBorderRadius => + _tooltipBorderRadius ?? BorderRadius.circular(4); + + /// Applies a padding for showing contents inside the tooltip. + final EdgeInsets tooltipPadding; + + /// Applies a bottom margin for showing tooltip on top of rods. + final double tooltipMargin; + + /// Controls showing tooltip on left side, right side or center aligned with rod, default is center + final FLHorizontalAlignment tooltipHorizontalAlignment; + + /// Applies horizontal offset for showing tooltip, default is zero. + final double tooltipHorizontalOffset; + + /// Restricts the tooltip's width. + final double maxContentWidth; + + /// Retrieves data for showing content inside the tooltip. + final GetBarTooltipItem getTooltipItem; + + /// Forces the tooltip to shift horizontally inside the chart, if overflow happens. + final bool fitInsideHorizontally; + + /// Forces the tooltip to shift vertically inside the chart, if overflow happens. + final bool fitInsideVertically; + + /// Controls showing tooltip on top or bottom, default is auto. + final TooltipDirection direction; + + /// Controls the rotation of the tooltip (in degrees) + final double rotateAngle; + + /// The tooltip border color. + final BorderSide tooltipBorder; + + /// Retrieves data for setting background color of the tooltip. + final GetBarTooltipColor getTooltipColor; + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + _tooltipBorderRadius, + tooltipPadding, + tooltipMargin, + tooltipHorizontalAlignment, + tooltipHorizontalOffset, + maxContentWidth, + getTooltipItem, + fitInsideHorizontally, + fitInsideVertically, + rotateAngle, + tooltipBorder, + getTooltipColor, + ]; +} + +/// Provides a [BarTooltipItem] for showing content inside the [BarTouchTooltipData]. +/// +/// You can override [BarTouchTooltipData.getTooltipItem], it gives you +/// [group], [groupIndex], [rod], and [rodIndex] that touch happened on, +/// then you should and pass your custom [BarTooltipItem] to show inside the tooltip popup. +typedef GetBarTooltipItem = BarTooltipItem? Function( + BarChartGroupData group, + int groupIndex, + BarChartRodData rod, + int rodIndex, +); + +/// Default implementation for [BarTouchTooltipData.getTooltipItem]. +BarTooltipItem? defaultBarTooltipItem( + BarChartGroupData group, + int groupIndex, + BarChartRodData rod, + int rodIndex, +) { + final color = rod.gradient?.colors.first ?? rod.color; + final textStyle = TextStyle( + color: color, + fontWeight: FontWeight.bold, + fontSize: 14, + ); + return BarTooltipItem(rod.toY.toString(), textStyle); +} + +/// Holds data needed for showing custom tooltip content. +class BarTooltipItem with EquatableMixin { + /// content of the tooltip, is a [text] String with a [textStyle], + /// [textDirection] and optional [children]. + BarTooltipItem( + this.text, + this.textStyle, { + this.textAlign = TextAlign.center, + this.textDirection = TextDirection.ltr, + this.children, + }); + + /// Text of the content. + final String text; + + /// TextStyle of the showing content. + final TextStyle textStyle; + + /// TextAlign of the showing content. + final TextAlign textAlign; + + /// Direction of showing text. + final TextDirection textDirection; + + /// Add further style and format to the text of the tooltip + final List? children; + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + text, + textStyle, + textAlign, + textDirection, + children, + ]; +} + +//// Provides a [Color] to show different background color for each rod +/// +/// You can override [BarTouchTooltipData.getTooltipColor], it gives you +/// [group] that touch happened on, then you should and pass your custom [Color] to set background color +/// of tooltip popup. +typedef GetBarTooltipColor = Color Function( + BarChartGroupData group, +); + +/// Default implementation for [BarTouchTooltipData.getTooltipColor]. +Color defaultBarTooltipColor(BarChartGroupData group) => + Colors.blueGrey.darken(15); + +/// Holds information about touch response in the [BarChart]. +/// +/// You can override [BarTouchData.touchCallback] to handle touch events, +/// it gives you a [BarTouchResponse] and you can do whatever you want. +class BarTouchResponse extends AxisBaseTouchResponse { + /// If touch happens, [BarChart] processes it internally and passes out a BarTouchedSpot + /// that contains a [spot], it gives you information about the touched spot. + BarTouchResponse({ + required super.touchLocation, + required super.touchChartCoordinate, + required this.spot, + }); + + /// Gives information about the touched spot + final BarTouchedSpot? spot; + + /// Copies current [BarTouchResponse] to a new [BarTouchResponse], + /// and replaces provided values. + BarTouchResponse copyWith({ + Offset? touchLocation, + Offset? touchChartCoordinate, + BarTouchedSpot? spot, + }) => + BarTouchResponse( + touchLocation: touchLocation ?? this.touchLocation, + touchChartCoordinate: touchChartCoordinate ?? this.touchChartCoordinate, + spot: spot ?? this.spot, + ); +} + +/// It gives you information about the touched spot. +class BarTouchedSpot extends TouchedSpot with EquatableMixin { + /// When touch happens, a [BarTouchedSpot] returns as a output, + /// it tells you where the touch happened. + /// [touchedBarGroup], and [touchedBarGroupIndex] tell you in which group touch happened, + /// [touchedRodData], and [touchedRodDataIndex] tell you in which rod touch happened, + /// [touchedStackItem], and [touchedStackItemIndex] tell you in which rod stack touch happened + /// ([touchedStackItemIndex] means nothing found). + /// You can also have the touched x and y in the chart as a [FlSpot] using [spot] value, + /// and you can have the local touch coordinates on the screen as a [Offset] using [offset] value. + BarTouchedSpot( + this.touchedBarGroup, + this.touchedBarGroupIndex, + this.touchedRodData, + this.touchedRodDataIndex, + this.touchedStackItem, + this.touchedStackItemIndex, + FlSpot spot, + Offset offset, + ) : super(spot, offset); + final BarChartGroupData touchedBarGroup; + final int touchedBarGroupIndex; + + final BarChartRodData touchedRodData; + final int touchedRodDataIndex; + + /// It can be null, if nothing found + final BarChartRodStackItem? touchedStackItem; + + /// It can be -1, if nothing found + final int touchedStackItemIndex; + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + touchedBarGroup, + touchedBarGroupIndex, + touchedRodData, + touchedRodDataIndex, + touchedStackItem, + touchedStackItemIndex, + spot, + offset, + ]; +} + +/// It is the input of the [GetSpotRangeErrorPainter] callback in +/// the [BarChartData.errorIndicatorData] +/// +/// As you see, we have some properties that are related to each individual +/// rod (the object we show the error range on top of it). +/// For example, +/// [group] is the group that the rod belongs to, +/// [groupIndex] is the index of the group, +/// [rod] is the rod that the error range belongs to, +/// [barRodIndex] is the index of the rod in the group. +class BarChartSpotErrorRangeCallbackInput + extends FlSpotErrorRangeCallbackInput { + BarChartSpotErrorRangeCallbackInput({ + required this.group, + required this.groupIndex, + required this.rod, + required this.barRodIndex, + }); + + // The group that the rod belongs to + final BarChartGroupData group; + + // The index of the group that the rod belongs to + final int groupIndex; + + // The rod that the error range belongs to + final BarChartRodData rod; + + // The index of the rod in the group + final int barRodIndex; + + @override + List get props => [ + group, + groupIndex, + rod, + barRodIndex, + ]; +} + +/// It lerps a [BarChartData] to another [BarChartData] (handles animation for updating values) +class BarChartDataTween extends Tween { + BarChartDataTween({required BarChartData begin, required BarChartData end}) + : super(begin: begin, end: end); + + /// Lerps a [BarChartData] based on [t] value, check [Tween.lerp]. + @override + BarChartData lerp(double t) => begin!.lerp(begin!, end!, t); +} diff --git a/lib/src/chart/bar_chart/bar_chart_helper.dart b/lib/src/chart/bar_chart/bar_chart_helper.dart new file mode 100644 index 0000000..0688f72 --- /dev/null +++ b/lib/src/chart/bar_chart/bar_chart_helper.dart @@ -0,0 +1,48 @@ +import 'dart:math'; + +import 'package:fl_chart/src/chart/bar_chart/bar_chart_data.dart'; + +/// Contains anything that helps BarChart works +class BarChartHelper { + /// Calculates minY, and maxY based on [barGroups], + /// returns cached values, to prevent redundant calculations. + (double minY, double maxY) calculateMaxAxisValues( + List barGroups, + ) { + if (barGroups.isEmpty) { + return (0, 0); + } + + final BarChartGroupData barGroup; + try { + barGroup = barGroups.firstWhere((element) => element.barRods.isNotEmpty); + } catch (_) { + // There is no barChartGroupData with at least one barRod + return (0, 0); + } + + var maxY = max(barGroup.barRods[0].fromY, barGroup.barRods[0].toY); + var minY = min(barGroup.barRods[0].fromY, barGroup.barRods[0].toY); + + for (var i = 0; i < barGroups.length; i++) { + final barGroup = barGroups[i]; + for (var j = 0; j < barGroup.barRods.length; j++) { + final rod = barGroup.barRods[j]; + + maxY = max(maxY, rod.fromY); + minY = min(minY, rod.fromY); + + maxY = max(maxY, rod.toY); + minY = min(minY, rod.toY); + + if (rod.backDrawRodData.show) { + maxY = max(maxY, rod.backDrawRodData.fromY); + minY = min(minY, rod.backDrawRodData.fromY); + maxY = max(maxY, rod.backDrawRodData.toY); + minY = min(minY, rod.backDrawRodData.toY); + } + } + } + return (minY, maxY); + } +} diff --git a/lib/src/chart/bar_chart/bar_chart_painter.dart b/lib/src/chart/bar_chart/bar_chart_painter.dart new file mode 100644 index 0000000..5920230 --- /dev/null +++ b/lib/src/chart/bar_chart/bar_chart_painter.dart @@ -0,0 +1,841 @@ +import 'dart:core'; +import 'dart:math'; + +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/base/axis_chart/axis_chart_painter.dart'; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart'; +import 'package:fl_chart/src/extensions/bar_chart_data_extension.dart'; +import 'package:fl_chart/src/extensions/paint_extension.dart'; +import 'package:fl_chart/src/extensions/path_extension.dart'; +import 'package:fl_chart/src/extensions/rrect_extension.dart'; +import 'package:fl_chart/src/utils/canvas_wrapper.dart'; +import 'package:fl_chart/src/utils/utils.dart'; +import 'package:flutter/material.dart'; + +/// Paints [BarChartData] in the canvas, it can be used in a [CustomPainter] +class BarChartPainter extends AxisChartPainter { + /// Paints [dataList] into canvas, it is the animating [BarChartData], + /// [targetData] is the animation's target and remains the same + /// during animation, then we should use it when we need to show + /// tooltips or something like that, because [dataList] is changing constantly. + /// + /// [textScale] used for scaling texts inside the chart, + /// parent can use [MediaQuery.textScaleFactor] to respect + /// the system's font size. + BarChartPainter() : super() { + _barPaint = Paint()..style = PaintingStyle.fill; + _barStrokePaint = Paint()..style = PaintingStyle.stroke; + + _bgTouchTooltipPaint = Paint() + ..style = PaintingStyle.fill + ..color = Colors.white; + + _borderTouchTooltipPaint = Paint() + ..style = PaintingStyle.stroke + ..color = Colors.transparent + ..strokeWidth = 1.0; + + _clipPaint = Paint(); + } + + late Paint _barPaint; + late Paint _barStrokePaint; + late Paint _bgTouchTooltipPaint; + late Paint _borderTouchTooltipPaint; + late Paint _clipPaint; + + List? _groupBarsPosition; + + /// Paints [BarChartData] into the provided canvas. + @override + void paint( + BuildContext context, + CanvasWrapper canvasWrapper, + PaintHolder holder, + ) { + if (holder.chartVirtualRect != null) { + final canvasRect = Offset.zero & canvasWrapper.size; + canvasWrapper + ..saveLayer( + canvasRect, + _clipPaint, + ) + ..clipRect(canvasRect); + } + + super.paint(context, canvasWrapper, holder); + final data = holder.data; + final targetData = holder.targetData; + + if (data.barGroups.isEmpty) { + return; + } + + final usableSize = holder.getChartUsableSize(canvasWrapper.size); + + final groupsX = data.calculateGroupsX(usableSize.width); + final adjustment = holder.chartVirtualRect?.left ?? 0; + final groupsXAdjusted = groupsX.map((e) => e + adjustment).toList(); + + _groupBarsPosition = calculateGroupAndBarsPosition( + usableSize, + groupsXAdjusted, + data.barGroups, + ); + + if (!data.extraLinesData.extraLinesOnTop) { + super.drawHorizontalLines( + context, + canvasWrapper, + holder, + usableSize, + ); + } + + drawBars(canvasWrapper, _groupBarsPosition!, holder); + + drawErrorIndicatorData(canvasWrapper, _groupBarsPosition!, holder); + + if (data.extraLinesData.extraLinesOnTop) { + super.drawHorizontalLines( + context, + canvasWrapper, + holder, + usableSize, + ); + } + + if (holder.chartVirtualRect != null) { + canvasWrapper.restore(); + } + + for (var i = 0; i < data.barGroups.length; i++) { + final barGroup = data.barGroups[i]; + for (var j = 0; j < barGroup.barRods.length; j++) { + if (!barGroup.showingTooltipIndicators.contains(j)) { + continue; + } + final barRod = barGroup.barRods[j]; + + drawTouchTooltip( + context, + canvasWrapper, + _groupBarsPosition!, + targetData.barTouchData.touchTooltipData, + barGroup, + i, + barRod, + j, + holder, + ); + } + } + } + + /// Calculates bars position alongside group positions. + @visibleForTesting + List calculateGroupAndBarsPosition( + Size viewSize, + List groupsX, + List barGroups, + ) { + if (groupsX.length != barGroups.length) { + throw Exception('inconsistent state groupsX.length != barGroups.length'); + } + + final groupBarsPosition = []; + for (var i = 0; i < barGroups.length; i++) { + final barGroup = barGroups[i]; + final groupX = groupsX[i]; + if (barGroup.groupVertically) { + groupBarsPosition.add( + GroupBarsPosition( + groupX, + List.generate(barGroup.barRods.length, (index) => groupX), + ), + ); + continue; + } + + var tempX = 0.0; + final barsX = []; + barGroup.barRods.asMap().forEach((barIndex, barRod) { + final widthHalf = barRod.width / 2; + barsX.add(groupX - (barGroup.width / 2) + tempX + widthHalf); + tempX += barRod.width + barGroup.barsSpace; + }); + groupBarsPosition.add(GroupBarsPosition(groupX, barsX)); + } + return groupBarsPosition; + } + + @visibleForTesting + void drawBars( + CanvasWrapper canvasWrapper, + List groupBarsPosition, + PaintHolder holder, + ) { + final data = holder.data; + final viewSize = canvasWrapper.size; + + for (var i = 0; i < data.barGroups.length; i++) { + final barGroup = data.barGroups[i]; + for (var j = 0; j < barGroup.barRods.length; j++) { + final barRod = barGroup.barRods[j]; + final widthHalf = barRod.width / 2; + final borderRadius = + barRod.borderRadius ?? BorderRadius.circular(barRod.width / 2); + final borderSide = barRod.borderSide; + + final x = groupBarsPosition[i].barsX[j]; + + final left = x - widthHalf; + final right = x + widthHalf; + final cornerHeight = + max(borderRadius.topLeft.y, borderRadius.topRight.y) + + max(borderRadius.bottomLeft.y, borderRadius.bottomRight.y); + + RRect barRRect; + + /// Draw [BackgroundBarChartRodData] + if (barRod.backDrawRodData.show && + barRod.backDrawRodData.toY != barRod.backDrawRodData.fromY) { + if (barRod.backDrawRodData.toY > barRod.backDrawRodData.fromY) { + // positive + final bottom = getPixelY( + max(data.minY, barRod.backDrawRodData.fromY), + viewSize, + holder, + ); + final top = min( + getPixelY(barRod.backDrawRodData.toY, viewSize, holder), + bottom - cornerHeight, + ); + + barRRect = RRect.fromLTRBAndCorners( + left, + top, + right, + bottom, + topLeft: borderRadius.topLeft, + topRight: borderRadius.topRight, + bottomLeft: borderRadius.bottomLeft, + bottomRight: borderRadius.bottomRight, + ); + } else { + // negative + final top = getPixelY( + min(data.maxY, barRod.backDrawRodData.fromY), + viewSize, + holder, + ); + final bottom = max( + getPixelY(barRod.backDrawRodData.toY, viewSize, holder), + top + cornerHeight, + ); + + barRRect = RRect.fromLTRBAndCorners( + left, + top, + right, + bottom, + topLeft: borderRadius.topLeft, + topRight: borderRadius.topRight, + bottomLeft: borderRadius.bottomLeft, + bottomRight: borderRadius.bottomRight, + ); + } + + final backDraw = barRod.backDrawRodData; + _barPaint.setColorOrGradient( + backDraw.color, + backDraw.gradient, + barRRect.getRect(), + ); + canvasWrapper.drawRRect(barRRect, _barPaint); + } + + // draw Main Rod + if (barRod.toY != barRod.fromY) { + if (barRod.toY > barRod.fromY) { + // positive + final bottom = + getPixelY(max(data.minY, barRod.fromY), viewSize, holder); + final top = min( + getPixelY(barRod.toY, viewSize, holder), + bottom - cornerHeight, + ); + + barRRect = RRect.fromLTRBAndCorners( + left, + top, + right, + bottom, + topLeft: borderRadius.topLeft, + topRight: borderRadius.topRight, + bottomLeft: borderRadius.bottomLeft, + bottomRight: borderRadius.bottomRight, + ); + } else { + // negative + final top = + getPixelY(min(data.maxY, barRod.fromY), viewSize, holder); + final bottom = max( + getPixelY(barRod.toY, viewSize, holder), + top + cornerHeight, + ); + + barRRect = RRect.fromLTRBAndCorners( + left, + top, + right, + bottom, + topLeft: borderRadius.topLeft, + topRight: borderRadius.topRight, + bottomLeft: borderRadius.bottomLeft, + bottomRight: borderRadius.bottomRight, + ); + } + _barPaint.setColorOrGradient( + barRod.color, + barRod.gradient, + barRRect.getRect(), + ); + canvasWrapper.drawRRect(barRRect, _barPaint); + + // draw rod stack + if (barRod.rodStackItems.isNotEmpty) { + for (var i = 0; i < barRod.rodStackItems.length; i++) { + final stackItem = barRod.rodStackItems[i]; + final stackFromY = getPixelY(stackItem.fromY, viewSize, holder); + final stackToY = getPixelY(stackItem.toY, viewSize, holder); + + final isNegative = stackItem.toY < stackItem.fromY; + _barPaint.color = stackItem.color; + final rect = isNegative + ? Rect.fromLTRB(left, stackFromY, right, stackToY) + : Rect.fromLTRB(left, stackToY, right, stackFromY); + canvasWrapper + ..save() + ..clipRect(rect) + ..drawRRect(barRRect, _barPaint) + ..restore(); + + // draw border stroke for each stack item + drawStackItemBorderStroke( + canvasWrapper, + stackItem, + i, + barRod.rodStackItems.length, + barRod.width, + barRRect, + viewSize, + holder, + ); + } + } + + // draw border stroke + if (borderSide.width > 0 && borderSide.color.a > 0) { + _barStrokePaint + ..color = borderSide.color + ..strokeWidth = borderSide.width; + + final borderPath = Path()..addRRect(barRRect); + + canvasWrapper.drawPath( + borderPath.toDashedPath( + barRod.borderDashArray, + ), + _barStrokePaint, + ); + } + } + } + } + } + + @visibleForTesting + void drawErrorIndicatorData( + CanvasWrapper canvasWrapper, + List groupBarsPosition, + PaintHolder holder, + ) { + final data = holder.data; + final errorIndicatorData = data.errorIndicatorData; + if (!errorIndicatorData.show) { + return; + } + + final viewSize = canvasWrapper.size; + for (var i = 0; i < data.barGroups.length; i++) { + final barGroup = data.barGroups[i]; + for (var j = 0; j < barGroup.barRods.length; j++) { + final barRod = barGroup.barRods[j]; + + if (barRod.toYErrorRange == null) { + continue; + } + + final x = groupBarsPosition[i].barsX[j]; + + final y = getPixelY(barRod.toY, viewSize, holder); + final top = getPixelY( + barRod.toY + barRod.toYErrorRange!.upperBy, + viewSize, + holder, + ) - + y; + + final bottom = getPixelY( + barRod.toY - barRod.toYErrorRange!.lowerBy, + viewSize, + holder, + ) - + y; + + final relativeErrorPixelsRect = Rect.fromLTRB( + 0, + top, + 0, + bottom, + ); + + final painter = errorIndicatorData.painter( + BarChartSpotErrorRangeCallbackInput( + group: barGroup, + groupIndex: i, + rod: barRod, + barRodIndex: j, + ), + ); + canvasWrapper.drawErrorIndicator( + painter, + FlSpot( + barGroup.x.toDouble(), + barRod.toY, + yError: barRod.toYErrorRange, + ), + Offset(x, y), + relativeErrorPixelsRect, + holder.data, + ); + } + } + } + + @visibleForTesting + void drawTouchTooltip( + BuildContext context, + CanvasWrapper canvasWrapper, + List groupPositions, + BarTouchTooltipData tooltipData, + BarChartGroupData showOnBarGroup, + int barGroupIndex, + BarChartRodData showOnRodData, + int barRodIndex, + PaintHolder holder, + ) { + final viewSize = canvasWrapper.size; + + const textsBelowMargin = 4; + + final tooltipItem = tooltipData.getTooltipItem( + showOnBarGroup, + barGroupIndex, + showOnRodData, + barRodIndex, + ); + + if (tooltipItem == null) { + return; + } + + final span = TextSpan( + style: Utils().getThemeAwareTextStyle(context, tooltipItem.textStyle), + text: tooltipItem.text, + children: tooltipItem.children, + ); + + final tp = TextPainter( + text: span, + textAlign: tooltipItem.textAlign, + textDirection: tooltipItem.textDirection, + textScaler: holder.textScaler, + )..layout(maxWidth: tooltipData.maxContentWidth); + + /// creating TextPainters to calculate the width and height of the tooltip + final drawingTextPainter = tp; + + /// biggerWidth + /// some texts maybe larger, then we should + /// draw the tooltip' width as wide as biggerWidth + /// + /// sumTextsHeight + /// sum up all Texts height, then we should + /// draw the tooltip's height as tall as sumTextsHeight + final textWidth = drawingTextPainter.width; + final textHeight = drawingTextPainter.height + textsBelowMargin; + + final barX = groupPositions[barGroupIndex].barsX[barRodIndex]; + + /// if we have multiple bar lines, + /// there are more than one FlCandidate on touch area, + /// we should get the most top FlSpot Offset to draw the tooltip on top of it + final barToYPixel = Offset( + barX, + getPixelY(showOnRodData.toY, viewSize, holder), + ); + + final barFromYPixel = Offset( + barX, + getPixelY(showOnRodData.fromY, viewSize, holder), + ); + + final tooltipWidth = textWidth + tooltipData.tooltipPadding.horizontal; + final tooltipHeight = textHeight + tooltipData.tooltipPadding.vertical; + + final barTopY = min(barToYPixel.dy, barFromYPixel.dy); + final barBottomY = max(barToYPixel.dy, barFromYPixel.dy); + final drawTooltipOnTop = tooltipData.direction == TooltipDirection.top || + (tooltipData.direction == TooltipDirection.auto && + showOnRodData.isUpward()); + + final tooltipOriginPoint = Offset( + barX, + drawTooltipOnTop ? barTopY : barBottomY, + ); + + final isZoomed = holder.chartVirtualRect != null; + if (isZoomed && !canvasWrapper.size.contains(tooltipOriginPoint)) { + return; + } + + final tooltipTop = drawTooltipOnTop + ? barTopY - tooltipHeight - tooltipData.tooltipMargin + : barBottomY + tooltipData.tooltipMargin; + + final tooltipLeft = getTooltipLeft( + barToYPixel.dx, + tooltipWidth, + tooltipData.tooltipHorizontalAlignment, + tooltipData.tooltipHorizontalOffset, + ); + + /// draw the background rect with rounded radius + // ignore: omit_local_variable_types + Rect rect = Rect.fromLTWH( + tooltipLeft, + tooltipTop, + tooltipWidth, + tooltipHeight, + ); + + if (tooltipData.fitInsideHorizontally) { + if (rect.left < 0) { + final shiftAmount = 0 - rect.left; + rect = Rect.fromLTRB( + rect.left + shiftAmount, + rect.top, + rect.right + shiftAmount, + rect.bottom, + ); + } + + if (rect.right > viewSize.width) { + final shiftAmount = rect.right - viewSize.width; + rect = Rect.fromLTRB( + rect.left - shiftAmount, + rect.top, + rect.right - shiftAmount, + rect.bottom, + ); + } + } + + if (tooltipData.fitInsideVertically) { + if (rect.top < 0) { + final shiftAmount = 0 - rect.top; + rect = Rect.fromLTRB( + rect.left, + rect.top + shiftAmount, + rect.right, + rect.bottom + shiftAmount, + ); + } + + if (rect.bottom > viewSize.height) { + final shiftAmount = rect.bottom - viewSize.height; + rect = Rect.fromLTRB( + rect.left, + rect.top - shiftAmount, + rect.right, + rect.bottom - shiftAmount, + ); + } + } + + final roundedRect = RRect.fromRectAndCorners( + rect, + topLeft: tooltipData.tooltipBorderRadius.topLeft, + topRight: tooltipData.tooltipBorderRadius.topRight, + bottomLeft: tooltipData.tooltipBorderRadius.bottomLeft, + bottomRight: tooltipData.tooltipBorderRadius.bottomRight, + ); + + /// set tooltip's background color for each rod + _bgTouchTooltipPaint.color = tooltipData.getTooltipColor(showOnBarGroup); + + final rotateAngle = tooltipData.rotateAngle; + final rectRotationOffset = + Offset(0, Utils().calculateRotationOffset(rect.size, rotateAngle).dy); + final rectDrawOffset = Offset(roundedRect.left, roundedRect.top); + + final textRotationOffset = + Utils().calculateRotationOffset(tp.size, rotateAngle); + + /// draw the texts one by one in below of each other + final top = tooltipData.tooltipPadding.top; + final drawOffset = Offset( + rect.center.dx - (tp.width / 2), + rect.topCenter.dy + top - textRotationOffset.dy + rectRotationOffset.dy, + ); + + if (tooltipData.tooltipBorder != BorderSide.none) { + _borderTouchTooltipPaint + ..color = tooltipData.tooltipBorder.color + ..strokeWidth = tooltipData.tooltipBorder.width; + } + + final reverseQuarterTurnsAngle = -holder.data.rotationQuarterTurns * 90; + canvasWrapper.drawRotated( + size: rect.size, + rotationOffset: rectRotationOffset, + drawOffset: rectDrawOffset, + angle: reverseQuarterTurnsAngle + rotateAngle, + drawCallback: () { + canvasWrapper + ..drawRRect(roundedRect, _bgTouchTooltipPaint) + ..drawRRect(roundedRect, _borderTouchTooltipPaint) + ..drawText(tp, drawOffset); + }, + ); + } + + @visibleForTesting + void drawStackItemBorderStroke( + CanvasWrapper canvasWrapper, + BarChartRodStackItem stackItem, + int index, + int rodStacksSize, + double barThickSize, + RRect barRRect, + Size drawSize, + PaintHolder holder, + ) { + if (stackItem.borderSide.width == 0 || stackItem.borderSide.color.a == 0) { + return; + } + RRect strokeBarRect; + if (index == 0) { + strokeBarRect = RRect.fromLTRBAndCorners( + barRRect.left, + getPixelY(stackItem.toY, drawSize, holder), + barRRect.right, + getPixelY(stackItem.fromY, drawSize, holder), + bottomLeft: + stackItem.fromY < stackItem.toY ? barRRect.blRadius : Radius.zero, + bottomRight: + stackItem.fromY < stackItem.toY ? barRRect.brRadius : Radius.zero, + topLeft: + stackItem.fromY < stackItem.toY ? Radius.zero : barRRect.tlRadius, + topRight: + stackItem.fromY < stackItem.toY ? Radius.zero : barRRect.trRadius, + ); + } else if (index == rodStacksSize - 1) { + strokeBarRect = RRect.fromLTRBAndCorners( + barRRect.left, + max(getPixelY(stackItem.toY, drawSize, holder), barRRect.top), + barRRect.right, + getPixelY(stackItem.fromY, drawSize, holder), + bottomLeft: + stackItem.fromY < stackItem.toY ? Radius.zero : barRRect.blRadius, + bottomRight: + stackItem.fromY < stackItem.toY ? Radius.zero : barRRect.brRadius, + topLeft: + stackItem.fromY < stackItem.toY ? barRRect.tlRadius : Radius.zero, + topRight: + stackItem.fromY < stackItem.toY ? barRRect.trRadius : Radius.zero, + ); + } else { + strokeBarRect = RRect.fromLTRBR( + barRRect.left, + getPixelY(stackItem.toY, drawSize, holder), + barRRect.right, + getPixelY(stackItem.fromY, drawSize, holder), + Radius.zero, + ); + } + _barStrokePaint + ..color = stackItem.borderSide.color + ..strokeWidth = min(stackItem.borderSide.width, barThickSize / 2); + canvasWrapper.drawRRect(strokeBarRect, _barStrokePaint); + } + + /// Makes a [BarTouchedSpot] based on the provided [localPosition] + /// + /// Processes [localPosition] and checks + /// the elements of the chart that are near the offset, + /// then makes a [BarTouchedSpot] from the elements that has been touched. + /// + /// Returns null if finds nothing! + BarTouchedSpot? handleTouch( + Offset localPosition, + Size size, + PaintHolder holder, + ) { + final data = holder.data; + final targetData = holder.targetData; + final touchedPoint = localPosition; + if (targetData.barGroups.isEmpty) { + return null; + } + + final viewSize = holder.getChartUsableSize(size); + + // Check if the touch is outside the canvas bounds + final isZoomed = holder.chartVirtualRect != null; + if (isZoomed && !size.contains(touchedPoint)) { + return null; + } + + if (_groupBarsPosition == null) { + final groupsX = data.calculateGroupsX(viewSize.width); + _groupBarsPosition = + calculateGroupAndBarsPosition(viewSize, groupsX, data.barGroups); + } + + /// Find the nearest barRod + for (var i = 0; i < _groupBarsPosition!.length; i++) { + final groupBarPos = _groupBarsPosition![i]; + for (var j = 0; j < groupBarPos.barsX.length; j++) { + final barX = groupBarPos.barsX[j]; + final barWidth = targetData.barGroups[i].barRods[j].width; + final halfBarWidth = barWidth / 2; + + double barTopY; + double barBotY; + + final isUpward = targetData.barGroups[i].barRods[j].isUpward(); + if (isUpward) { + barTopY = getPixelY( + targetData.barGroups[i].barRods[j].toY, + viewSize, + holder, + ); + barBotY = getPixelY( + targetData.barGroups[i].barRods[j].fromY + + targetData.barGroups[i].barRods[j].backDrawRodData.fromY, + viewSize, + holder, + ); + } else { + barTopY = getPixelY( + targetData.barGroups[i].barRods[j].fromY + + targetData.barGroups[i].barRods[j].backDrawRodData.fromY, + viewSize, + holder, + ); + barBotY = getPixelY( + targetData.barGroups[i].barRods[j].toY, + viewSize, + holder, + ); + } + + final backDrawBarY = getPixelY( + targetData.barGroups[i].barRods[j].backDrawRodData.toY, + viewSize, + holder, + ); + final touchExtraThreshold = targetData.barTouchData.touchExtraThreshold; + + final isXInTouchBounds = (touchedPoint.dx <= + barX + halfBarWidth + touchExtraThreshold.right) && + (touchedPoint.dx >= barX - halfBarWidth - touchExtraThreshold.left); + + bool isYInBarBounds; + if (isUpward) { + isYInBarBounds = + (touchedPoint.dy <= barBotY + touchExtraThreshold.bottom) && + (touchedPoint.dy >= barTopY - touchExtraThreshold.top); + } else { + isYInBarBounds = + (touchedPoint.dy >= barTopY - touchExtraThreshold.top) && + (touchedPoint.dy <= barBotY + touchExtraThreshold.bottom); + } + + bool isYInBarBackDrawBounds; + if (isUpward) { + isYInBarBackDrawBounds = + (touchedPoint.dy <= barBotY + touchExtraThreshold.bottom) && + (touchedPoint.dy >= backDrawBarY - touchExtraThreshold.top); + } else { + isYInBarBackDrawBounds = (touchedPoint.dy >= + barTopY - touchExtraThreshold.top) && + (touchedPoint.dy <= backDrawBarY + touchExtraThreshold.bottom); + } + + final isYInTouchBounds = + (targetData.barTouchData.allowTouchBarBackDraw && + isYInBarBackDrawBounds) || + isYInBarBounds; + + if (isXInTouchBounds && isYInTouchBounds) { + final nearestGroup = targetData.barGroups[i]; + final nearestBarRod = nearestGroup.barRods[j]; + final nearestSpot = + FlSpot(nearestGroup.x.toDouble(), nearestBarRod.toY); + final nearestSpotPos = + Offset(barX, getPixelY(nearestSpot.y, viewSize, holder)); + + var touchedStackIndex = -1; + BarChartRodStackItem? touchedStack; + for (var stackIndex = 0; + stackIndex < nearestBarRod.rodStackItems.length; + stackIndex++) { + final stackItem = nearestBarRod.rodStackItems[stackIndex]; + final fromPixel = getPixelY(stackItem.fromY, viewSize, holder); + final toPixel = getPixelY(stackItem.toY, viewSize, holder); + if (touchedPoint.dy <= fromPixel && touchedPoint.dy >= toPixel) { + touchedStackIndex = stackIndex; + touchedStack = stackItem; + break; + } + } + + return BarTouchedSpot( + nearestGroup, + i, + nearestBarRod, + j, + touchedStack, + touchedStackIndex, + nearestSpot, + nearestSpotPos, + ); + } + } + } + + return null; + } +} + +@visibleForTesting +class GroupBarsPosition { + GroupBarsPosition(this.groupX, this.barsX); + + final double groupX; + final List barsX; +} diff --git a/lib/src/chart/bar_chart/bar_chart_renderer.dart b/lib/src/chart/bar_chart/bar_chart_renderer.dart new file mode 100644 index 0000000..c04a7ef --- /dev/null +++ b/lib/src/chart/bar_chart/bar_chart_renderer.dart @@ -0,0 +1,140 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/bar_chart/bar_chart_painter.dart'; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart'; +import 'package:fl_chart/src/chart/base/base_chart/render_base_chart.dart'; +import 'package:fl_chart/src/utils/canvas_wrapper.dart'; +import 'package:flutter/cupertino.dart'; + +// coverage:ignore-start + +/// Low level BarChart Widget. +class BarChartLeaf extends LeafRenderObjectWidget { + const BarChartLeaf({ + super.key, + required this.data, + required this.targetData, + required this.canBeScaled, + required this.chartVirtualRect, + }); + + final BarChartData data; + final BarChartData targetData; + final Rect? chartVirtualRect; + final bool canBeScaled; + + @override + RenderBarChart createRenderObject(BuildContext context) => RenderBarChart( + context, + data, + targetData, + MediaQuery.of(context).textScaler, + chartVirtualRect, + canBeScaled: canBeScaled, + ); + + @override + void updateRenderObject(BuildContext context, RenderBarChart renderObject) { + renderObject + ..data = data + ..targetData = targetData + ..textScaler = MediaQuery.of(context).textScaler + ..buildContext = context + ..chartVirtualRect = chartVirtualRect + ..canBeScaled = canBeScaled; + } +} +// coverage:ignore-end + +/// Renders our BarChart, also handles hitTest. +class RenderBarChart extends RenderBaseChart { + RenderBarChart( + BuildContext context, + BarChartData data, + BarChartData targetData, + TextScaler textScaler, + Rect? chartVirtualRect, { + required bool canBeScaled, + }) : _data = data, + _targetData = targetData, + _textScaler = textScaler, + _chartVirtualRect = chartVirtualRect, + super(targetData.barTouchData, context, canBeScaled: canBeScaled); + + BarChartData get data => _data; + BarChartData _data; + + set data(BarChartData value) { + if (_data == value) return; + _data = value; + markNeedsPaint(); + } + + BarChartData get targetData => _targetData; + BarChartData _targetData; + + set targetData(BarChartData value) { + if (_targetData == value) return; + _targetData = value; + super.updateBaseTouchData(_targetData.barTouchData); + markNeedsPaint(); + } + + TextScaler get textScaler => _textScaler; + TextScaler _textScaler; + + set textScaler(TextScaler value) { + if (_textScaler == value) return; + _textScaler = value; + markNeedsPaint(); + } + + Rect? get chartVirtualRect => _chartVirtualRect; + Rect? _chartVirtualRect; + + set chartVirtualRect(Rect? value) { + if (_chartVirtualRect == value) return; + _chartVirtualRect = value; + markNeedsPaint(); + } + + // We couldn't mock [size] property of this class, that's why we have this + @visibleForTesting + Size? mockTestSize; + + @visibleForTesting + BarChartPainter painter = BarChartPainter(); + + PaintHolder get paintHolder => + PaintHolder(data, targetData, textScaler, chartVirtualRect); + + @override + void paint(PaintingContext context, Offset offset) { + final canvas = context.canvas + ..save() + ..translate(offset.dx, offset.dy); + painter.paint( + buildContext, + CanvasWrapper(canvas, mockTestSize ?? size), + paintHolder, + ); + canvas.restore(); + } + + @override + BarTouchResponse getResponseAtLocation(Offset localPosition) { + final chartSize = mockTestSize ?? size; + return BarTouchResponse( + touchLocation: localPosition, + touchChartCoordinate: painter.getChartCoordinateFromPixel( + localPosition, + chartSize, + paintHolder, + ), + spot: painter.handleTouch( + localPosition, + chartSize, + paintHolder, + ), + ); + } +} diff --git a/lib/src/chart/base/axis_chart/axis_chart_data.dart b/lib/src/chart/base/axis_chart/axis_chart_data.dart new file mode 100644 index 0000000..71203fb --- /dev/null +++ b/lib/src/chart/base/axis_chart/axis_chart_data.dart @@ -0,0 +1,2451 @@ +// coverage:ignore-file +import 'dart:ui'; + +import 'package:equatable/equatable.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/base/axis_chart/axis_chart_painter.dart'; +import 'package:fl_chart/src/extensions/paint_extension.dart'; +import 'package:fl_chart/src/utils/canvas_wrapper.dart'; +import 'package:fl_chart/src/utils/lerp.dart'; +import 'package:fl_chart/src/utils/utils.dart'; +import 'package:flutter/material.dart' hide Image; + +/// This is the base class for axis base charts data +/// that contains a [FlGridData] that holds data for showing grid lines, +/// also we have [minX], [maxX], [minY], [maxY] values +/// we use them to determine how much is the scale of chart, +/// and calculate x and y according to the scale. +/// each child have to set it in their constructor. +abstract class AxisChartData extends BaseChartData with EquatableMixin { + AxisChartData({ + FlGridData? gridData, + required this.titlesData, + RangeAnnotations? rangeAnnotations, + required this.minX, + required this.maxX, + double? baselineX, + required this.minY, + required this.maxY, + double? baselineY, + FlClipData? clipData, + Color? backgroundColor, + super.borderData, + ExtraLinesData? extraLinesData, + this.rotationQuarterTurns = 0, + }) : gridData = gridData ?? const FlGridData(), + rangeAnnotations = rangeAnnotations ?? const RangeAnnotations(), + baselineX = baselineX ?? 0, + baselineY = baselineY ?? 0, + clipData = clipData ?? const FlClipData.none(), + backgroundColor = backgroundColor ?? Colors.transparent, + extraLinesData = extraLinesData ?? const ExtraLinesData(); + final FlGridData gridData; + final FlTitlesData titlesData; + final RangeAnnotations rangeAnnotations; + + final double minX; + final double maxX; + final double baselineX; + final double minY; + final double maxY; + final double baselineY; + + /// clip the chart to the border (prevent draw outside the border) + final FlClipData clipData; + + /// A background color which is drawn behind the chart. + final Color backgroundColor; + + /// Difference of [maxY] and [minY] + double get verticalDiff => maxY - minY; + + /// Difference of [maxX] and [minX] + double get horizontalDiff => maxX - minX; + + /// Extra horizontal or vertical lines to draw on the chart. + final ExtraLinesData extraLinesData; + + /// Rotates the chart by 90 degrees clockwise in each turn + final int rotationQuarterTurns; + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + gridData, + titlesData, + rangeAnnotations, + minX, + maxX, + baselineX, + minY, + maxY, + baselineY, + clipData, + backgroundColor, + borderData, + extraLinesData, + rotationQuarterTurns, + ]; +} + +/// This class holds the touch response details of the axis-based charts +abstract class AxisBaseTouchResponse extends BaseTouchResponse { + AxisBaseTouchResponse({ + required super.touchLocation, + required this.touchChartCoordinate, + }); + + /// The axis coordinate of the touch in chart's coordinate system. + final Offset touchChartCoordinate; +} + +/// Represents a side of the chart +enum AxisSide { + left, + top, + right, + bottom; + + AxisSide rotateByQuarterTurns(int quarterTurns) { + const values = AxisSide.values; + return values[(values.indexOf(this) + quarterTurns) % values.length]; + } +} + +/// Contains meta information about the drawing title. +class TitleMeta { + TitleMeta({ + required this.min, + required this.max, + required this.parentAxisSize, + required this.axisPosition, + required this.appliedInterval, + required this.sideTitles, + required this.formattedValue, + required this.axisSide, + required this.rotationQuarterTurns, + }) : assert( + rotationQuarterTurns >= 0, + "TitleMeta.rotationQuarterTurns couldn't be negative", + ); + + /// min axis value + final double min; + + /// max axis value + final double max; + + /// parent axis max width/height + final double parentAxisSize; + + /// The position (in pixel) that applied to + /// this drawing title along its axis. + final double axisPosition; + + /// The interval that applied to this drawing title + final double appliedInterval; + + /// Reference of [SideTitles] object. + final SideTitles sideTitles; + + /// Formatted value that is suitable to show, for example 100, 2k, 5m, ... + final String formattedValue; + + /// Determines the axis side of titles (left, top, right, bottom) + final AxisSide axisSide; + + /// Chart is rotated by 90 degrees clockwise in each turn + /// + /// default is zero, which means chart is normal and upward + final int rotationQuarterTurns; +} + +/// It gives you the axis value and gets a String value based on it. +typedef GetTitleWidgetFunction = Widget Function(double value, TitleMeta meta); + +/// The default [SideTitles.getTitlesWidget] function. +/// +/// formats the axis number to a shorter string using [formatNumber]. +Widget defaultGetTitle(double value, TitleMeta meta) { + return SideTitleWidget( + meta: meta, + child: Text( + meta.formattedValue, + ), + ); +} + +/// Holds data for showing label values on axis numbers +class SideTitles with EquatableMixin { + /// It draws some title on an axis, per axis values, + /// [showTitles] determines showing or hiding this side, + /// + /// Texts are depend on the axis value, you can override [getTitles], + /// it gives you an axis value (double value) and a [TitleMeta] which contains + /// additional information about the axis. + /// Then you should return a [Widget] to show. + /// It allows you to do anything you want, For example you can show icons + /// instead of texts, because it accepts a [Widget] + /// + /// [reservedSize] determines the maximum space that your titles need, + /// (All titles will stretch using this value) + /// + /// Texts are showing with provided [interval]. If you don't provide anything, + /// we try to find a suitable value to set as [interval] under the hood. + const SideTitles({ + this.showTitles = false, + this.getTitlesWidget = defaultGetTitle, + this.reservedSize = 22, + this.interval, + this.minIncluded = true, + this.maxIncluded = true, + }) : assert(interval != 0, "SideTitles.interval couldn't be zero"); + + /// Determines showing or hiding this side titles + final bool showTitles; + + /// You can override it to pass your custom widget to show in each axis value + /// We recommend you to use [SideTitleWidget]. + /// + /// If you decide to implement your custom widget + /// (instead of [SideTitleWidget]), you have to take care of the alignment, + /// space to the chart and also the rotation (if you are rotating the chart, + /// for example for Horizontal Bar Chart) + final GetTitleWidgetFunction getTitlesWidget; + + /// It determines the maximum space that your titles need, + /// (All titles will stretch using this value) + final double reservedSize; + + /// Texts are showing with provided [interval]. If you don't provide anything, + /// we try to find a suitable value to set as [interval] under the hood. + final double? interval; + + /// If true (default), a title for the minimum data value is included + /// independent of the sampling interval + final bool minIncluded; + + /// If true (default), a title for the maximum data value is included + /// independent of the sampling interval + final bool maxIncluded; + + /// Lerps a [SideTitles] based on [t] value, check [Tween.lerp]. + static SideTitles lerp(SideTitles a, SideTitles b, double t) => SideTitles( + showTitles: b.showTitles, + getTitlesWidget: b.getTitlesWidget, + reservedSize: lerpDouble(a.reservedSize, b.reservedSize, t)!, + interval: lerpDouble(a.interval, b.interval, t), + minIncluded: b.minIncluded, + maxIncluded: b.maxIncluded, + ); + + /// Copies current [SideTitles] to a new [SideTitles], + /// and replaces provided values. + SideTitles copyWith({ + bool? showTitles, + GetTitleWidgetFunction? getTitlesWidget, + double? reservedSize, + double? interval, + bool? minIncluded, + bool? maxIncluded, + }) => + SideTitles( + showTitles: showTitles ?? this.showTitles, + getTitlesWidget: getTitlesWidget ?? this.getTitlesWidget, + reservedSize: reservedSize ?? this.reservedSize, + interval: interval ?? this.interval, + minIncluded: minIncluded ?? this.minIncluded, + maxIncluded: maxIncluded ?? this.maxIncluded, + ); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + showTitles, + getTitlesWidget, + reservedSize, + interval, + minIncluded, + maxIncluded, + ]; +} + +/// Force child widget to be positioned inside its +/// corresponding axis bounding box +/// +/// To makes things simpler, it's recommended to use +/// [SideTitleFitInsideData.fromTitleMeta] and pass the +/// TitleMeta provided from [SideTitles.getTitlesWidget] +class SideTitleFitInsideData with EquatableMixin { + /// Force child widget to be positioned inside its + /// corresponding axis bounding box + /// + /// To makes things simpler, it's recommended to use + /// [SideTitleFitInsideData.fromTitleMeta] and pass the + /// TitleMeta provided from [SideTitles.getTitlesWidget] + /// + /// Some translations will be applied to force + /// children to be positioned inside the parent axis bounding box. + /// + /// Will override the [SideTitleWidget.space] and caused + /// spacing between [SideTitles] children might be not equal. + const SideTitleFitInsideData({ + required this.enabled, + required this.axisPosition, + required this.parentAxisSize, + required this.distanceFromEdge, + }); + + /// Create a disabled [SideTitleFitInsideData]. + /// If used, the child widget wouldn't be fitted + /// inside its corresponding axis bounding box + factory SideTitleFitInsideData.disable() => const SideTitleFitInsideData( + enabled: false, + distanceFromEdge: 0, + parentAxisSize: 0, + axisPosition: 0, + ); + + /// Help to Create [SideTitleFitInsideData] from [TitleMeta]. + /// [TitleMeta] is provided by [SideTitles.getTitlesWidget] function. + factory SideTitleFitInsideData.fromTitleMeta( + TitleMeta meta, { + bool enabled = true, + double distanceFromEdge = 6, + }) => + SideTitleFitInsideData( + enabled: enabled, + distanceFromEdge: distanceFromEdge, + parentAxisSize: meta.parentAxisSize, + axisPosition: meta.axisPosition, + ); + + /// Whether to enable fit inside to SideTitleWidget + final bool enabled; + + /// Distance between child widget and its closest corresponding axis edge + final double distanceFromEdge; + + /// Parent axis max width/height + final double parentAxisSize; + + /// The position (in pixel) that applied to + /// the child widget along its corresponding axis. + final double axisPosition; + + @override + List get props => [ + enabled, + distanceFromEdge, + parentAxisSize, + axisPosition, + ]; +} + +/// Holds data for showing each side titles (left, top, right, bottom) +class AxisTitles with EquatableMixin { + /// you can provide [axisName] if you want to show a general + /// label on this axis, + /// + /// [axisNameSize] determines the maximum size that [axisName] can use + /// + /// [sideTitles] property is responsible to show your axis side labels + const AxisTitles({ + this.axisNameWidget, + this.axisNameSize = 16, + this.sideTitles = const SideTitles(), + this.drawBelowEverything = true, + }); + + /// Determines the size of [axisName] + final double axisNameSize; + + /// It shows the name of axis, for example your x-axis shows year, + /// then you might want to show it using [axisNameWidget] property as a widget + final Widget? axisNameWidget; + + /// It is responsible to show your axis side labels. + final SideTitles sideTitles; + + /// If titles are showing on top of your tooltip, you can draw them below everything. + /// + /// In the future, we will convert tooltips to a widget, that would solve this problem. + final bool drawBelowEverything; + + /// If there is something to show as axisTitles, it returns true + bool get showAxisTitles => axisNameWidget != null && axisNameSize != 0; + + /// If there is something to show as sideTitles, it returns true + bool get showSideTitles => + sideTitles.showTitles && sideTitles.reservedSize != 0; + + /// Lerps a [AxisTitles] based on [t] value, check [Tween.lerp]. + static AxisTitles lerp(AxisTitles a, AxisTitles b, double t) => AxisTitles( + axisNameWidget: b.axisNameWidget, + axisNameSize: lerpDouble(a.axisNameSize, b.axisNameSize, t)!, + sideTitles: SideTitles.lerp(a.sideTitles, b.sideTitles, t), + drawBelowEverything: b.drawBelowEverything, + ); + + /// Copies current [SideTitles] to a new [SideTitles], + /// and replaces provided values. + AxisTitles copyWith({ + Widget? axisNameWidget, + double? axisNameSize, + SideTitles? sideTitles, + bool? drawBelowEverything, + }) => + AxisTitles( + axisNameWidget: axisNameWidget ?? this.axisNameWidget, + axisNameSize: axisNameSize ?? this.axisNameSize, + sideTitles: sideTitles ?? this.sideTitles, + drawBelowEverything: drawBelowEverything ?? this.drawBelowEverything, + ); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + axisNameWidget, + axisNameSize, + sideTitles, + drawBelowEverything, + ]; +} + +/// Holds data for showing titles on each side of charts. +class FlTitlesData with EquatableMixin { + /// [show] determines showing or hiding all titles, + /// [leftTitles], [topTitles], [rightTitles], [bottomTitles] defines + /// side titles of left, top, right, bottom sides respectively. + const FlTitlesData({ + this.show = true, + this.leftTitles = const AxisTitles( + sideTitles: SideTitles( + reservedSize: 44, + showTitles: true, + ), + ), + this.topTitles = const AxisTitles( + sideTitles: SideTitles( + reservedSize: 30, + showTitles: true, + ), + ), + this.rightTitles = const AxisTitles( + sideTitles: SideTitles( + reservedSize: 44, + showTitles: true, + ), + ), + this.bottomTitles = const AxisTitles( + sideTitles: SideTitles( + reservedSize: 30, + showTitles: true, + ), + ), + }); + + final bool show; + final AxisTitles leftTitles; + final AxisTitles topTitles; + final AxisTitles rightTitles; + final AxisTitles bottomTitles; + + /// Lerps a [FlTitlesData] based on [t] value, check [Tween.lerp]. + static FlTitlesData lerp(FlTitlesData a, FlTitlesData b, double t) => + FlTitlesData( + show: b.show, + leftTitles: AxisTitles.lerp(a.leftTitles, b.leftTitles, t), + rightTitles: AxisTitles.lerp(a.rightTitles, b.rightTitles, t), + bottomTitles: AxisTitles.lerp(a.bottomTitles, b.bottomTitles, t), + topTitles: AxisTitles.lerp(a.topTitles, b.topTitles, t), + ); + + /// Copies current [FlTitlesData] to a new [FlTitlesData], + /// and replaces provided values. + FlTitlesData copyWith({ + bool? show, + AxisTitles? leftTitles, + AxisTitles? topTitles, + AxisTitles? rightTitles, + AxisTitles? bottomTitles, + }) => + FlTitlesData( + show: show ?? this.show, + leftTitles: leftTitles ?? this.leftTitles, + topTitles: topTitles ?? this.topTitles, + rightTitles: rightTitles ?? this.rightTitles, + bottomTitles: bottomTitles ?? this.bottomTitles, + ); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + show, + leftTitles, + topTitles, + rightTitles, + bottomTitles, + ]; +} + +/// Represents a conceptual position in cartesian (axis based) space. +@immutable +class FlSpot { + /// [x] determines cartesian (axis based) horizontally position + /// 0 means most left point of the chart + /// + /// [y] determines cartesian (axis based) vertically position + /// 0 means most bottom point of the chart + const FlSpot( + this.x, + this.y, { + this.xError, + this.yError, + }); + + final double x; + final double y; + final FlErrorRange? xError; + final FlErrorRange? yError; + + /// Copies current [FlSpot] to a new [FlSpot], + /// and replaces provided values. + // Prevent polymorphism + FlSpot copyWith({ + double? x, + double? y, + FlErrorRange? xError, + FlErrorRange? yError, + }) => + FlSpot( + x ?? this.x, + y ?? this.y, + xError: xError ?? this.xError, + yError: yError ?? this.yError, + ); + + ///Prints x and y coordinates of FlSpot list + @override + String toString() => '($x, $y, $xError, $yError)'; + + /// Used for splitting lines, or maybe other concepts. + static const FlSpot nullSpot = FlSpot(double.nan, double.nan); + + /// Sets zero for x and y + static const FlSpot zero = FlSpot(0, 0); + + /// Determines if [x] or [y] is null. + bool isNull() => this == nullSpot; + + /// Determines if [x] and [y] is not null. + bool isNotNull() => !isNull(); + + /// Lerps a [FlSpot] based on [t] value, check [Tween.lerp]. + static FlSpot lerp(FlSpot a, FlSpot b, double t) { + if (a == FlSpot.nullSpot) { + return b; + } + + if (b == FlSpot.nullSpot) { + return a; + } + + return FlSpot( + lerpDouble(a.x, b.x, t)!, + lerpDouble(a.y, b.y, t)!, + xError: FlErrorRange.lerp(a.xError, b.xError, t), + yError: FlErrorRange.lerp(a.yError, b.yError, t), + ); + } + + /// Two [FlSpot] are equal if their [x] and [y] are equal. + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! FlSpot) { + return false; + } + + if (x.isNaN && y.isNaN && other.x.isNaN && other.y.isNaN) { + return true; + } + + return other.x == x && + other.y == y && + other.xError == xError && + other.yError == yError; + } + + /// Override hashCode + @override + int get hashCode => + x.hashCode ^ y.hashCode ^ xError.hashCode ^ yError.hashCode; +} + +/// Represents a range of values that can be used to show error bars/threshold +/// +/// [lowerBy] and [upperBy] are the values that will be added and subtracted +/// from the main value. It means that they should be non-negative. +/// Also it means that they are relative to the main value. +class FlErrorRange with EquatableMixin { + const FlErrorRange({ + required this.lowerBy, + required this.upperBy, + }) : assert(lowerBy >= 0, 'lowerBy must be non-negative'), + assert(upperBy >= 0, 'upperBy must be non-negative'); + + /// Creates a symmetric error range. + /// It sets [lowerBy] and [upperBy] to the same [value]. + const FlErrorRange.symmetric(double value) + : lowerBy = value, + upperBy = value, + assert(value >= 0, 'value must be non-negative'); + + /// determines the lower bound of the error range, it will be subtracted from + /// the main value. So it is non-negative and it is relative to the main value + final double lowerBy; + + /// determines the lower bound of the error range, it will be added to + /// the main value. So it is non-negative and it is relative to the main value + final double upperBy; + + /// Lerps a [FlErrorRange] based on [t] value + static FlErrorRange? lerp(FlErrorRange? a, FlErrorRange? b, double t) { + if (a != null && b != null) { + return FlErrorRange( + lowerBy: lerpDouble(a.lowerBy, b.lowerBy, t)!, + upperBy: lerpDouble(a.upperBy, b.upperBy, t)!, + ); + } + + return b; + } + + @override + List get props => [lowerBy, upperBy]; +} + +/// Responsible to hold grid data, +class FlGridData with EquatableMixin { + /// Responsible for rendering grid lines behind the content of charts, + /// [show] determines showing or hiding all grids, + /// + /// [AxisChartPainter] draws horizontal lines from left to right of the chart, + /// with increasing y value, it increases by [horizontalInterval]. + /// Representation of each line determines by [getDrawingHorizontalLine] callback, + /// it gives you a double value (in the y axis), and you should return a [FlLine] that represents + /// a horizontal line. + /// You are allowed to show or hide any horizontal line, using [checkToShowHorizontalLine] callback, + /// it gives you a double value (in the y axis), and you should return a boolean that determines + /// showing or hiding specified line. + /// or you can hide all horizontal lines by setting [drawHorizontalLine] false. + /// + /// [AxisChartPainter] draws vertical lines from bottom to top of the chart, + /// with increasing x value, it increases by [verticalInterval]. + /// Representation of each line determines by [getDrawingVerticalLine] callback, + /// it gives you a double value (in the x axis), and you should return a [FlLine] that represents + /// a horizontal line. + /// You are allowed to show or hide any vertical line, using [checkToShowVerticalLine] callback, + /// it gives you a double value (in the x axis), and you should return a boolean that determines + /// showing or hiding specified line. + /// or you can hide all vertical lines by setting [drawVerticalLine] false. + const FlGridData({ + this.show = true, + this.drawHorizontalLine = true, + this.horizontalInterval, + this.getDrawingHorizontalLine = defaultGridLine, + this.checkToShowHorizontalLine = showAllGrids, + this.drawVerticalLine = true, + this.verticalInterval, + this.getDrawingVerticalLine = defaultGridLine, + this.checkToShowVerticalLine = showAllGrids, + }) : assert( + horizontalInterval != 0, + "FlGridData.horizontalInterval couldn't be zero", + ), + assert( + verticalInterval != 0, + "FlGridData.verticalInterval couldn't be zero", + ); + + /// Determines showing or hiding all horizontal and vertical lines. + final bool show; + + /// Determines showing or hiding all horizontal lines. + final bool drawHorizontalLine; + + /// Determines interval between horizontal lines, left it null to be auto calculated. + final double? horizontalInterval; + + /// Gives you a y value, and gets a [FlLine] that represents specified line. + final GetDrawingGridLine getDrawingHorizontalLine; + + /// Gives you a y value, and gets a boolean that determines showing or hiding specified line. + final CheckToShowGrid checkToShowHorizontalLine; + + /// Determines showing or hiding all vertical lines. + final bool drawVerticalLine; + + /// Determines interval between vertical lines, left it null to be auto calculated. + final double? verticalInterval; + + /// Gives you a x value, and gets a [FlLine] that represents specified line. + final GetDrawingGridLine getDrawingVerticalLine; + + /// Gives you a x value, and gets a boolean that determines showing or hiding specified line. + final CheckToShowGrid checkToShowVerticalLine; + + /// Lerps a [FlGridData] based on [t] value, check [Tween.lerp]. + static FlGridData lerp(FlGridData a, FlGridData b, double t) => FlGridData( + show: b.show, + drawHorizontalLine: b.drawHorizontalLine, + horizontalInterval: + lerpDouble(a.horizontalInterval, b.horizontalInterval, t), + getDrawingHorizontalLine: b.getDrawingHorizontalLine, + checkToShowHorizontalLine: b.checkToShowHorizontalLine, + drawVerticalLine: b.drawVerticalLine, + verticalInterval: lerpDouble(a.verticalInterval, b.verticalInterval, t), + getDrawingVerticalLine: b.getDrawingVerticalLine, + checkToShowVerticalLine: b.checkToShowVerticalLine, + ); + + /// Copies current [FlGridData] to a new [FlGridData], + /// and replaces provided values. + FlGridData copyWith({ + bool? show, + bool? drawHorizontalLine, + double? horizontalInterval, + GetDrawingGridLine? getDrawingHorizontalLine, + CheckToShowGrid? checkToShowHorizontalLine, + bool? drawVerticalLine, + double? verticalInterval, + GetDrawingGridLine? getDrawingVerticalLine, + CheckToShowGrid? checkToShowVerticalLine, + }) => + FlGridData( + show: show ?? this.show, + drawHorizontalLine: drawHorizontalLine ?? this.drawHorizontalLine, + horizontalInterval: horizontalInterval ?? this.horizontalInterval, + getDrawingHorizontalLine: + getDrawingHorizontalLine ?? this.getDrawingHorizontalLine, + checkToShowHorizontalLine: + checkToShowHorizontalLine ?? this.checkToShowHorizontalLine, + drawVerticalLine: drawVerticalLine ?? this.drawVerticalLine, + verticalInterval: verticalInterval ?? this.verticalInterval, + getDrawingVerticalLine: + getDrawingVerticalLine ?? this.getDrawingVerticalLine, + checkToShowVerticalLine: + checkToShowVerticalLine ?? this.checkToShowVerticalLine, + ); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + show, + drawHorizontalLine, + horizontalInterval, + getDrawingHorizontalLine, + checkToShowHorizontalLine, + drawVerticalLine, + verticalInterval, + getDrawingVerticalLine, + checkToShowVerticalLine, + ]; +} + +/// Determines showing or hiding specified line. +typedef CheckToShowGrid = bool Function(double value); + +/// Shows all lines. +bool showAllGrids(double value) => true; + +/// Determines the appearance of specified line. +/// +/// It gives you an axis [value] (horizontal or vertical), +/// you should pass a [FlLine] that represents style of specified line. +typedef GetDrawingGridLine = FlLine Function(double value); + +/// Returns a grey line for all values. +FlLine defaultGridLine(double value) => const FlLine( + color: Colors.blueGrey, + strokeWidth: 0.4, + dashArray: [8, 4], + ); + +/// Defines style of a line. +class FlLine with EquatableMixin { + /// Renders a line, color it by [color], + /// thickness is defined by [strokeWidth], + /// and if you want to have dashed line, you should fill [dashArray], + /// it is a circular array of dash offsets and lengths. + /// For example, the array `[5, 10]` would result in dashes 5 pixels long + /// followed by blank spaces 10 pixels long. + const FlLine({ + Color? color, + this.gradient, + this.strokeWidth = 2, + this.dashArray, + }) : color = color ?? + ((color == null && gradient == null) ? Colors.black : null); + + /// Defines color of the line. + final Color? color; + + /// Defines the gradient of the line. + final Gradient? gradient; + + /// Defines thickness of the line. + final double strokeWidth; + + /// Defines dash effect of the line. + /// + /// it is a circular array of dash offsets and lengths. + /// For example, the array `[5, 10]` would result in dashes 5 pixels long + /// followed by blank spaces 10 pixels long. + final List? dashArray; + + /// Lerps a [FlLine] based on [t] value, check [Tween.lerp]. + static FlLine lerp(FlLine a, FlLine b, double t) => FlLine( + color: Color.lerp(a.color, b.color, t), + gradient: Gradient.lerp(a.gradient, b.gradient, t), + strokeWidth: lerpDouble(a.strokeWidth, b.strokeWidth, t)!, + dashArray: lerpIntList(a.dashArray, b.dashArray, t), + ); + + /// Copies current [FlLine] to a new [FlLine], + /// and replaces provided values. + FlLine copyWith({ + Color? color, + Gradient? gradient, + double? strokeWidth, + List? dashArray, + }) => + FlLine( + color: color ?? this.color, + gradient: gradient ?? this.gradient, + strokeWidth: strokeWidth ?? this.strokeWidth, + dashArray: dashArray ?? this.dashArray, + ); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + color, + gradient, + strokeWidth, + dashArray, + ]; +} + +/// holds information about touched spot on the axis based charts. +abstract class TouchedSpot with EquatableMixin { + /// [spot] represents the spot inside our axis based chart, + /// 0, 0 is bottom left, and 1, 1 is top right. + /// + /// [offset] is the touch position in device pixels, + /// 0, 0 is top, left, and 1, 1 is bottom right. + TouchedSpot( + this.spot, + this.offset, + ); + + /// Represents the spot inside our axis based chart, + /// 0, 0 is bottom left, and 1, 1 is top right. + final FlSpot spot; + + /// Represents the touch position in device pixels, + /// 0, 0 is top, left, and 1, 1 is bottom right. + final Offset offset; + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + spot, + offset, + ]; +} + +/// Holds data for rendering horizontal and vertical range annotations. +class RangeAnnotations with EquatableMixin { + /// Axis based charts can annotate some horizontal and vertical regions, + /// using [horizontalRangeAnnotations], and [verticalRangeAnnotations] respectively. + const RangeAnnotations({ + this.horizontalRangeAnnotations = const [], + this.verticalRangeAnnotations = const [], + }); + + final List horizontalRangeAnnotations; + final List verticalRangeAnnotations; + + /// Lerps a [RangeAnnotations] based on [t] value, check [Tween.lerp]. + static RangeAnnotations lerp( + RangeAnnotations a, + RangeAnnotations b, + double t, + ) => + RangeAnnotations( + horizontalRangeAnnotations: lerpHorizontalRangeAnnotationList( + a.horizontalRangeAnnotations, + b.horizontalRangeAnnotations, + t, + )!, + verticalRangeAnnotations: lerpVerticalRangeAnnotationList( + a.verticalRangeAnnotations, + b.verticalRangeAnnotations, + t, + )!, + ); + + /// Copies current [RangeAnnotations] to a new [RangeAnnotations], + /// and replaces provided values. + RangeAnnotations copyWith({ + List? horizontalRangeAnnotations, + List? verticalRangeAnnotations, + }) => + RangeAnnotations( + horizontalRangeAnnotations: + horizontalRangeAnnotations ?? this.horizontalRangeAnnotations, + verticalRangeAnnotations: + verticalRangeAnnotations ?? this.verticalRangeAnnotations, + ); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + horizontalRangeAnnotations, + verticalRangeAnnotations, + ]; +} + +/// Defines an annotation region in y (vertical) axis. +class HorizontalRangeAnnotation with EquatableMixin { + /// Annotates a horizontal region from most left to most right point of the chart, and + /// from [y1] to [y2], and fills the area with [color] or [gradient]. + HorizontalRangeAnnotation({ + required this.y1, + required this.y2, + Color? color, + this.gradient, + }) : color = color ?? + ((color == null && gradient == null) ? Colors.white : null); + + /// Determines starting point in vertical (y) axis. + final double y1; + + /// Determines ending point in vertical (y) axis. + final double y2; + + /// If provided, this [HorizontalRangeAnnotation] draws with this [color] + /// Otherwise we use [gradient] to draw the background. + /// It draws with [gradient] if you provide both [color] and [gradient]. + /// If none is provided, it draws with a white color. + final Color? color; + + /// If provided, this [HorizontalRangeAnnotation] draws with this [gradient] + /// Otherwise we use [color] to draw the background. + /// It draws with [gradient] if you provide both [color] and [gradient]. + /// If none is provided, it draws with a white color. + final Gradient? gradient; + + /// Lerps a [HorizontalRangeAnnotation] based on [t] value, check [Tween.lerp]. + static HorizontalRangeAnnotation lerp( + HorizontalRangeAnnotation a, + HorizontalRangeAnnotation b, + double t, + ) => + HorizontalRangeAnnotation( + y1: lerpDouble(a.y1, b.y1, t)!, + y2: lerpDouble(a.y2, b.y2, t)!, + color: Color.lerp(a.color, b.color, t), + gradient: Gradient.lerp(a.gradient, b.gradient, t), + ); + + /// Copies current [HorizontalRangeAnnotation] to a new [HorizontalRangeAnnotation], + /// and replaces provided values. + HorizontalRangeAnnotation copyWith({ + double? y1, + double? y2, + Color? color, + Gradient? gradient, + }) => + HorizontalRangeAnnotation( + y1: y1 ?? this.y1, + y2: y2 ?? this.y2, + color: color ?? this.color, + gradient: gradient ?? this.gradient, + ); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + y1, + y2, + color, + gradient, + ]; +} + +/// Defines an annotation region in x (horizontal) axis. +class VerticalRangeAnnotation with EquatableMixin { + /// Annotates a vertical region from most bottom to most top point of the chart, and + /// from [x1] to [x2], and fills the area with [color] or [gradient]. + VerticalRangeAnnotation({ + required this.x1, + required this.x2, + Color? color, + this.gradient, + }) : color = color ?? + ((color == null && gradient == null) ? Colors.white : null); + + /// Determines starting point in horizontal (x) axis. + final double x1; + + /// Determines ending point in horizontal (x) axis. + final double x2; + + /// If provided, this [VerticalRangeAnnotation] draws with this [color] + /// Otherwise we use [gradient] to draw the background. + /// It draws with [gradient] if you provide both [color] and [gradient]. + /// If none is provided, it draws with a white color. + final Color? color; + + /// If provided, this [VerticalRangeAnnotation] draws with this [gradient] + /// Otherwise we use [color] to draw the background. + /// It draws with [gradient] if you provide both [color] and [gradient]. + /// If none is provided, it draws with a white color. + final Gradient? gradient; + + /// Lerps a [VerticalRangeAnnotation] based on [t] value, check [Tween.lerp]. + static VerticalRangeAnnotation lerp( + VerticalRangeAnnotation a, + VerticalRangeAnnotation b, + double t, + ) => + VerticalRangeAnnotation( + x1: lerpDouble(a.x1, b.x1, t)!, + x2: lerpDouble(a.x2, b.x2, t)!, + color: Color.lerp(a.color, b.color, t), + gradient: Gradient.lerp(a.gradient, b.gradient, t), + ); + + /// Copies current [VerticalRangeAnnotation] to a new [VerticalRangeAnnotation], + /// and replaces provided values. + VerticalRangeAnnotation copyWith({ + double? x1, + double? x2, + Color? color, + Gradient? gradient, + }) => + VerticalRangeAnnotation( + x1: x1 ?? this.x1, + x2: x2 ?? this.x2, + color: color ?? this.color, + gradient: gradient ?? this.gradient, + ); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + x1, + x2, + color, + gradient, + ]; +} + +/// Holds data for drawing extra horizontal lines. +/// +/// [LineChart] draws some [HorizontalLine] (set by [LineChartData.extraLinesData]), +/// in below or above of everything, it draws from left to right side of the chart. +class HorizontalLine extends FlLine with EquatableMixin { + /// [LineChart] draws horizontal lines from left to right side of the chart + /// in the provided [y] value, and color it using [color]. + /// You can define the thickness using [strokeWidth] + /// + /// It draws a [label] over it. + /// + /// You can have a dashed line by filling [dashArray] with dash size and space respectively. + /// + /// It draws an image in left side of the chart, use [sizedPicture] for vectors, + /// or [image] for any kind of image. + HorizontalLine({ + required this.y, + HorizontalLineLabel? label, + super.color, + super.gradient, + super.strokeWidth, + super.dashArray, + this.image, + this.sizedPicture, + this.strokeCap = StrokeCap.butt, + }) : label = label ?? HorizontalLineLabel(); + + /// Draws from left to right of the chart using the [y] value. + final double y; + + /// Use it for any kind of image, to draw it in left side of the chart. + final Image? image; + + /// Use it for vector images, to draw it in left side of the chart. + final SizedPicture? sizedPicture; + + /// Draws a text label over the line. + final HorizontalLineLabel label; + + /// if not drawing dash line, then this is the StrokeCap for the line. + /// i.e. if the two ends of the line is round or butt or square. + final StrokeCap strokeCap; + + /// Lerps a [HorizontalLine] based on [t] value, check [Tween.lerp]. + static HorizontalLine lerp(HorizontalLine a, HorizontalLine b, double t) => + HorizontalLine( + y: lerpDouble(a.y, b.y, t)!, + label: HorizontalLineLabel.lerp(a.label, b.label, t), + color: Color.lerp(a.color, b.color, t), + gradient: Gradient.lerp(a.gradient, b.gradient, t), + strokeWidth: lerpDouble(a.strokeWidth, b.strokeWidth, t)!, + dashArray: lerpIntList(a.dashArray, b.dashArray, t), + image: b.image, + sizedPicture: b.sizedPicture, + strokeCap: b.strokeCap, + ); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + y, + label, + color, + strokeWidth, + dashArray, + image, + sizedPicture, + strokeCap, + ]; +} + +/// Holds data for drawing extra vertical lines. +/// +/// [LineChart] draws some [VerticalLine] (set by [LineChartData.extraLinesData]), +/// in below or above of everything, it draws from bottom to top side of the chart. +class VerticalLine extends FlLine with EquatableMixin { + /// [LineChart] draws vertical lines from bottom to top side of the chart + /// in the provided [x] value, and color it using [color]. + /// You can define the thickness using [strokeWidth] + /// + /// It draws a [label] over it. + /// + /// You can have a dashed line by filling [dashArray] with dash size and space respectively. + /// + /// It draws an image in bottom side of the chart, use [sizedPicture] for vectors, + /// or [image] for any kind of image. + VerticalLine({ + required this.x, + VerticalLineLabel? label, + super.color, + super.gradient, + super.strokeWidth, + super.dashArray, + this.image, + this.sizedPicture, + this.strokeCap = StrokeCap.butt, + }) : label = label ?? VerticalLineLabel(); + + /// Draws from bottom to top of the chart using the [x] value. + final double x; + + /// Use it for any kind of image, to draw it in bottom side of the chart. + final Image? image; + + /// Use it for vector images, to draw it in bottom side of the chart. + final SizedPicture? sizedPicture; + + /// Draws a text label over the line. + final VerticalLineLabel label; + + /// if not drawing dash line, then this is the StrokeCap for the line. + /// i.e. if the two ends of the line is round or butt or square. + final StrokeCap strokeCap; + + /// Lerps a [VerticalLine] based on [t] value, check [Tween.lerp]. + static VerticalLine lerp(VerticalLine a, VerticalLine b, double t) => + VerticalLine( + x: lerpDouble(a.x, b.x, t)!, + label: VerticalLineLabel.lerp(a.label, b.label, t), + color: Color.lerp(a.color, b.color, t), + gradient: Gradient.lerp(a.gradient, b.gradient, t), + strokeWidth: lerpDouble(a.strokeWidth, b.strokeWidth, t)!, + dashArray: lerpIntList(a.dashArray, b.dashArray, t), + image: b.image, + sizedPicture: b.sizedPicture, + strokeCap: b.strokeCap, + ); + + /// Copies current [VerticalLine] to a new [VerticalLine] + /// and replaces provided values. + VerticalLine copyVerticalLineWith({ + double? x, + VerticalLineLabel? label, + Color? color, + double? strokeWidth, + List? dashArray, + Image? image, + SizedPicture? sizedPicture, + StrokeCap? strokeCap, + }) => + VerticalLine( + x: x ?? this.x, + label: label ?? this.label, + color: color ?? this.color, + strokeWidth: strokeWidth ?? this.strokeWidth, + dashArray: dashArray ?? this.dashArray, + image: image ?? this.image, + sizedPicture: sizedPicture ?? this.sizedPicture, + strokeCap: strokeCap ?? this.strokeCap, + ); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + x, + label, + color, + strokeWidth, + dashArray, + image, + sizedPicture, + strokeCap, + ]; +} + +/// Draws a title on the [HorizontalLine] +class HorizontalLineLabel extends FlLineLabel with EquatableMixin { + /// Draws a title on the [HorizontalLine], align it with [alignment] over the line, + /// applies [padding] for spaces, and applies [style for changing color, + /// size, ... of the text. + /// Drawing text will retrieve through [labelResolver], + /// you can override it with your custom data. + /// [show] determines showing label or not. + /// [direction] determines if the direction of the text should be horizontal or vertical. + HorizontalLineLabel({ + super.padding = const EdgeInsets.all(6), + super.style, + super.alignment = Alignment.topLeft, + super.show = false, + super.direction = LabelDirection.horizontal, + this.labelResolver = HorizontalLineLabel.defaultLineLabelResolver, + }); + + /// Resolves a label for showing. + final String Function(HorizontalLine) labelResolver; + + /// Returns the [HorizontalLine.y] as the drawing label. + static String defaultLineLabelResolver(HorizontalLine line) => + line.y.toStringAsFixed(1); + + /// Lerps a [HorizontalLineLabel] based on [t] value, check [Tween.lerp]. + static HorizontalLineLabel lerp( + HorizontalLineLabel a, + HorizontalLineLabel b, + double t, + ) => + HorizontalLineLabel( + padding: EdgeInsets.lerp( + a.padding as EdgeInsets, + b.padding as EdgeInsets, + t, + )!, + style: TextStyle.lerp(a.style, b.style, t), + alignment: Alignment.lerp(a.alignment, b.alignment, t)!, + labelResolver: b.labelResolver, + show: b.show, + direction: b.direction, + ); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + labelResolver, + show, + padding, + style, + alignment, + direction, + ]; +} + +/// Draws a title on the [VerticalLine] +class VerticalLineLabel extends FlLineLabel with EquatableMixin { + /// Draws a title on the [VerticalLine], align it with [alignment] over the line, + /// applies [padding] for spaces, and applies [style for changing color, + /// size, ... of the text. + /// Drawing text will retrieve through [labelResolver], + /// you can override it with your custom data. + /// [show] determines showing label or not. + /// [direction] determines if the direction of the text should be horizontal or vertical. + VerticalLineLabel({ + super.padding = const EdgeInsets.all(6), + super.style = const TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: 14, + ), + super.alignment = Alignment.bottomRight, + super.show = false, + super.direction = LabelDirection.horizontal, + this.labelResolver = VerticalLineLabel.defaultLineLabelResolver, + }); + + /// Resolves a label for showing. + final String Function(VerticalLine) labelResolver; + + /// Returns the [VerticalLine.x] as the drawing label. + static String defaultLineLabelResolver(VerticalLine line) => + line.x.toStringAsFixed(1); + + /// Lerps a [VerticalLineLabel] based on [t] value, check [Tween.lerp]. + static VerticalLineLabel lerp( + VerticalLineLabel a, + VerticalLineLabel b, + double t, + ) => + VerticalLineLabel( + padding: EdgeInsets.lerp( + a.padding as EdgeInsets, + b.padding as EdgeInsets, + t, + )!, + style: TextStyle.lerp(a.style, b.style, t), + alignment: Alignment.lerp(a.alignment, b.alignment, t)!, + labelResolver: b.labelResolver, + show: b.show, + direction: b.direction, + ); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + labelResolver, + show, + padding, + style, + alignment, + direction, + ]; +} + +/// Holds data for showing a vector image inside the chart. +/// +/// for example: +/// ```dart +/// Future loadSvg() async { +/// const String rawSvg = 'your svg string'; +/// final DrawableRoot svgRoot = await svg.fromSvgString(rawSvg, rawSvg); +/// final sizedPicture = SizedPicture(svgRoot.toPicture(), 14, 14); +/// return sizedPicture; +/// } +/// ``` +class SizedPicture with EquatableMixin { + /// [picture] is the showing image, + /// it can retrieve from a svg icon, + /// for example: + /// ```dart + /// const String rawSvg = 'your svg string'; + /// final DrawableRoot svgRoot = await svg.fromSvgString(rawSvg, rawSvg); + /// final picture = svgRoot.toPicture() + /// ``` + /// [width] and [height] determines the size of our picture. + SizedPicture(this.picture, this.width, this.height); + + /// Is the showing image. + final Picture picture; + + /// width of our [picture]. + final int width; + + /// height of our [picture]. + final int height; + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + picture, + width, + height, + ]; +} + +/// Draws some straight horizontal or vertical lines in the [LineChart] +class ExtraLinesData with EquatableMixin { + /// [LineChart] draws some straight horizontal or vertical lines, + /// you should set [LineChartData.extraLinesData]. + /// Draws horizontal lines using [horizontalLines], + /// and vertical lines using [verticalLines]. + /// + /// If [extraLinesOnTop] sets true, it draws the line above the main bar lines, otherwise + /// it draws them below the main bar lines. + const ExtraLinesData({ + this.horizontalLines = const [], + this.verticalLines = const [], + this.extraLinesOnTop = true, + }); + + final List horizontalLines; + final List verticalLines; + final bool extraLinesOnTop; + + /// Lerps a [ExtraLinesData] based on [t] value, check [Tween.lerp]. + static ExtraLinesData lerp(ExtraLinesData a, ExtraLinesData b, double t) => + ExtraLinesData( + extraLinesOnTop: b.extraLinesOnTop, + horizontalLines: lerpHorizontalLineList( + a.horizontalLines, + b.horizontalLines, + t, + )!, + verticalLines: lerpVerticalLineList( + a.verticalLines, + b.verticalLines, + t, + )!, + ); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + horizontalLines, + verticalLines, + extraLinesOnTop, + ]; +} + +/// This class contains the interface that all DotPainters should conform to. +abstract class FlDotPainter with EquatableMixin { + const FlDotPainter(); + + /// This method should be overridden to draw the dot shape. + void draw(Canvas canvas, FlSpot spot, Offset offsetInCanvas); + + /// This method should be overridden to return the size of the shape. + Size getSize(FlSpot spot); + + /// Used to show default UIs, for example [defaultScatterTooltipItem] + Color get mainColor; + + FlDotPainter lerp(FlDotPainter a, FlDotPainter b, double t); + + /// Used to implement touch behaviour of this dot, for example, + /// it behaves like a square of [getSize] + /// Check [FlDotCirclePainter.hitTest] for an example of an implementation + bool hitTest( + FlSpot spot, + Offset touched, + Offset center, + double extraThreshold, + ) { + final size = getSize(spot); + final spotRect = Rect.fromCenter( + center: center, + width: size.width, + height: size.height, + ); + final thresholdRect = spotRect.inflate(extraThreshold); + return thresholdRect.contains(touched); + } +} + +/// This class is an implementation of a [FlDotPainter] that draws +/// a circled shape +class FlDotCirclePainter extends FlDotPainter { + /// The color of the circle is determined determined by [color], + /// [radius] determines the radius of the circle. + /// You can have a stroke line around the circle, + /// by setting the thickness with [strokeWidth], + /// and you can change the color of of the stroke with [strokeColor]. + FlDotCirclePainter({ + this.color = Colors.green, + double? radius, + this.strokeColor = const Color.fromRGBO(76, 175, 80, 1), + this.strokeWidth = 0.0, + }) : radius = radius ?? 4.0; + + /// The fill color to use for the circle + final Color color; + + /// Customizes the radius of the circle + final double radius; + + /// The stroke color to use for the circle + final Color strokeColor; + + /// The stroke width to use for the circle + final double strokeWidth; + + /// Implementation of the parent class to draw the circle + @override + void draw(Canvas canvas, FlSpot spot, Offset offsetInCanvas) { + if (strokeWidth != 0.0 && strokeColor.a != 0.0) { + canvas.drawCircle( + offsetInCanvas, + radius + (strokeWidth / 2), + Paint() + ..color = strokeColor + ..strokeWidth = strokeWidth + ..style = PaintingStyle.stroke, + ); + } + canvas.drawCircle( + offsetInCanvas, + radius, + Paint() + ..color = color + ..style = PaintingStyle.fill, + ); + } + + /// Implementation of the parent class to get the size of the circle + @override + Size getSize(FlSpot spot) => Size.fromRadius(radius + strokeWidth); + + @override + Color get mainColor => color; + + FlDotCirclePainter _lerp( + FlDotCirclePainter a, + FlDotCirclePainter b, + double t, + ) => + FlDotCirclePainter( + color: Color.lerp(a.color, b.color, t)!, + radius: lerpDouble(a.radius, b.radius, t), + strokeColor: Color.lerp(a.strokeColor, b.strokeColor, t)!, + strokeWidth: lerpDouble(a.strokeWidth, b.strokeWidth, t)!, + ); + + @override + FlDotPainter lerp(FlDotPainter a, FlDotPainter b, double t) { + if (a is! FlDotCirclePainter || b is! FlDotCirclePainter) { + return b; + } + return _lerp(a, b, t); + } + + @override + bool hitTest( + FlSpot spot, + Offset touched, + Offset center, + double extraThreshold, + ) { + final distance = (touched - center).distance.abs(); + return distance < radius + extraThreshold; + } + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + color, + radius, + strokeColor, + strokeWidth, + ]; +} + +/// This class is an implementation of a [FlDotPainter] that draws +/// a squared shape +class FlDotSquarePainter extends FlDotPainter { + /// The color of the square is determined determined by [color], + /// [size] determines the size of the square. + /// You can have a stroke line around the square, + /// by setting the thickness with [strokeWidth], + /// and you can change the color of of the stroke with [strokeColor]. + FlDotSquarePainter({ + this.color = Colors.green, + this.size = 4.0, + this.strokeColor = const Color.fromRGBO(76, 175, 80, 1), + this.strokeWidth = 1.0, + }); + + /// The fill color to use for the square + final Color color; + + /// Customizes the size of the square + final double size; + + /// The stroke color to use for the square + final Color strokeColor; + + /// The stroke width to use for the square + final double strokeWidth; + + /// Implementation of the parent class to draw the square + @override + void draw(Canvas canvas, FlSpot spot, Offset offsetInCanvas) { + if (strokeWidth != 0.0 && strokeColor.a != 0.0) { + canvas.drawRect( + Rect.fromCircle( + center: offsetInCanvas, + radius: (size / 2) + (strokeWidth / 2), + ), + Paint() + ..color = strokeColor + ..strokeWidth = strokeWidth + ..style = PaintingStyle.stroke, + ); + } + canvas.drawRect( + Rect.fromCircle( + center: offsetInCanvas, + radius: size / 2, + ), + Paint() + ..color = color + ..style = PaintingStyle.fill, + ); + } + + /// Implementation of the parent class to get the size of the square + @override + Size getSize(FlSpot spot) => Size.square(size + strokeWidth); + + @override + Color get mainColor => color; + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + color, + size, + strokeColor, + strokeWidth, + ]; + + FlDotSquarePainter _lerp( + FlDotSquarePainter a, + FlDotSquarePainter b, + double t, + ) => + FlDotSquarePainter( + color: Color.lerp(a.color, b.color, t)!, + size: lerpDouble(a.size, b.size, t)!, + strokeColor: Color.lerp(a.strokeColor, b.strokeColor, t)!, + strokeWidth: lerpDouble(a.strokeWidth, b.strokeWidth, t)!, + ); + + @override + FlDotPainter lerp(FlDotPainter a, FlDotPainter b, double t) { + if (a is! FlDotSquarePainter || b is! FlDotSquarePainter) { + return b; + } + return _lerp(a, b, t); + } +} + +/// This class is an implementation of a [FlDotPainter] that draws +/// a cross (X mark) shape +class FlDotCrossPainter extends FlDotPainter { + /// The [color] and [width] properties determines the color and thickness of the cross shape, + /// [size] determines the width and height of the shape. + FlDotCrossPainter({ + this.color = Colors.green, + this.size = 8.0, + this.width = 2.0, + }); + + /// The fill color to use for the X mark + final Color color; + + /// Determines size (width and height) of shape. + final double size; + + /// Determines thickness of X mark. + final double width; + + /// Implementation of the parent class to draw the cross + @override + void draw(Canvas canvas, FlSpot spot, Offset offsetInCanvas) { + final path = Path() + ..moveTo(offsetInCanvas.dx, offsetInCanvas.dy) + ..relativeMoveTo(-size / 2, -size / 2) + ..relativeLineTo(size, size) + ..moveTo(offsetInCanvas.dx, offsetInCanvas.dy) + ..relativeMoveTo(size / 2, -size / 2) + ..relativeLineTo(-size, size); + + final paint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = width + ..color = color; + + canvas.drawPath(path, paint); + } + + /// Implementation of the parent class to get the size of the circle + @override + Size getSize(FlSpot spot) => Size(size, size); + + @override + Color get mainColor => color; + + FlDotCrossPainter _lerp( + FlDotCrossPainter a, + FlDotCrossPainter b, + double t, + ) => + FlDotCrossPainter( + color: Color.lerp(a.color, b.color, t)!, + size: lerpDouble(a.size, b.size, t)!, + width: lerpDouble(a.width, b.width, t)!, + ); + + @override + FlDotPainter lerp(FlDotPainter a, FlDotPainter b, double t) { + if (a is! FlDotCrossPainter || b is! FlDotCrossPainter) { + return b; + } + return _lerp(a, b, t); + } + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + color, + size, + width, + ]; +} + +/// Holds the information about the error range of a spot +/// +/// We support horizontal and vertical error range/indicator for our axis based +/// charts such as [LineChart], [BarChart] and [PieChart] +/// +/// For example, in [LineChart] you can add [FlSpot.xError] and [FlSpot.yError] +/// in your data points, so we can draw error indicators for them. +/// And it works relative to the point that you are setting the error range +/// +/// For [BarChart], you can set the [BarChartRodData.toYErrorRange] to have +/// vertical error range for each bar. (relative to [BarChartRodData.toY] value) +/// +/// [show] is tru by default, it means that we show +/// the error indicator lines (if you provide them in [FlSpot]s) +/// +/// [painter] is a callback that allows you to return a +/// [FlSpotErrorRangePainter] per each data point which is responsible for +/// drawing the error indicator. You can use the default [FlSimpleErrorPainter] +/// or create your own by extending our abstract [FlSpotErrorRangePainter] +class FlErrorIndicatorData + with EquatableMixin { + const FlErrorIndicatorData({ + this.show = true, + this.painter = _defaultGetSpotRangeErrorPainter, + }); + + /// Determines showing the error indicator or not + final bool show; + + /// A callback that allows you to return a [FlSpotErrorRangePainter] + /// per each data point (for example [FlSpot] in line chart) + final GetSpotRangeErrorPainter painter; + + /// Lerps a [FlErrorIndicatorData] based on [t] value. + static FlErrorIndicatorData lerp( + FlErrorIndicatorData a, + FlErrorIndicatorData b, + double t, + ) => + FlErrorIndicatorData( + show: b.show, + painter: b.painter, + ); + + @override + List get props => [ + show, + painter, + ]; +} + +/// A callback that allows you to return a [FlSpotErrorRangePainter] based on +/// the provided specific data point (for example [FlSpot] in [LineChart]) +/// +/// So [input] is different based on the chart type, +/// for example in [LineChart] it will be [LineChartSpotErrorRangeCallbackInput] +typedef GetSpotRangeErrorPainter + = FlSpotErrorRangePainter Function( + T input, +); + +/// The default [GetSpotRangeErrorPainter] for [FlErrorIndicatorData], +/// it draws a simple and typical error indicator using [FlSimpleErrorPainter] +FlSpotErrorRangePainter _defaultGetSpotRangeErrorPainter( + FlSpotErrorRangeCallbackInput input, +) => + FlSimpleErrorPainter(); + +/// The abstract painter that is responsible for drawing the error range of +/// a point in our axis based charts such as [LineChart] and [BarChart] +/// +/// It has a [draw] method that you should override to draw the error range +/// as you like +/// +/// The default implementation is [FlSpotErrorRangePainter]. It is a simple and +/// common error indicator painter. +/// +/// You can see how does it look in the [example app](https://app.flchart.dev/) +abstract class FlSpotErrorRangePainter with EquatableMixin { + const FlSpotErrorRangePainter(); + + /// Draws the error range of a point in our axis based charts + /// + /// [canvas] is the canvas that you should draw on it + /// [offsetInCanvas] is the absolute position/offset of the point in + /// the canvas that you can use it as your center point + /// [origin] is the relative point point that you should draw + /// the error range on it (it is based on the chart values) + /// [errorRelativeRect] is the relative rect that you should draw the error, + /// it is absolute and you can shift it with [offsetInCanvas] to draw your + /// shape inside it. + /// [axisChartData] is the axis chart data that you can use it to get more + /// information about the chart + /// + /// You can take a look at our default implementation [FlSimpleErrorPainter] + void draw( + Canvas canvas, + Offset offsetInCanvas, + FlSpot origin, + Rect errorRelativeRect, + AxisChartData axisChartData, + ); +} + +/// The default implementation of [FlSpotErrorRangePainter] +/// +/// It draws a simple and common error indicator for the error range of a point +/// in our axis based charts such as [LineChart] and [BarChart] +/// +/// You can see how does it look in the [example app](https://app.flchart.dev/) +/// +/// You can customize the lines using [lineColor], [lineWidth], [capLength], +/// +/// You can customize the text using [showErrorTexts], [errorTextStyle] +/// and [errorTextDirection] +/// +/// You can customize the alignment of the error lines using [crossAlignment] +class FlSimpleErrorPainter extends FlSpotErrorRangePainter with EquatableMixin { + FlSimpleErrorPainter({ + this.lineColor = Colors.white, + this.lineWidth = 1.0, + this.capLength = 8.0, + this.crossAlignment = 0, + this.showErrorTexts = false, + this.errorTextStyle = const TextStyle( + color: Colors.white, + fontSize: 12, + ), + this.errorTextDirection = TextDirection.ltr, + }) { + _linePaint = Paint() + ..color = lineColor + ..strokeWidth = lineWidth + ..style = PaintingStyle.stroke; + assert( + crossAlignment >= -1 && crossAlignment <= 1, + 'crossAlignment must be between -1 (start) and 1 (end)', + ); + } + + /// The color of the error lines + final Color lineColor; + + /// The thickness of the error lines + final double lineWidth; + + /// The length of the cap of the error lines + final double capLength; + + /// The alignment of the error lines, + /// it should be between -1 (start) and 1 (end) + final double crossAlignment; + + /// Determines showing the error texts or not + final bool showErrorTexts; + + /// The style of the error texts + final TextStyle errorTextStyle; + + /// The direction of the error texts + final TextDirection errorTextDirection; + + late final Paint _linePaint; + + @override + void draw( + Canvas canvas, + Offset offsetInCanvas, + FlSpot origin, + Rect errorRelativeRect, + AxisChartData axisChartData, + ) { + final rect = errorRelativeRect.shift(offsetInCanvas); + final hasVerticalError = errorRelativeRect.height != 0; + if (hasVerticalError) { + _drawDirectErrorLine( + canvas, + Offset(offsetInCanvas.dx, rect.top), + Offset(offsetInCanvas.dx, rect.bottom), + ); + + if (showErrorTexts) { + // lower + _drawErrorText( + canvas: canvas, + rect: rect, + isHorizontal: false, + isLower: true, + text: Utils().formatNumber( + axisChartData.minY, + axisChartData.maxY, + origin.y - origin.yError!.lowerBy, + ), + textStyle: errorTextStyle, + ); + + // upper + _drawErrorText( + canvas: canvas, + rect: rect, + isHorizontal: false, + isLower: false, + text: Utils().formatNumber( + axisChartData.minY, + axisChartData.maxY, + origin.y + origin.yError!.upperBy, + ), + textStyle: errorTextStyle, + ); + } + } + + final hasHorizontalError = errorRelativeRect.width != 0; + if (hasHorizontalError) { + _drawDirectErrorLine( + canvas, + Offset(rect.left, offsetInCanvas.dy), + Offset(rect.right, offsetInCanvas.dy), + ); + + if (showErrorTexts) { + // lower + _drawErrorText( + canvas: canvas, + rect: rect, + isHorizontal: true, + isLower: true, + text: Utils().formatNumber( + axisChartData.minX, + axisChartData.maxX, + origin.x - origin.xError!.lowerBy, + ), + textStyle: errorTextStyle, + ); + + // upper + _drawErrorText( + canvas: canvas, + rect: rect, + isHorizontal: true, + isLower: false, + text: Utils().formatNumber( + axisChartData.minX, + axisChartData.maxX, + origin.x + origin.xError!.upperBy, + ), + textStyle: errorTextStyle, + ); + } + } + } + + void _drawDirectErrorLine(Canvas canvas, Offset from, Offset to) { + final isLineVertical = from.dx == to.dx; + final mainLineOffset = crossAlignment * capLength; + + if (isLineVertical) { + from = Offset(from.dx + mainLineOffset, from.dy); + to = Offset(to.dx + mainLineOffset, to.dy); + } else { + from = Offset(from.dx, from.dy + mainLineOffset); + to = Offset(to.dx, to.dy + mainLineOffset); + } + + canvas.drawLine( + from, + to, + _linePaint, + ); + + final t = (crossAlignment + 1) / 2; + final end = capLength - lerpDouble(0, capLength, t)!; + final start = capLength - end; + // Draw edge lines + if (isLineVertical) { + canvas + // draw top cap + ..drawLine( + Offset(from.dx - start, from.dy), + Offset(from.dx + end, from.dy), + _linePaint, + ) + // draw bottom cap + ..drawLine( + Offset(to.dx - start, to.dy), + Offset(to.dx + end, to.dy), + _linePaint, + ); + } else { + canvas + // draw left cap + ..drawLine( + Offset(from.dx, from.dy - start), + Offset(from.dx, from.dy + end), + _linePaint, + ) + // draw right cap + ..drawLine( + Offset(to.dx, to.dy - start), + Offset(to.dx, to.dy + end), + _linePaint, + ); + } + } + + void _drawErrorText({ + required Canvas canvas, + required Rect rect, + required bool isHorizontal, + required bool isLower, + required String text, + required TextStyle textStyle, + }) { + final lowerText = TextPainter( + text: TextSpan( + text: text, + style: textStyle, + ), + textDirection: TextDirection.ltr, + )..layout(); + + const spacing = 4.0; + final textX = isHorizontal + ? isLower + ? rect.left - lowerText.width - spacing + : rect.right + spacing + : rect.center.dx - lowerText.width / 2; + + final textY = isHorizontal + ? rect.center.dy - lowerText.height / 2 + : isLower + ? rect.bottom + spacing + : rect.top - lowerText.width - spacing; + + lowerText.paint( + canvas, + Offset( + textX, + textY, + ), + ); + } + + @override + List get props => [ + lineColor, + lineWidth, + capLength, + crossAlignment, + showErrorTexts, + errorTextStyle, + errorTextDirection, + ]; +} + +/// The abstract class that is used as the input of +/// the [GetSpotRangeErrorPainter] callback. +/// +/// So as you know, we have this feature in our axis-based charts and each chart +/// has its own input type, for example in [LineChart] +/// it is [LineChartSpotErrorRangeCallbackInput] (which contains the [FlSpot]) +abstract class FlSpotErrorRangeCallbackInput with EquatableMixin {} + +typedef ValueInCanvasProvider = double Function(double axisValue); + +/// The class to hold the information about showing a specific point +/// in the axis-based charts +/// +/// You can use the [x] and [y] properties to set the point, Otherwise it +/// uses the touch point (if `handleBuiltinTouches` is true) +/// +/// There's a [painter] property that manages the drawing of the point. +/// We have a default implementation of the painter which is +/// [AxisLinesIndicatorPainter], it draws a horizontal and a vertical line +/// that goes through the point. +/// +/// You can override the [painter] by implementing your own +/// [AxisSpotIndicatorPainter] implementation. +/// +/// For more information, look at our default implementation: +/// [AxisLinesIndicatorPainter]. +class AxisSpotIndicator with EquatableMixin { + const AxisSpotIndicator({ + this.x, + this.y, + required this.painter, + }); + + final double? x; + final double? y; + final AxisSpotIndicatorPainter painter; + + /// Lerps a [AxisSpotIndicator] based on [t] value, check [Tween.lerp]. + static AxisSpotIndicator lerp( + AxisSpotIndicator a, + AxisSpotIndicator b, + double t, + ) => + AxisSpotIndicator( + x: lerpDouble(a.x, b.x, t), + y: lerpDouble(a.y, b.y, t), + painter: a.painter.lerp(b.painter, t), + ); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + x, + y, + painter, + ]; +} + +/// The abstract class that is used to draw the point indicator +/// +/// You can create your own custom painter by extending this class +/// and implementing the [paint] method. +/// +/// You can also use the default implementation which is +/// [AxisLinesIndicatorPainter], it draws a horizontal and a vertical line +/// that goes through the point. +abstract class AxisSpotIndicatorPainter { + const AxisSpotIndicatorPainter(); + + /// Draws the point indicator + void paint( + BuildContext context, + Canvas canvas, + Size viewSize, + AxisSpotIndicator axisPointIndicator, + ValueInCanvasProvider xInCanvasProvider, + ValueInCanvasProvider yInCanvasProvider, + AxisChartData axisChartData, + ); + + /// Lerps a [AxisSpotIndicatorPainter] based on [t] value, check [Tween.lerp]. + AxisSpotIndicatorPainter lerp( + AxisSpotIndicatorPainter b, + double t, + ); +} + +/// The default implementation of the [AxisSpotIndicatorPainter] +/// +/// It draws a horizontal and a vertical line that goes through the point +class AxisLinesIndicatorPainter extends AxisSpotIndicatorPainter { + AxisLinesIndicatorPainter({ + required this.verticalLineProvider, + required this.horizontalLineProvider, + }); + + final VerticalLine? Function(double x)? verticalLineProvider; + + final HorizontalLine? Function(double y)? horizontalLineProvider; + + /// The paint object that is used to draw the lines + final _linePaint = Paint(); + + /// The paint object that is used to draw the images + final _imagePaint = Paint(); + + void _drawHorizontalLine( + BuildContext context, + CanvasWrapper canvasWrapper, + HorizontalLine line, + Offset from, + Offset to, + ) { + _linePaint + ..setColorOrGradientForLine( + line.color, + line.gradient, + from: from, + to: to, + ) + ..style = PaintingStyle.stroke + ..strokeWidth = line.strokeWidth + ..transparentIfWidthIsZero() + ..strokeCap = line.strokeCap; + + canvasWrapper.drawDashedLine( + from, + to, + _linePaint, + line.dashArray, + ); + + if (line.sizedPicture != null) { + final centerX = line.sizedPicture!.width / 2; + final centerY = line.sizedPicture!.height / 2; + final xPosition = centerX; + final yPosition = to.dy - centerY; + + canvasWrapper + ..save() + ..translate(xPosition, yPosition) + ..drawPicture(line.sizedPicture!.picture) + ..restore(); + } + + if (line.image != null) { + final centerX = line.image!.width / 2; + final centerY = line.image!.height / 2; + final centeredImageOffset = Offset(centerX, to.dy - centerY); + canvasWrapper.drawImage( + line.image!, + centeredImageOffset, + _imagePaint, + ); + } + + if (line.label.show) { + final label = line.label; + final style = + TextStyle(fontSize: 11, color: line.color).merge(label.style); + final padding = label.padding as EdgeInsets; + + final span = TextSpan( + text: label.labelResolver(line), + style: Utils().getThemeAwareTextStyle(context, style), + ); + + final tp = TextPainter( + text: span, + textDirection: TextDirection.ltr, + )..layout(); + + switch (label.direction) { + case LabelDirection.horizontal: + canvasWrapper.drawText( + tp, + label.alignment.withinRect( + Rect.fromLTRB( + from.dx + padding.left, + from.dy - padding.bottom - tp.height, + to.dx - padding.right - tp.width, + to.dy + padding.top, + ), + ), + ); + case LabelDirection.vertical: + canvasWrapper.drawVerticalText( + tp, + label.alignment.withinRect( + Rect.fromLTRB( + from.dx + padding.left + tp.height, + from.dy - padding.bottom - tp.width, + to.dx - padding.right, + to.dy + padding.top, + ), + ), + ); + } + } + } + + void _drawVerticalLine( + BuildContext context, + CanvasWrapper canvasWrapper, + VerticalLine line, + Offset from, + Offset to, + ) { + final viewSize = canvasWrapper.size; + + _linePaint + ..setColorOrGradientForLine( + line.color, + line.gradient, + from: from, + to: to, + ) + ..strokeWidth = line.strokeWidth + ..style = PaintingStyle.stroke + ..transparentIfWidthIsZero() + ..strokeCap = line.strokeCap; + + canvasWrapper.drawDashedLine( + from, + to, + _linePaint, + line.dashArray, + ); + + if (line.sizedPicture != null) { + final centerX = line.sizedPicture!.width / 2; + final centerY = line.sizedPicture!.height / 2; + final xPosition = to.dx - centerX; + final yPosition = viewSize.height - centerY; + + canvasWrapper + ..save() + ..translate(xPosition, yPosition) + ..drawPicture(line.sizedPicture!.picture) + ..restore(); + } + + if (line.image != null) { + final centerX = line.image!.width / 2; + final centerY = line.image!.height + 2; + final centeredImageOffset = + Offset(to.dx - centerX, viewSize.height - centerY); + canvasWrapper.drawImage( + line.image!, + centeredImageOffset, + _imagePaint, + ); + } + + if (line.label.show) { + final label = line.label; + final style = + TextStyle(fontSize: 11, color: line.color).merge(label.style); + final padding = label.padding as EdgeInsets; + + final span = TextSpan( + text: label.labelResolver(line), + style: Utils().getThemeAwareTextStyle(context, style), + ); + + final tp = TextPainter( + text: span, + textDirection: TextDirection.ltr, + )..layout(); + + switch (label.direction) { + case LabelDirection.horizontal: + canvasWrapper.drawText( + tp, + label.alignment.withinRect( + Rect.fromLTRB( + from.dx - padding.right - tp.width, + from.dy + padding.top, + to.dx + padding.left, + to.dy - padding.bottom - tp.height, + ), + ), + ); + case LabelDirection.vertical: + canvasWrapper.drawVerticalText( + tp, + label.alignment.withinRect( + Rect.fromLTRB( + from.dx - padding.right, + from.dy + padding.top, + to.dx + padding.left + tp.height, + to.dy - padding.bottom - tp.width, + ), + ), + ); + } + } + } + + @override + void paint( + BuildContext context, + Canvas canvas, + Size viewSize, + AxisSpotIndicator axisPointIndicator, + ValueInCanvasProvider xInCanvasProvider, + ValueInCanvasProvider yInCanvasProvider, + AxisChartData axisChartData, + ) { + final canvasWrapper = CanvasWrapper(canvas, viewSize); + final horizontalLine = + axisPointIndicator.y == null || horizontalLineProvider == null + ? null + : horizontalLineProvider!(axisPointIndicator.y!); + if (horizontalLine != null) { + final left = Offset( + xInCanvasProvider(axisChartData.minX), + yInCanvasProvider(horizontalLine.y), + ); + final right = Offset( + xInCanvasProvider(axisChartData.maxX), + yInCanvasProvider(horizontalLine.y), + ); + _drawHorizontalLine( + context, + canvasWrapper, + horizontalLine, + left, + right, + ); + } + + final verticalLine = + axisPointIndicator.x == null || verticalLineProvider == null + ? null + : verticalLineProvider!(axisPointIndicator.x!); + if (verticalLine != null) { + final top = Offset( + xInCanvasProvider(verticalLine.x), + yInCanvasProvider(axisChartData.maxY), + ); + final bottom = Offset( + xInCanvasProvider(verticalLine.x), + yInCanvasProvider(axisChartData.minY), + ); + + _drawVerticalLine( + context, + canvasWrapper, + verticalLine, + top, + bottom, + ); + } + } + + /// Lerps a [AxisLinesIndicatorPainter] based on [t] value, check [Tween.lerp]. + AxisLinesIndicatorPainter _lerp( + AxisLinesIndicatorPainter b, + double t, + ) => + AxisLinesIndicatorPainter( + horizontalLineProvider: b.horizontalLineProvider, + verticalLineProvider: b.verticalLineProvider, + ); + + /// Used for equality check, see [EquatableMixin]. + @override + AxisSpotIndicatorPainter lerp( + AxisSpotIndicatorPainter b, + double t, + ) { + if (b is! AxisLinesIndicatorPainter) { + return b; + } + return _lerp(b, t); + } +} diff --git a/lib/src/chart/base/axis_chart/axis_chart_extensions.dart b/lib/src/chart/base/axis_chart/axis_chart_extensions.dart new file mode 100644 index 0000000..db05738 --- /dev/null +++ b/lib/src/chart/base/axis_chart/axis_chart_extensions.dart @@ -0,0 +1,23 @@ +import 'package:fl_chart/fl_chart.dart'; + +extension FlSpotListExtension on List { + /// Splits a line by [FlSpot.nullSpot] values inside it. + List> splitByNullSpots() { + final barList = >[[]]; + + // handle nullability by splitting off the list into multiple + // separate lists when separated by nulls + for (final spot in this) { + if (spot.isNotNull()) { + barList.last.add(spot); + } else if (barList.last.isNotEmpty) { + barList.add([]); + } + } + // remove last item if one or more last spots were null + if (barList.last.isEmpty) { + barList.removeLast(); + } + return barList; + } +} diff --git a/lib/src/chart/base/axis_chart/axis_chart_helper.dart b/lib/src/chart/base/axis_chart/axis_chart_helper.dart new file mode 100644 index 0000000..d9179fb --- /dev/null +++ b/lib/src/chart/base/axis_chart/axis_chart_helper.dart @@ -0,0 +1,104 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/utils/utils.dart'; +import 'package:flutter/material.dart'; + +class AxisChartHelper { + factory AxisChartHelper() { + return _singleton; + } + + AxisChartHelper._internal(); + + static final _singleton = AxisChartHelper._internal(); + + /// Iterates over an axis from [min] to [max]. + /// + /// [interval] determines each step + /// + /// If [minIncluded] is true, it starts from [min] value, + /// otherwise it starts from [min] + [interval] + /// + /// If [maxIncluded] is true, it ends at [max] value, + /// otherwise it ends at [max] - [interval] + Iterable iterateThroughAxis({ + required double min, + bool minIncluded = true, + required double max, + bool maxIncluded = true, + required double baseLine, + required double interval, + }) sync* { + final initialValue = Utils() + .getBestInitialIntervalValue(min, max, interval, baseline: baseLine); + var axisSeek = initialValue; + final firstPositionOverlapsWithMin = axisSeek == min; + if (!minIncluded && firstPositionOverlapsWithMin) { + // If initial value is equal to data minimum, + // move first label one interval further + axisSeek += interval; + } + final diff = max - min; + final count = diff ~/ interval; + final lastPosition = initialValue + (count * interval); + final lastPositionOverlapsWithMax = lastPosition == max; + final end = + !maxIncluded && lastPositionOverlapsWithMax ? max - interval : max; + + final epsilon = interval / 100000; + if (minIncluded && !firstPositionOverlapsWithMin) { + // Data minimum shall be included and is not yet covered + yield min; + } + while (axisSeek <= end + epsilon) { + yield axisSeek; + axisSeek += interval; + } + if (maxIncluded && !lastPositionOverlapsWithMax) { + yield max; + } + } + + /// Calculate translate offset to keep [SideTitle] child + /// placed inside its corresponding axis. + /// The offset will translate the child to the closest edge inside + /// of the corresponding axis bounding box + Offset calcFitInsideOffset({ + required AxisSide axisSide, + required double? childSize, + required double parentAxisSize, + required double axisPosition, + required double distanceFromEdge, + }) { + if (childSize == null) return Offset.zero; + + // Find title alignment along its axis + final axisMid = parentAxisSize / 2; + final mainAxisAlignment = (axisPosition - axisMid).isNegative + ? MainAxisAlignment.start + : MainAxisAlignment.end; + + // Find if child widget overflowed outside the chart + late bool isOverflowed; + if (mainAxisAlignment == MainAxisAlignment.start) { + isOverflowed = (axisPosition - (childSize / 2)).isNegative; + } else { + isOverflowed = (axisPosition + (childSize / 2)) > parentAxisSize; + } + + if (isOverflowed == false) return Offset.zero; + + // Calc offset if child overflowed + late double offset; + if (mainAxisAlignment == MainAxisAlignment.start) { + offset = (childSize / 2) - axisPosition + distanceFromEdge; + } else { + offset = + -(childSize / 2) + (parentAxisSize - axisPosition) - distanceFromEdge; + } + + return switch (axisSide) { + AxisSide.left || AxisSide.right => Offset(0, offset), + AxisSide.top || AxisSide.bottom => Offset(offset, 0), + }; + } +} diff --git a/lib/src/chart/base/axis_chart/axis_chart_painter.dart b/lib/src/chart/base/axis_chart/axis_chart_painter.dart new file mode 100644 index 0000000..40659f6 --- /dev/null +++ b/lib/src/chart/base/axis_chart/axis_chart_painter.dart @@ -0,0 +1,570 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/bar_chart/bar_chart_painter.dart'; +import 'package:fl_chart/src/chart/base/axis_chart/axis_chart_helper.dart'; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart'; +import 'package:fl_chart/src/chart/line_chart/line_chart_painter.dart'; +import 'package:fl_chart/src/extensions/paint_extension.dart'; +import 'package:fl_chart/src/utils/canvas_wrapper.dart'; +import 'package:fl_chart/src/utils/utils.dart'; +import 'package:flutter/material.dart'; + +/// This class is responsible to draw the grid behind all axis base charts. +/// also we have two useful function [getPixelX] and [getPixelY] that used +/// in child classes -> [BarChartPainter], [LineChartPainter] +/// [dataList] is the currently showing data (it may produced by an animation using lerp function), +/// [targetData] is the target data, that animation is going to show (if animating) +abstract class AxisChartPainter + extends BaseChartPainter { + AxisChartPainter() { + _gridPaint = Paint()..style = PaintingStyle.stroke; + + _backgroundPaint = Paint()..style = PaintingStyle.fill; + + _rangeAnnotationPaint = Paint()..style = PaintingStyle.fill; + + _extraLinesPaint = Paint()..style = PaintingStyle.stroke; + + _imagePaint = Paint(); + + _clipPaint = Paint(); + } + + late Paint _gridPaint; + late Paint _backgroundPaint; + late Paint _extraLinesPaint; + late Paint _imagePaint; + late Paint _clipPaint; + + /// [_rangeAnnotationPaint] draws range annotations; + late Paint _rangeAnnotationPaint; + + /// Paints [AxisChartData] into the provided canvas. + @override + void paint( + BuildContext context, + CanvasWrapper canvasWrapper, + PaintHolder holder, + ) { + super.paint(context, canvasWrapper, holder); + drawBackground(canvasWrapper, holder); + drawRangeAnnotation(canvasWrapper, holder); + drawGrid(canvasWrapper, holder); + } + + @visibleForTesting + void drawGrid(CanvasWrapper canvasWrapper, PaintHolder holder) { + final data = holder.data; + if (!data.gridData.show) { + return; + } + final viewSize = canvasWrapper.size; + // Show Vertical Grid + if (data.gridData.drawVerticalLine) { + final verticalInterval = data.gridData.verticalInterval ?? + Utils().getEfficientInterval( + viewSize.width, + data.horizontalDiff, + ); + final axisValues = AxisChartHelper().iterateThroughAxis( + min: data.minX, + minIncluded: false, + max: data.maxX, + maxIncluded: false, + baseLine: data.baselineX, + interval: verticalInterval, + ); + for (final axisValue in axisValues) { + if (!data.gridData.checkToShowVerticalLine(axisValue)) { + continue; + } + final bothX = getPixelX(axisValue, viewSize, holder); + final x1 = bothX; + const y1 = 0.0; + final x2 = bothX; + final y2 = viewSize.height; + final from = Offset(x1, y1); + final to = Offset(x2, y2); + + final flLineStyle = data.gridData.getDrawingVerticalLine(axisValue); + _gridPaint + ..setColorOrGradientForLine( + flLineStyle.color, + flLineStyle.gradient, + from: from, + to: to, + ) + ..strokeWidth = flLineStyle.strokeWidth + ..transparentIfWidthIsZero(); + + canvasWrapper.drawDashedLine( + from, + to, + _gridPaint, + flLineStyle.dashArray, + ); + } + } + + // Show Horizontal Grid + if (data.gridData.drawHorizontalLine) { + final horizontalInterval = data.gridData.horizontalInterval ?? + Utils().getEfficientInterval(viewSize.height, data.verticalDiff); + + final axisValues = AxisChartHelper().iterateThroughAxis( + min: data.minY, + minIncluded: false, + max: data.maxY, + maxIncluded: false, + baseLine: data.baselineY, + interval: horizontalInterval, + ); + for (final axisValue in axisValues) { + if (!data.gridData.checkToShowHorizontalLine(axisValue)) { + continue; + } + final flLine = data.gridData.getDrawingHorizontalLine(axisValue); + + final bothY = getPixelY(axisValue, viewSize, holder); + const x1 = 0.0; + final y1 = bothY; + final x2 = viewSize.width; + final y2 = bothY; + final from = Offset(x1, y1); + final to = Offset(x2, y2); + + _gridPaint + ..setColorOrGradientForLine( + flLine.color, + flLine.gradient, + from: from, + to: to, + ) + ..strokeWidth = flLine.strokeWidth + ..transparentIfWidthIsZero(); + + canvasWrapper.drawDashedLine( + from, + to, + _gridPaint, + flLine.dashArray, + ); + } + } + } + + /// This function draws a colored background behind the chart. + @visibleForTesting + void drawBackground(CanvasWrapper canvasWrapper, PaintHolder holder) { + final data = holder.data; + if (data.backgroundColor.a == 0.0) { + return; + } + + final viewSize = canvasWrapper.size; + _backgroundPaint.color = data.backgroundColor; + canvasWrapper.drawRect( + Rect.fromLTWH(0, 0, viewSize.width, viewSize.height), + _backgroundPaint, + ); + } + + @visibleForTesting + void drawRangeAnnotation(CanvasWrapper canvasWrapper, PaintHolder holder) { + final data = holder.data; + final viewSize = canvasWrapper.size; + + if (data.rangeAnnotations.verticalRangeAnnotations.isNotEmpty) { + for (final annotation in data.rangeAnnotations.verticalRangeAnnotations) { + final from = Offset(getPixelX(annotation.x1, viewSize, holder), 0); + final to = Offset( + getPixelX(annotation.x2, viewSize, holder), + viewSize.height, + ); + + final rect = Rect.fromPoints(from, to); + + _rangeAnnotationPaint.setColorOrGradient( + annotation.color, + annotation.gradient, + rect, + ); + + canvasWrapper.drawRect(rect, _rangeAnnotationPaint); + } + } + + if (data.rangeAnnotations.horizontalRangeAnnotations.isNotEmpty) { + for (final annotation + in data.rangeAnnotations.horizontalRangeAnnotations) { + final from = Offset(0, getPixelY(annotation.y1, viewSize, holder)); + final to = Offset( + viewSize.width, + getPixelY(annotation.y2, viewSize, holder), + ); + + final rect = Rect.fromPoints(from, to); + + _rangeAnnotationPaint.setColorOrGradient( + annotation.color, + annotation.gradient, + rect, + ); + + canvasWrapper.drawRect(rect, _rangeAnnotationPaint); + } + } + } + + void drawExtraLines( + BuildContext context, + CanvasWrapper canvasWrapper, + PaintHolder holder, + ) { + if (holder.chartVirtualRect != null) { + canvasWrapper.restore(); + } + + super.paint(context, canvasWrapper, holder); + final data = holder.data; + final viewSize = canvasWrapper.size; + + if (data.extraLinesData.horizontalLines.isNotEmpty) { + drawHorizontalLines(context, canvasWrapper, holder, viewSize); + } + + if (data.extraLinesData.verticalLines.isNotEmpty) { + drawVerticalLines(context, canvasWrapper, holder, viewSize); + } + + if (holder.chartVirtualRect != null) { + canvasWrapper + ..saveLayer( + Offset.zero & canvasWrapper.size, + _clipPaint, + ) + ..clipRect(Offset.zero & canvasWrapper.size); + } + } + + void drawHorizontalLines( + BuildContext context, + CanvasWrapper canvasWrapper, + PaintHolder holder, + Size viewSize, + ) { + for (final line in holder.data.extraLinesData.horizontalLines) { + final from = Offset(0, getPixelY(line.y, viewSize, holder)); + final to = Offset(viewSize.width, getPixelY(line.y, viewSize, holder)); + + final isLineOutsideOfChart = from.dy < 0 || + to.dy < 0 || + from.dy > viewSize.height || + to.dy > viewSize.height; + + if (!isLineOutsideOfChart) { + _extraLinesPaint + ..setColorOrGradientForLine( + line.color, + line.gradient, + from: from, + to: to, + ) + ..strokeWidth = line.strokeWidth + ..transparentIfWidthIsZero() + ..strokeCap = line.strokeCap; + + canvasWrapper.drawDashedLine( + from, + to, + _extraLinesPaint, + line.dashArray, + ); + + if (line.sizedPicture != null) { + final centerX = line.sizedPicture!.width / 2; + final centerY = line.sizedPicture!.height / 2; + final xPosition = centerX; + final yPosition = to.dy - centerY; + + canvasWrapper + ..save() + ..translate(xPosition, yPosition) + ..drawPicture(line.sizedPicture!.picture) + ..restore(); + } + + if (line.image != null) { + final centerX = line.image!.width / 2; + final centerY = line.image!.height / 2; + final centeredImageOffset = Offset(centerX, to.dy - centerY); + canvasWrapper.drawImage( + line.image!, + centeredImageOffset, + _imagePaint, + ); + } + + if (line.label.show) { + final label = line.label; + final style = + TextStyle(fontSize: 11, color: line.color).merge(label.style); + final padding = label.padding as EdgeInsets; + + final span = TextSpan( + text: label.labelResolver(line), + style: Utils().getThemeAwareTextStyle(context, style), + ); + + final tp = TextPainter( + text: span, + textDirection: TextDirection.ltr, + )..layout(); + + switch (label.direction) { + case LabelDirection.horizontal: + canvasWrapper.drawText( + tp, + label.alignment.withinRect( + Rect.fromLTRB( + from.dx + padding.left, + from.dy - padding.bottom - tp.height, + to.dx - padding.right - tp.width, + to.dy + padding.top, + ), + ), + ); + case LabelDirection.vertical: + canvasWrapper.drawVerticalText( + tp, + label.alignment.withinRect( + Rect.fromLTRB( + from.dx + padding.left + tp.height, + from.dy - padding.bottom - tp.width, + to.dx - padding.right, + to.dy + padding.top, + ), + ), + ); + } + } + } + } + } + + void drawVerticalLines( + BuildContext context, + CanvasWrapper canvasWrapper, + PaintHolder holder, + Size viewSize, + ) { + for (final line in holder.data.extraLinesData.verticalLines) { + final from = Offset(getPixelX(line.x, viewSize, holder), 0); + final to = Offset(getPixelX(line.x, viewSize, holder), viewSize.height); + + final isLineOutsideOfChart = from.dx < 0 || + to.dx < 0 || + from.dx > viewSize.width || + to.dx > viewSize.width; + + if (!isLineOutsideOfChart) { + _extraLinesPaint + ..setColorOrGradientForLine( + line.color, + line.gradient, + from: from, + to: to, + ) + ..strokeWidth = line.strokeWidth + ..transparentIfWidthIsZero() + ..strokeCap = line.strokeCap; + + canvasWrapper.drawDashedLine( + from, + to, + _extraLinesPaint, + line.dashArray, + ); + + if (line.sizedPicture != null) { + final centerX = line.sizedPicture!.width / 2; + final centerY = line.sizedPicture!.height / 2; + final xPosition = to.dx - centerX; + final yPosition = viewSize.height - centerY; + + canvasWrapper + ..save() + ..translate(xPosition, yPosition) + ..drawPicture(line.sizedPicture!.picture) + ..restore(); + } + + if (line.image != null) { + final centerX = line.image!.width / 2; + final centerY = line.image!.height + 2; + final centeredImageOffset = + Offset(to.dx - centerX, viewSize.height - centerY); + canvasWrapper.drawImage( + line.image!, + centeredImageOffset, + _imagePaint, + ); + } + + if (line.label.show) { + final label = line.label; + final style = + TextStyle(fontSize: 11, color: line.color).merge(label.style); + final padding = label.padding as EdgeInsets; + + final span = TextSpan( + text: label.labelResolver(line), + style: Utils().getThemeAwareTextStyle(context, style), + ); + + final tp = TextPainter( + text: span, + textDirection: TextDirection.ltr, + )..layout(); + + switch (label.direction) { + case LabelDirection.horizontal: + canvasWrapper.drawText( + tp, + label.alignment.withinRect( + Rect.fromLTRB( + from.dx - padding.right - tp.width, + from.dy + padding.top, + to.dx + padding.left, + to.dy - padding.bottom - tp.height, + ), + ), + ); + case LabelDirection.vertical: + canvasWrapper.drawVerticalText( + tp, + label.alignment.withinRect( + Rect.fromLTRB( + from.dx - padding.right, + from.dy + padding.top, + to.dx + padding.left + tp.height, + to.dy - padding.bottom - tp.width, + ), + ), + ); + } + } + } + } + } + + /// With this function we can convert our [FlSpot] x + /// to the view base axis x . + /// the view 0, 0 is on the top/left, but the spots is bottom/left + double getPixelX( + double spotX, + Size viewSize, + PaintHolder holder, + ) { + final usableSize = holder.getChartUsableSize(viewSize); + + final pixelXUnadjusted = _getPixelX(spotX, holder.data, usableSize); + + // Adjust the position relative to the canvas if chartVirtualRect + // is provided + final adjustment = holder.chartVirtualRect?.left ?? 0; + return pixelXUnadjusted + adjustment; + } + + double _getPixelX(double spotX, D data, Size usableSize) { + final deltaX = data.maxX - data.minX; + if (deltaX == 0.0) { + return 0; + } + return ((spotX - data.minX) / deltaX) * usableSize.width; + } + + /// With this function we can convert our [FlSpot] y + /// to the view base axis y. + double getPixelY( + double spotY, + Size viewSize, + PaintHolder holder, + ) { + final usableSize = holder.getChartUsableSize(viewSize); + + final pixelYUnadjusted = _getPixelY(spotY, holder.data, usableSize); + + // Adjust the position relative to the canvas if chartVirtualRect + // is provided + final adjustment = holder.chartVirtualRect?.top ?? 0; + return pixelYUnadjusted + adjustment; + } + + double _getPixelY(double spotY, D data, Size usableSize) { + final deltaY = data.maxY - data.minY; + if (deltaY == 0.0) { + return usableSize.height; + } + return usableSize.height - + (((spotY - data.minY) / deltaY) * usableSize.height); + } + + /// Converts pixel X position to axis X coordinates + double getXForPixel( + double pixelX, + Size viewSize, + PaintHolder holder, + ) { + final usableSize = holder.getChartUsableSize(viewSize); + final adjustment = holder.chartVirtualRect?.left ?? 0; + final unadjustedPixelX = pixelX - adjustment; + + final deltaX = holder.data.maxX - holder.data.minX; + if (deltaX == 0.0) return holder.data.minX; + + return (unadjustedPixelX / usableSize.width) * deltaX + holder.data.minX; + } + + /// Converts pixel Y position to axis Y coordinates + double getYForPixel( + double pixelY, + Size viewSize, + PaintHolder holder, + ) { + final usableSize = holder.getChartUsableSize(viewSize); + final adjustment = holder.chartVirtualRect?.top ?? 0; + final unadjustedPixelY = pixelY - adjustment; + + final deltaY = holder.data.maxY - holder.data.minY; + if (deltaY == 0.0) return holder.data.minY; + + return holder.data.maxY - (unadjustedPixelY / usableSize.height) * deltaY; + } + + /// Converts pixel coordinates to chart coordinates + Offset getChartCoordinateFromPixel( + Offset pixelOffset, + Size viewSize, + PaintHolder holder, + ) => + Offset( + getXForPixel(pixelOffset.dx, viewSize, holder), + getYForPixel(pixelOffset.dy, viewSize, holder), + ); + + /// With this function we can get horizontal + /// position for the tooltip. + double getTooltipLeft( + double dx, + double tooltipWidth, + FLHorizontalAlignment tooltipHorizontalAlignment, + double tooltipHorizontalOffset, + ) => + switch (tooltipHorizontalAlignment) { + FLHorizontalAlignment.center => + dx - (tooltipWidth / 2) + tooltipHorizontalOffset, + FLHorizontalAlignment.right => dx + tooltipHorizontalOffset, + FLHorizontalAlignment.left => + dx - tooltipWidth + tooltipHorizontalOffset, + }; +} diff --git a/lib/src/chart/base/axis_chart/axis_chart_scaffold_widget.dart b/lib/src/chart/base/axis_chart/axis_chart_scaffold_widget.dart new file mode 100644 index 0000000..3e9628a --- /dev/null +++ b/lib/src/chart/base/axis_chart/axis_chart_scaffold_widget.dart @@ -0,0 +1,324 @@ +import 'package:fl_chart/src/chart/base/axis_chart/axis_chart_data.dart'; +import 'package:fl_chart/src/chart/base/axis_chart/scale_axis.dart'; +import 'package:fl_chart/src/chart/base/axis_chart/side_titles/side_titles_widget.dart'; +import 'package:fl_chart/src/chart/base/axis_chart/transformation_config.dart'; +import 'package:fl_chart/src/chart/base/custom_interactive_viewer.dart'; +import 'package:fl_chart/src/extensions/fl_titles_data_extension.dart'; +import 'package:flutter/material.dart'; + +/// A builder to build a chart. +/// +/// The [chartVirtualRect] is the virtual chart virtual rect to be used when +/// laying out the chart's content. It is transformed based on users' +/// interactions like scaling and panning. +typedef ChartBuilder = Widget Function( + BuildContext context, + Rect? chartVirtualRect, +); + +/// A scaffold to show a scalable axis-based chart +/// +/// It contains some placeholders to represent an axis-based chart. +/// +/// It's something like the below graph: +/// |----------------------| +/// | | top | | +/// |------|-------|-------| +/// | left | chart | right | +/// |------|-------|-------| +/// | | bottom| | +/// |----------------------| +/// +/// `left`, `top`, `right`, `bottom` are some place holders to show titles +/// provided by [AxisChartData.titlesData] around the chart +/// `chart` is a centered place holder to show a raw chart. The chart is +/// built using [chartBuilder]. +class AxisChartScaffoldWidget extends StatefulWidget { + const AxisChartScaffoldWidget({ + super.key, + required this.chartBuilder, + required this.data, + this.transformationConfig = const FlTransformationConfig(), + }); + + /// The builder to build the chart. + final ChartBuilder chartBuilder; + + /// The data to build the chart. + final AxisChartData data; + + /// {@template fl_chart.AxisChartScaffoldWidget.transformationConfig} + /// The transformation configuration of the chart. + /// + /// Used to configure scaling and panning of the chart. + /// {@endtemplate} + final FlTransformationConfig transformationConfig; + + @override + State createState() => + _AxisChartScaffoldWidgetState(); +} + +class _AxisChartScaffoldWidgetState extends State { + late TransformationController _transformationController; + + final _chartKey = GlobalKey(); + + FlTransformationConfig get _transformationConfig => + widget.transformationConfig; + + bool get _canScaleHorizontally => + _transformationConfig.scaleAxis == FlScaleAxis.horizontal || + _transformationConfig.scaleAxis == FlScaleAxis.free; + + bool get _canScaleVertically => + _transformationConfig.scaleAxis == FlScaleAxis.vertical || + _transformationConfig.scaleAxis == FlScaleAxis.free; + + @override + void initState() { + super.initState(); + _transformationController = + _transformationConfig.transformationController ?? + TransformationController(); + _transformationController.addListener(_transformationControllerListener); + } + + @override + void dispose() { + _transformationController.removeListener(_transformationControllerListener); + if (_transformationConfig.transformationController == null) { + _transformationController.dispose(); + } + super.dispose(); + } + + @override + void didUpdateWidget(AxisChartScaffoldWidget oldWidget) { + super.didUpdateWidget(oldWidget); + + switch (( + oldWidget.transformationConfig.transformationController, + widget.transformationConfig.transformationController + )) { + case (null, null): + break; + case (null, TransformationController()): + _transformationController.dispose(); + _transformationController = + widget.transformationConfig.transformationController!; + _transformationController + .addListener(_transformationControllerListener); + case (TransformationController(), null): + _transformationController + .removeListener(_transformationControllerListener); + _transformationController = TransformationController(); + _transformationController + .addListener(_transformationControllerListener); + case (TransformationController(), TransformationController()): + if (oldWidget.transformationConfig.transformationController != + widget.transformationConfig.transformationController) { + _transformationController + .removeListener(_transformationControllerListener); + _transformationController = + widget.transformationConfig.transformationController!; + _transformationController + .addListener(_transformationControllerListener); + } + } + } + + void _transformationControllerListener() { + setState(() {}); + } + + // Applies the inverse transformation to the chart to get the zoomed + // bounding box. + // + // The transformation matrix is inverted because the bounding box needs to + // grow beyond the chart's boundaries when the chart is scaled in order + // for its content to be laid out on the larger area. This leads to the + // scaling effect. + Rect? _calculateAdjustedRect(Rect rect) { + final scale = _transformationController.value.getMaxScaleOnAxis(); + if (scale == 1.0) { + return null; + } + final inverseMatrix = Matrix4.inverted(_transformationController.value); + + final chartVirtualQuad = CustomInteractiveViewer.transformViewport( + inverseMatrix, + rect, + ); + + final chartVirtualRect = CustomInteractiveViewer.axisAlignedBoundingBox( + chartVirtualQuad, + ); + + return Rect.fromLTWH( + _canScaleHorizontally ? chartVirtualRect.left : rect.left, + _canScaleVertically ? chartVirtualRect.top : rect.top, + _canScaleHorizontally ? chartVirtualRect.width : rect.width, + _canScaleVertically ? chartVirtualRect.height : rect.height, + ); + } + + bool get showLeftTitles { + if (!widget.data.titlesData.show) { + return false; + } + final showAxisTitles = widget.data.titlesData.leftTitles.showAxisTitles; + final showSideTitles = widget.data.titlesData.leftTitles.showSideTitles; + return showAxisTitles || showSideTitles; + } + + bool get showRightTitles { + if (!widget.data.titlesData.show) { + return false; + } + final showAxisTitles = widget.data.titlesData.rightTitles.showAxisTitles; + final showSideTitles = widget.data.titlesData.rightTitles.showSideTitles; + return showAxisTitles || showSideTitles; + } + + bool get showTopTitles { + if (!widget.data.titlesData.show) { + return false; + } + final showAxisTitles = widget.data.titlesData.topTitles.showAxisTitles; + final showSideTitles = widget.data.titlesData.topTitles.showSideTitles; + return showAxisTitles || showSideTitles; + } + + bool get showBottomTitles { + if (!widget.data.titlesData.show) { + return false; + } + final showAxisTitles = widget.data.titlesData.bottomTitles.showAxisTitles; + final showSideTitles = widget.data.titlesData.bottomTitles.showSideTitles; + return showAxisTitles || showSideTitles; + } + + List _stackWidgets(BoxConstraints constraints) { + final margin = widget.data.titlesData.allSidesPadding; + final borderData = widget.data.borderData.isVisible() + ? widget.data.borderData.border + : null; + + final borderWidth = + borderData == null ? 0 : borderData.dimensions.horizontal; + final borderHeight = + borderData == null ? 0 : borderData.dimensions.vertical; + + final rect = Rect.fromLTRB( + 0, + 0, + constraints.maxWidth - margin.horizontal - borderWidth, + constraints.maxHeight - margin.vertical - borderHeight, + ); + + final adjustedRect = _calculateAdjustedRect(rect); + + final virtualRect = switch (_transformationConfig.scaleAxis) { + FlScaleAxis.none => null, + FlScaleAxis() => adjustedRect, + }; + + final chart = KeyedSubtree( + key: _chartKey, + child: widget.chartBuilder(context, virtualRect), + ); + + final child = switch (_transformationConfig.scaleAxis) { + FlScaleAxis.none => chart, + FlScaleAxis() => CustomInteractiveViewer( + transformationController: _transformationController, + clipBehavior: Clip.none, + trackpadScrollCausesScale: + _transformationConfig.trackpadScrollCausesScale, + maxScale: _transformationConfig.maxScale, + minScale: _transformationConfig.minScale, + panEnabled: _transformationConfig.panEnabled, + scaleEnabled: _transformationConfig.scaleEnabled, + child: SizedBox( + width: rect.width, + height: rect.height, + child: chart, + ), + ), + }; + + final widgets = [ + Container( + margin: margin, + decoration: BoxDecoration(border: borderData), + child: child, + ), + ]; + + int insertIndex(bool drawBelow) => drawBelow ? 0 : widgets.length; + + if (showLeftTitles) { + widgets.insert( + insertIndex(widget.data.titlesData.leftTitles.drawBelowEverything), + SideTitlesWidget( + side: AxisSide.left, + axisChartData: widget.data, + parentSize: constraints.biggest, + chartVirtualRect: adjustedRect, + ), + ); + } + + if (showTopTitles) { + widgets.insert( + insertIndex(widget.data.titlesData.topTitles.drawBelowEverything), + SideTitlesWidget( + side: AxisSide.top, + axisChartData: widget.data, + parentSize: constraints.biggest, + chartVirtualRect: adjustedRect, + ), + ); + } + + if (showRightTitles) { + widgets.insert( + insertIndex(widget.data.titlesData.rightTitles.drawBelowEverything), + SideTitlesWidget( + side: AxisSide.right, + axisChartData: widget.data, + parentSize: constraints.biggest, + chartVirtualRect: adjustedRect, + ), + ); + } + + if (showBottomTitles) { + widgets.insert( + insertIndex(widget.data.titlesData.bottomTitles.drawBelowEverything), + SideTitlesWidget( + side: AxisSide.bottom, + axisChartData: widget.data, + parentSize: constraints.biggest, + chartVirtualRect: adjustedRect, + ), + ); + } + return widgets; + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + return RotatedBox( + quarterTurns: widget.data.rotationQuarterTurns, + child: Stack( + children: _stackWidgets(constraints), + ), + ); + }, + ); + } +} diff --git a/lib/src/chart/base/axis_chart/axis_chart_widgets.dart b/lib/src/chart/base/axis_chart/axis_chart_widgets.dart new file mode 100644 index 0000000..424fe4d --- /dev/null +++ b/lib/src/chart/base/axis_chart/axis_chart_widgets.dart @@ -0,0 +1,134 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/base/axis_chart/axis_chart_helper.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; + +/// Wraps a [child] widget and applies some default behaviours +/// +/// Recommended to be used in [SideTitles.getTitlesWidget] +/// You need to pass [axisSide] value that provided by [TitleMeta] +/// It forces the widget to be close to the chart. +/// It also applies a [space] to the chart. +/// You can also fill [angle] in radians if you need to rotate your widget. +/// To force widget to be positioned within its axis bounding box, +/// define [fitInside] by passing [SideTitleFitInsideData] +class SideTitleWidget extends StatefulWidget { + const SideTitleWidget({ + super.key, + required this.child, + required this.meta, + this.space = 8.0, + this.angle = 0.0, + this.fitInside = const SideTitleFitInsideData( + enabled: false, + distanceFromEdge: 0, + parentAxisSize: 0, + axisPosition: 0, + ), + }); + + final TitleMeta meta; + final double space; + final Widget child; + final double angle; + + /// Define fitInside options with [SideTitleFitInsideData] + /// + /// To makes things simpler, it's recommended to use + /// [SideTitleFitInsideData.fromTitleMeta] and pass the + /// TitleMeta provided from [SideTitles.getTitlesWidget] + /// + /// If [fitInside.enabled] is true, the widget will be placed + /// inside the parent axis bounding box. + /// + /// Some translations will be applied to force + /// children to be positioned inside the parent axis bounding box. + /// + /// Will override the [SideTitleWidget.space] and caused + /// spacing between [SideTitles] children might be not equal. + final SideTitleFitInsideData fitInside; + + @override + State createState() => _SideTitleWidgetState(); +} + +class _SideTitleWidgetState extends State { + Alignment _getAlignment() => switch (widget.meta.axisSide) { + AxisSide.left => Alignment.centerRight, + AxisSide.top => Alignment.bottomCenter, + AxisSide.right => Alignment.centerLeft, + AxisSide.bottom => Alignment.topCenter, + }; + + EdgeInsets _getMargin() => switch (widget.meta.axisSide) { + AxisSide.left => EdgeInsets.only(right: widget.space), + AxisSide.top => EdgeInsets.only(bottom: widget.space), + AxisSide.right => EdgeInsets.only(left: widget.space), + AxisSide.bottom => EdgeInsets.only(top: widget.space), + }; + + /// Calculate child width/height + final GlobalKey widgetKey = GlobalKey(); + double? _childSize; + + void _getChildSize(_) { + // If fitInside is false, no need to find child size + if (!widget.fitInside.enabled) return; + + // If childSize is not null, no need to find the size anymore + if (_childSize != null) return; + + final context = widgetKey.currentContext; + if (context == null) return; + + // Set size based on its axis side + final size = switch (widget.meta.axisSide) { + AxisSide.left || AxisSide.right => context.size?.height ?? 0, + AxisSide.top || AxisSide.bottom => context.size?.width ?? 0, + }; + + // If childSize is the same, no need to set new value + if (_childSize == size) return; + + setState(() => _childSize = size); + } + + @override + void initState() { + super.initState(); + SchedulerBinding.instance.addPostFrameCallback(_getChildSize); + } + + @override + void didUpdateWidget(covariant SideTitleWidget oldWidget) { + super.didUpdateWidget(oldWidget); + SchedulerBinding.instance.addPostFrameCallback(_getChildSize); + } + + @override + Widget build(BuildContext context) { + return Transform.translate( + offset: !widget.fitInside.enabled + ? Offset.zero + : AxisChartHelper().calcFitInsideOffset( + axisSide: widget.meta.axisSide, + childSize: _childSize, + parentAxisSize: widget.fitInside.parentAxisSize, + axisPosition: widget.fitInside.axisPosition, + distanceFromEdge: widget.fitInside.distanceFromEdge, + ), + child: Transform.rotate( + angle: widget.angle, + child: Container( + key: widgetKey, + margin: _getMargin(), + alignment: _getAlignment(), + child: RotatedBox( + quarterTurns: -widget.meta.rotationQuarterTurns, + child: widget.child, + ), + ), + ), + ); + } +} diff --git a/lib/src/chart/base/axis_chart/scale_axis.dart b/lib/src/chart/base/axis_chart/scale_axis.dart new file mode 100644 index 0000000..b54f7b2 --- /dev/null +++ b/lib/src/chart/base/axis_chart/scale_axis.dart @@ -0,0 +1,20 @@ +enum FlScaleAxis { + /// Scales the horizontal axis. + horizontal, + + /// Scales the vertical axis. + vertical, + + /// Scales both the horizontal and vertical axes. + free, + + /// Does not scale the axes. + none; + + /// Axes that allow scaling. + static const scalingEnabledAxis = [ + free, + horizontal, + vertical, + ]; +} diff --git a/lib/src/chart/base/axis_chart/side_titles/side_titles_flex.dart b/lib/src/chart/base/axis_chart/side_titles/side_titles_flex.dart new file mode 100644 index 0000000..eacb08c --- /dev/null +++ b/lib/src/chart/base/axis_chart/side_titles/side_titles_flex.dart @@ -0,0 +1,298 @@ +import 'dart:math' as math; + +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +/// Inspired from [Flex] +class SideTitlesFlex extends MultiChildRenderObjectWidget { + SideTitlesFlex({ + super.key, + required this.direction, + required this.axisSideMetaData, + List widgetHolders = + const [], + }) : axisSideTitlesMetaData = widgetHolders.map((e) => e.metaData).toList(), + super(children: widgetHolders.map((e) => e.widget).toList()); + + final Axis direction; + final AxisSideMetaData axisSideMetaData; + final List axisSideTitlesMetaData; + + @override + AxisSideTitlesRenderFlex createRenderObject(BuildContext context) { + return AxisSideTitlesRenderFlex( + direction: direction, + axisSideMetaData: axisSideMetaData, + axisSideTitlesMetaData: axisSideTitlesMetaData, + ); + } + + @override + void updateRenderObject( + BuildContext context, + covariant AxisSideTitlesRenderFlex renderObject, + ) { + renderObject + ..direction = direction + ..axisSideMetaData = axisSideMetaData + ..axisSideTitlesMetaData = axisSideTitlesMetaData; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(EnumProperty('direction', direction)); + } +} + +class AxisSideTitlesRenderFlex extends RenderBox + with + ContainerRenderObjectMixin, + RenderBoxContainerDefaultsMixin, + DebugOverflowIndicatorMixin { + AxisSideTitlesRenderFlex({ + Axis direction = Axis.horizontal, + required AxisSideMetaData axisSideMetaData, + required List axisSideTitlesMetaData, + }) : _direction = direction, + _axisSideMetaData = axisSideMetaData, + _axisSideTitlesMetaData = axisSideTitlesMetaData; + + Axis get direction => _direction; + Axis _direction; + + set direction(Axis value) { + if (_direction != value) { + _direction = value; + markNeedsLayout(); + } + } + + AxisSideMetaData get axisSideMetaData => _axisSideMetaData; + AxisSideMetaData _axisSideMetaData; + + set axisSideMetaData(AxisSideMetaData value) { + if (_axisSideMetaData != value) { + _axisSideMetaData = value; + markNeedsLayout(); + } + } + + List get axisSideTitlesMetaData => + _axisSideTitlesMetaData; + List _axisSideTitlesMetaData; + + set axisSideTitlesMetaData(List value) { + if (_axisSideTitlesMetaData != value) { + _axisSideTitlesMetaData = value; + markNeedsLayout(); + } + } + + @override + void setupParentData(RenderBox child) { + if (child.parentData is! FlexParentData) { + child.parentData = FlexParentData(); + } + } + + @override + bool get debugNeedsLayout => false; + + @override + double? computeDistanceToActualBaseline(TextBaseline baseline) { + if (_direction == Axis.horizontal) { + return defaultComputeDistanceToHighestActualBaseline(baseline); + } + return defaultComputeDistanceToFirstActualBaseline(baseline); + } + + double _getCrossSize(Size size) { + switch (_direction) { + case Axis.horizontal: + return size.height; + case Axis.vertical: + return size.width; + } + } + + double _getMainSize(Size size) { + switch (_direction) { + case Axis.horizontal: + return size.width; + case Axis.vertical: + return size.height; + } + } + + @override + Size computeDryLayout(BoxConstraints constraints) { + final sizes = _computeSizes( + layoutChild: ChildLayoutHelper.dryLayoutChild, + constraints: constraints, + ); + + switch (_direction) { + case Axis.horizontal: + return constraints.constrain(Size(sizes.mainSize, sizes.crossSize)); + case Axis.vertical: + return constraints.constrain(Size(sizes.crossSize, sizes.mainSize)); + } + } + + _LayoutSizes _computeSizes({ + required BoxConstraints constraints, + required ChildLayouter layoutChild, + }) { + // Determine used flex factor, size inflexible items, calculate free space. + final maxMainSize = _direction == Axis.horizontal + ? constraints.maxWidth + : constraints.maxHeight; + final canFlex = maxMainSize < double.infinity; + + var crossSize = 0.0; + var allocatedSize = 0.0; // Sum of the sizes of the non-flexible children. + var child = firstChild; + while (child != null) { + final childParentData = child.parentData! as FlexParentData; + + // Stretch + final innerConstraints = switch (_direction) { + Axis.horizontal => BoxConstraints.tightFor( + height: constraints.maxHeight, + ), + Axis.vertical => BoxConstraints.tightFor( + width: constraints.maxWidth, + ), + }; + + final childSize = layoutChild(child, innerConstraints); + allocatedSize += _getMainSize(childSize); + crossSize = math.max(crossSize, _getCrossSize(childSize)); + assert(child.parentData == childParentData); + child = childParentData.nextSibling; + } + + final idealSize = canFlex ? maxMainSize : allocatedSize; + return _LayoutSizes( + mainSize: idealSize, + crossSize: crossSize, + allocatedSize: allocatedSize, + ); + } + + @override + void performLayout() { + final constraints = this.constraints; + final sizes = _computeSizes( + layoutChild: ChildLayoutHelper.layoutChild, + constraints: constraints, + ); + + var actualSize = sizes.mainSize; + var crossSize = sizes.crossSize; + + // Align items along the main axis. + switch (_direction) { + case Axis.horizontal: + size = constraints.constrain(Size(actualSize, crossSize)); + actualSize = size.width; + crossSize = size.height; + case Axis.vertical: + size = constraints.constrain(Size(crossSize, actualSize)); + actualSize = size.height; + crossSize = size.width; + } + + // Position elements + var child = firstChild; + var counter = 0; + while (child != null) { + final childParentData = child.parentData! as FlexParentData; + final metaData = _axisSideTitlesMetaData[counter]; + final double childCrossPosition; + + // Stretch + childCrossPosition = 0.0; + final childMainPosition = + metaData.axisPixelLocation - (_getMainSize(child.size) / 2); + childParentData.offset = switch (_direction) { + Axis.horizontal => Offset(childMainPosition, childCrossPosition), + Axis.vertical => Offset(childCrossPosition, childMainPosition), + }; + child = childParentData.nextSibling; + counter++; + } + } + + @override + bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { + return defaultHitTestChildren(result, position: position); + } + + @override + void paint(PaintingContext context, Offset offset) { + // There's no point in drawing the children if we're empty. + if (size.isEmpty) { + return; + } + + _clipRectLayer.layer = null; + defaultPaint(context, offset); + } + + final LayerHandle _clipRectLayer = + LayerHandle(); + + @override + void dispose() { + _clipRectLayer.layer = null; + super.dispose(); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(EnumProperty('direction', direction)); + } +} + +class _LayoutSizes { + const _LayoutSizes({ + required this.mainSize, + required this.crossSize, + required this.allocatedSize, + }); + + final double mainSize; + final double crossSize; + final double allocatedSize; +} + +class AxisSideMetaData { + AxisSideMetaData(this.minValue, this.maxValue, this.axisViewSize); + final double minValue; + final double maxValue; + final double axisViewSize; + + double get diff => maxValue - minValue; +} + +class AxisSideTitleMetaData with EquatableMixin { + AxisSideTitleMetaData(this.axisValue, this.axisPixelLocation); + final double axisValue; + final double axisPixelLocation; + + @override + List get props => [ + axisValue, + axisPixelLocation, + ]; +} + +class AxisSideTitleWidgetHolder { + AxisSideTitleWidgetHolder(this.metaData, this.widget); + final AxisSideTitleMetaData metaData; + final Widget widget; +} diff --git a/lib/src/chart/base/axis_chart/side_titles/side_titles_widget.dart b/lib/src/chart/base/axis_chart/side_titles/side_titles_widget.dart new file mode 100644 index 0000000..d0f6760 --- /dev/null +++ b/lib/src/chart/base/axis_chart/side_titles/side_titles_widget.dart @@ -0,0 +1,320 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/base/axis_chart/axis_chart_helper.dart'; +import 'package:fl_chart/src/chart/base/axis_chart/side_titles/side_titles_flex.dart'; +import 'package:fl_chart/src/extensions/bar_chart_data_extension.dart'; +import 'package:fl_chart/src/extensions/edge_insets_extension.dart'; +import 'package:fl_chart/src/extensions/fl_border_data_extension.dart'; +import 'package:fl_chart/src/extensions/fl_titles_data_extension.dart'; +import 'package:fl_chart/src/extensions/size_extension.dart'; +import 'package:fl_chart/src/utils/utils.dart'; +import 'package:flutter/material.dart'; + +class SideTitlesWidget extends StatefulWidget { + const SideTitlesWidget({ + super.key, + required this.side, + required this.axisChartData, + required this.parentSize, + this.chartVirtualRect, + }); + + final AxisSide side; + final AxisChartData axisChartData; + final Size parentSize; + final Rect? chartVirtualRect; + + @override + State createState() => _SideTitlesWidgetState(); +} + +class _SideTitlesWidgetState extends State { + bool get isHorizontal => + widget.side == AxisSide.top || widget.side == AxisSide.bottom; + + bool get isVertical => !isHorizontal; + + double get minX => widget.axisChartData.minX; + + double get maxX => widget.axisChartData.maxX; + + double get baselineX => widget.axisChartData.baselineX; + + double get minY => widget.axisChartData.minY; + + double get maxY => widget.axisChartData.maxY; + + double get baselineY => widget.axisChartData.baselineY; + + double get axisMin => isHorizontal ? minX : minY; + + double get axisMax => isHorizontal ? maxX : maxY; + + double get axisBaseLine => isHorizontal ? baselineX : baselineY; + + FlTitlesData get titlesData => widget.axisChartData.titlesData; + + bool get isLeftOrTop => + widget.side == AxisSide.left || widget.side == AxisSide.top; + + bool get isRightOrBottom => + widget.side == AxisSide.right || widget.side == AxisSide.bottom; + + AxisTitles get axisTitles => switch (widget.side) { + AxisSide.left => titlesData.leftTitles, + AxisSide.top => titlesData.topTitles, + AxisSide.right => titlesData.rightTitles, + AxisSide.bottom => titlesData.bottomTitles, + }; + + SideTitles get sideTitles => axisTitles.sideTitles; + + Axis get direction => isHorizontal ? Axis.horizontal : Axis.vertical; + + Axis get counterDirection => isHorizontal ? Axis.vertical : Axis.horizontal; + + Alignment get alignment => switch (widget.side) { + AxisSide.left => Alignment.centerLeft, + AxisSide.top => Alignment.topCenter, + AxisSide.right => Alignment.centerRight, + AxisSide.bottom => Alignment.bottomCenter, + }; + + EdgeInsets get thisSidePadding { + final titlesPadding = titlesData.allSidesPadding; + final borderPadding = widget.axisChartData.borderData.allSidesPadding; + return switch (widget.side) { + AxisSide.right || + AxisSide.left => + titlesPadding.onlyTopBottom + borderPadding.onlyTopBottom, + AxisSide.top || + AxisSide.bottom => + titlesPadding.onlyLeftRight + borderPadding.onlyLeftRight, + }; + } + + double get thisSidePaddingTotal { + final borderPadding = widget.axisChartData.borderData.allSidesPadding; + final titlesPadding = titlesData.allSidesPadding; + return switch (widget.side) { + AxisSide.right || + AxisSide.left => + titlesPadding.vertical + borderPadding.vertical, + AxisSide.top || + AxisSide.bottom => + titlesPadding.horizontal + borderPadding.horizontal, + }; + } + + Size get viewSize { + late Size size; + final chartVirtualRect = widget.chartVirtualRect; + if (chartVirtualRect == null) { + size = widget.parentSize; + } else { + size = chartVirtualRect.size + + Offset(thisSidePaddingTotal, thisSidePaddingTotal); + } + + return size.rotateByQuarterTurns( + widget.axisChartData.rotationQuarterTurns, + ); + } + + double get axisOffset { + final chartVirtualRect = widget.chartVirtualRect; + if (chartVirtualRect == null) { + return 0; + } + + return switch (widget.side) { + AxisSide.left || AxisSide.right => chartVirtualRect.top, + AxisSide.top || AxisSide.bottom => chartVirtualRect.left, + }; + } + + List makeWidgets( + double axisViewSize, + double axisMin, + double axisMax, + AxisSide side, + ) { + List axisPositions; + final interval = sideTitles.interval ?? + Utils().getEfficientInterval( + axisViewSize, + axisMax - axisMin, + ); + if (isHorizontal && widget.axisChartData is BarChartData) { + final barChartData = widget.axisChartData as BarChartData; + if (barChartData.barGroups.isEmpty) { + return []; + } + final xLocations = barChartData.calculateGroupsX(axisViewSize); + axisPositions = xLocations.asMap().entries.map((e) { + final index = e.key; + final xLocation = e.value; + final xValue = barChartData.barGroups[index].x; + final adjustedLocation = xLocation + axisOffset; + return AxisSideTitleMetaData(xValue.toDouble(), adjustedLocation); + }).toList(); + } else { + final axisValues = AxisChartHelper().iterateThroughAxis( + min: axisMin, + max: axisMax, + minIncluded: sideTitles.minIncluded, + maxIncluded: sideTitles.maxIncluded, + baseLine: axisBaseLine, + interval: interval, + ); + axisPositions = axisValues.map((axisValue) { + final axisDiff = axisMax - axisMin; + var portion = 0.0; + if (axisDiff > 0) { + portion = (axisValue - axisMin) / axisDiff; + } + if (isVertical) { + portion = 1 - portion; + } + final axisLocation = portion * axisViewSize + axisOffset; + return AxisSideTitleMetaData(axisValue, axisLocation); + }).toList(); + } + + axisPositions = _getPositionsWithinChartRange(axisPositions, side); + + return axisPositions.map( + (metaData) { + return AxisSideTitleWidgetHolder( + metaData, + sideTitles.getTitlesWidget( + metaData.axisValue, + TitleMeta( + min: axisMin, + max: axisMax, + appliedInterval: interval, + sideTitles: sideTitles, + formattedValue: Utils().formatNumber( + axisMin, + axisMax, + metaData.axisValue, + ), + axisSide: side, + parentAxisSize: axisViewSize, + axisPosition: metaData.axisPixelLocation, + rotationQuarterTurns: widget.axisChartData.rotationQuarterTurns, + ), + ), + ); + }, + ).toList(); + } + + List _getPositionsWithinChartRange( + List axisPositions, + AxisSide side, + ) { + final chartSize = Size( + widget.parentSize.width - thisSidePaddingTotal, + widget.parentSize.height - thisSidePaddingTotal, + ).rotateByQuarterTurns(widget.axisChartData.rotationQuarterTurns); + // Add 1 pixel to the chart's edges to avoid clipping the last title. + final chartRect = (Offset.zero & chartSize).inflate(1); + + return axisPositions.where((metaData) { + final location = metaData.axisPixelLocation; + return switch (side) { + AxisSide.left || + AxisSide.right => + chartRect.contains(Offset(0, location)), + AxisSide.top || + AxisSide.bottom => + chartRect.contains(Offset(location, 0)), + }; + }).toList(); + } + + @override + Widget build(BuildContext context) { + if (!axisTitles.showAxisTitles && !axisTitles.showSideTitles) { + return Container(); + } + + final axisViewSize = isHorizontal ? viewSize.width : viewSize.height; + return Align( + alignment: alignment, + child: Flex( + direction: counterDirection, + mainAxisSize: MainAxisSize.min, + children: [ + if (isLeftOrTop && axisTitles.axisNameWidget != null) + _AxisTitleWidget( + axisTitles: axisTitles, + side: widget.side, + axisViewSize: axisViewSize, + ), + if (sideTitles.showTitles) + Container( + width: isHorizontal ? axisViewSize : sideTitles.reservedSize, + height: isHorizontal ? sideTitles.reservedSize : axisViewSize, + margin: thisSidePadding, + child: SideTitlesFlex( + direction: direction, + axisSideMetaData: AxisSideMetaData( + axisMin, + axisMax, + axisViewSize - thisSidePaddingTotal, + ), + widgetHolders: makeWidgets( + axisViewSize - thisSidePaddingTotal, + axisMin, + axisMax, + widget.side, + ), + ), + ), + if (isRightOrBottom && axisTitles.axisNameWidget != null) + _AxisTitleWidget( + axisTitles: axisTitles, + side: widget.side, + axisViewSize: axisViewSize, + ), + ], + ), + ); + } +} + +class _AxisTitleWidget extends StatelessWidget { + const _AxisTitleWidget({ + required this.axisTitles, + required this.side, + required this.axisViewSize, + }); + + final AxisTitles axisTitles; + final AxisSide side; + final double axisViewSize; + + int get axisNameQuarterTurns => switch (side) { + AxisSide.right => 3, + AxisSide.left => 3, + AxisSide.top => 0, + AxisSide.bottom => 0, + }; + + bool get isHorizontal => side == AxisSide.top || side == AxisSide.bottom; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: isHorizontal ? axisViewSize : axisTitles.axisNameSize, + height: isHorizontal ? axisTitles.axisNameSize : axisViewSize, + child: Center( + child: RotatedBox( + quarterTurns: axisNameQuarterTurns, + child: axisTitles.axisNameWidget, + ), + ), + ); + } +} diff --git a/lib/src/chart/base/axis_chart/transformation_config.dart b/lib/src/chart/base/axis_chart/transformation_config.dart new file mode 100644 index 0000000..3f7b421 --- /dev/null +++ b/lib/src/chart/base/axis_chart/transformation_config.dart @@ -0,0 +1,50 @@ +import 'package:fl_chart/src/chart/base/axis_chart/scale_axis.dart'; +import 'package:flutter/widgets.dart'; + +/// Configuration for the transformation of an axis-based chart. +class FlTransformationConfig { + const FlTransformationConfig({ + this.scaleAxis = FlScaleAxis.none, + this.minScale = 1, + this.maxScale = 2.5, + this.panEnabled = true, + this.scaleEnabled = true, + this.trackpadScrollCausesScale = false, + this.transformationController, + }) : assert(minScale >= 1, 'minScale must be greater than or equal to 1'), + assert( + maxScale >= minScale, + 'maxScale must be greater than or equal to minScale', + ); + + /// Determines what axis of the chart should be scaled. + final FlScaleAxis scaleAxis; + + /// The minimum scale of the chart. + /// + /// Ignored when [scaleAxis] is [FlScaleAxis.none]. + final double minScale; + + /// The maximum scale of the chart. + /// + /// Ignored when [scaleAxis] is [FlScaleAxis.none]. + final double maxScale; + + /// If false, the user will be prevented from panning. + /// + /// Ignored when [scaleAxis] is [FlScaleAxis.none]. + final bool panEnabled; + + /// If false, the user will be prevented from scaling. + /// + /// Ignored when [scaleAxis] is [FlScaleAxis.none]. + final bool scaleEnabled; + + /// Whether trackpad scroll causes scale. + /// + /// Ignored when [scaleAxis] is [FlScaleAxis.none]. + final bool trackpadScrollCausesScale; + + /// The transformation controller to control the transformation of the chart. + final TransformationController? transformationController; +} diff --git a/lib/src/chart/base/base_chart/base_chart_data.dart b/lib/src/chart/base/base_chart/base_chart_data.dart new file mode 100644 index 0000000..904b815 --- /dev/null +++ b/lib/src/chart/base/base_chart/base_chart_data.dart @@ -0,0 +1,203 @@ +// coverage:ignore-file +import 'dart:core'; + +import 'package:equatable/equatable.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart'; +import 'package:fl_chart/src/extensions/border_extension.dart'; +import 'package:flutter/material.dart'; + +/// This class holds all data needed for [BaseChartPainter]. +/// +/// In this phase we draw the border, +/// and handle touches in an abstract way. +abstract class BaseChartData with EquatableMixin { + /// It draws 4 borders around your chart, you can customize it using [borderData], + /// [touchData] defines the touch behavior and responses. + BaseChartData({ + FlBorderData? borderData, + }) : borderData = borderData ?? FlBorderData(); + + /// Holds data to drawing border around the chart. + final FlBorderData borderData; + + BaseChartData lerp(BaseChartData a, BaseChartData b, double t); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + borderData, + ]; +} + +/// Holds data to drawing border around the chart. +class FlBorderData with EquatableMixin { + /// [show] Determines showing or hiding border around the chart. + /// [border] Determines the visual look of 4 borders, see [Border]. + FlBorderData({ + bool? show, + Border? border, + }) : show = show ?? true, + border = border ?? Border.all(); + final bool show; + final Border border; + + /// returns false if all borders have 0 width or 0 opacity + bool isVisible() => show && border.isVisible(); + + /// Lerps a [FlBorderData] based on [t] value, check [Tween.lerp]. + static FlBorderData lerp(FlBorderData a, FlBorderData b, double t) => + FlBorderData( + show: b.show, + border: Border.lerp(a.border, b.border, t), + ); + + /// Copies current [FlBorderData] to a new [FlBorderData], + /// and replaces provided values. + FlBorderData copyWith({ + bool? show, + Border? border, + }) => + FlBorderData( + show: show ?? this.show, + border: border ?? this.border, + ); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + show, + border, + ]; +} + +/// Holds data to handle touch events, and touch responses in abstract way. +/// +/// There is a touch flow, explained [here](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/handle_touches.md) +/// in a simple way, each chart's renderer captures the touch events, and passes the pointerEvent +/// to the painter, and gets touched spot, and wraps it into a concrete [BaseTouchResponse]. +abstract class FlTouchData with EquatableMixin { + /// You can disable or enable the touch system using [enabled] flag, + const FlTouchData( + this.enabled, + this.touchCallback, + this.mouseCursorResolver, + this.longPressDuration, + ); + + /// You can disable or enable the touch system using [enabled] flag, + final bool enabled; + + /// [touchCallback] notifies you about the happened touch/pointer events. + /// It gives you a [FlTouchEvent] which is the happened event such as [FlPointerHoverEvent], [FlTapUpEvent], ... + /// It also gives you a [BaseTouchResponse] which is the chart specific type and contains information + /// about the elements that has touched. + final BaseTouchCallback? touchCallback; + + /// Using [mouseCursorResolver] you can change the mouse cursor + /// based on the provided [FlTouchEvent] and [BaseTouchResponse] + final MouseCursorResolver? mouseCursorResolver; + + /// This property that allows to customize the duration of the longPress gesture. + /// default to 500 milliseconds refer to [kLongPressTimeout]. + final Duration? longPressDuration; + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + enabled, + touchCallback, + mouseCursorResolver, + longPressDuration, + ]; +} + +/// Holds data to clipping chart around its borders. +class FlClipData with EquatableMixin { + /// Creates data that clips specified sides + const FlClipData({ + required this.top, + required this.bottom, + required this.left, + required this.right, + }); + + /// Creates data that clips all sides + const FlClipData.all() + : this(top: true, bottom: true, left: true, right: true); + + /// Creates data that clips only top and bottom side + const FlClipData.vertical() + : this(top: true, bottom: true, left: false, right: false); + + /// Creates data that clips only left and right side + const FlClipData.horizontal() + : this(top: false, bottom: false, left: true, right: true); + + /// Creates data that doesn't clip any side + const FlClipData.none() + : this(top: false, bottom: false, left: false, right: false); + + final bool top; + final bool bottom; + final bool left; + final bool right; + + /// Checks whether any of the sides should be clipped + bool get any => top || bottom || left || right; + + /// Copies current [FlBorderData] to a new [FlBorderData], + /// and replaces provided values. + FlClipData copyWith({ + bool? top, + bool? bottom, + bool? left, + bool? right, + }) => + FlClipData( + top: top ?? this.top, + bottom: bottom ?? this.bottom, + left: left ?? this.left, + right: right ?? this.right, + ); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [top, bottom, left, right]; +} + +/// Chart's touch callback. +typedef BaseTouchCallback = void Function( + FlTouchEvent, + R?, +); + +/// It gives you the happened [FlTouchEvent] and existed [R] data at the event's location, +/// then you should provide a [MouseCursor] to change the cursor at the event's location. +/// For example you can pass the [SystemMouseCursors.click] to change the mouse cursor to click. +typedef MouseCursorResolver = MouseCursor Function( + FlTouchEvent, + R?, +); + +/// This class holds the touch response details of charts. +abstract class BaseTouchResponse { + BaseTouchResponse({ + required this.touchLocation, + }); + + /// The location of the touch in pixels on the screen. + final Offset touchLocation; +} + +/// Controls an element horizontal alignment to given point. +enum FLHorizontalAlignment { + /// Element shown horizontally center aligned to a given point. + center, + + /// Element shown on the left side of the given point. + left, + + /// Element shown on the right side of the given point. + right, +} diff --git a/lib/src/chart/base/base_chart/base_chart_painter.dart b/lib/src/chart/base/base_chart/base_chart_painter.dart new file mode 100644 index 0000000..09aba8a --- /dev/null +++ b/lib/src/chart/base/base_chart/base_chart_painter.dart @@ -0,0 +1,60 @@ +// coverage:ignore-file +import 'package:fl_chart/src/chart/base/base_chart/base_chart_data.dart'; +import 'package:fl_chart/src/utils/canvas_wrapper.dart'; +import 'package:flutter/material.dart'; + +/// Base class of our painters. +class BaseChartPainter { + /// Draws some basic elements + const BaseChartPainter(); + + // Paints [BaseChartData] into the provided canvas. + void paint( + BuildContext context, + CanvasWrapper canvasWrapper, + PaintHolder holder, + ) {} +} + +/// Holds data for painting on canvas +class PaintHolder { + /// Holds data for painting on canvas + const PaintHolder( + this.data, + this.targetData, + this.textScaler, [ + this.chartVirtualRect, + ]); + + /// [data] is what we need to show frame by frame (it might be changed by an animator) + final Data data; + + /// [targetData] is the target of animation that is playing. + final Data targetData; + + /// system [TextScaler] used for scaling texts for better readability + final TextScaler textScaler; + + /// The virtual rect representing the chart when it is scaled or panned. + /// + /// The chart will be drawn in this virtual canvas, and then clipped to the + /// actual canvas. + /// + /// When the chart is scaled, the virtual canvas will be larger than the + /// actual canvas. This will lead to the content being laid out on the larger + /// area. Thus resulting in the scaling effect. + /// + /// Null when not scaling or panning. + final Rect? chartVirtualRect; + + /// Returns the size of the chart that is actually being painted. + /// + /// When scaling the chart, the chart is painted on a larger area to simulate + /// the zoom effect. This function returns the size of the area that is + /// actually being painted. + /// + /// When not scaled it returns the actual size of the chart. + Size getChartUsableSize(Size viewSize) { + return chartVirtualRect?.size ?? viewSize; + } +} diff --git a/lib/src/chart/base/base_chart/fl_touch_event.dart b/lib/src/chart/base/base_chart/fl_touch_event.dart new file mode 100644 index 0000000..73e235d --- /dev/null +++ b/lib/src/chart/base/base_chart/fl_touch_event.dart @@ -0,0 +1,249 @@ +// coverage:ignore-file +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/services.dart'; + +/// Parent class for several kind of touch/pointer events (like tap, panMode, longPressStart, ...) +abstract class FlTouchEvent { + const FlTouchEvent(); + + /// Represents the position of happened touch/pointer event + /// + /// Some events such as [FlPanCancelEvent] and [FlTapCancelEvent] + /// doesn't have any position (their details come from flutter engine). + /// That's why this field is nullable + Offset? get localPosition => null; + + /// excludes exit or up events to show interactions on charts + bool get isInterestedForInteractions { + final isLinux = defaultTargetPlatform == TargetPlatform.linux; + final isMacOS = defaultTargetPlatform == TargetPlatform.macOS; + final isWindows = defaultTargetPlatform == TargetPlatform.windows; + + final isDesktopOrWeb = kIsWeb || isLinux || isMacOS || isWindows; + + /// In desktop when mouse hovers into a chart element using [FlPointerHoverEvent], we show the interaction + /// and when tap happens at the same position, interaction will be dismissed because of [FlTapUpEvent]. + /// That's why we exclude it on desktop or web + if (isDesktopOrWeb && this is FlTapUpEvent) { + return true; + } + + return this is! FlPanEndEvent && + this is! FlPanCancelEvent && + this is! FlPointerExitEvent && + this is! FlLongPressEnd && + this is! FlTapUpEvent && + this is! FlTapCancelEvent; + } +} + +/// When a pointer has contacted the screen and might begin to move +/// +/// The [details] object provides the position of the touch. +/// Inspired from [GestureDragDownCallback] +class FlPanDownEvent extends FlTouchEvent { + const FlPanDownEvent(this.details); + + /// Contains information of happened touch gesture + final DragDownDetails details; + + /// Represents the position of happened touch/pointer event + @override + Offset get localPosition => details.localPosition; +} + +/// When a pointer has contacted the screen and has begun to move. +/// +/// The [details] object provides the position of the touch when it first +/// touched the surface. +/// Inspired from [GestureDragStartCallback]. +class FlPanStartEvent extends FlTouchEvent { + /// Creates + const FlPanStartEvent(this.details); + + /// Contains information of happened touch gesture + final DragStartDetails details; + + /// Represents the position of happened touch/pointer event + @override + Offset get localPosition => details.localPosition; +} + +/// When a pointer that is in contact with the screen and moving +/// has moved again. +/// +/// The [details] object provides the position of the touch and the distance it +/// has traveled since the last update. +/// Inspired from [GestureDragUpdateCallback] +class FlPanUpdateEvent extends FlTouchEvent { + const FlPanUpdateEvent(this.details); + + /// Contains information of happened touch gesture + final DragUpdateDetails details; + + /// Represents the position of happened touch/pointer event + @override + Offset get localPosition => details.localPosition; +} + +/// When the pointer that previously triggered a [FlPanStartEvent] did not complete. +/// Inspired from [GestureDragCancelCallback] +class FlPanCancelEvent extends FlTouchEvent { + const FlPanCancelEvent(); +} + +/// When a pointer that was previously in contact with the screen +/// and moving is no longer in contact with the screen. +/// +/// The velocity at which the pointer was moving when it stopped contacting +/// the screen is available in the [details]. +/// Inspired from [GestureDragEndCallback] +class FlPanEndEvent extends FlTouchEvent { + const FlPanEndEvent(this.details); + + /// Contains information of happened touch gesture + final DragEndDetails details; +} + +/// When a pointer that might cause a tap has contacted the +/// screen. +/// +/// The position at which the pointer contacted the screen is available in the +/// [details]. +/// Inspired from [GestureTapDownCallback] +class FlTapDownEvent extends FlTouchEvent { + const FlTapDownEvent(this.details); + + /// Contains information of happened touch gesture + final TapDownDetails details; + + /// Represents the position of happened touch/pointer event + @override + Offset get localPosition => details.localPosition; +} + +/// When the pointer that previously triggered a [FlTapDownEvent] will not end up causing a tap. +/// Inspired from [GestureTapCancelCallback] +class FlTapCancelEvent extends FlTouchEvent { + const FlTapCancelEvent(); +} + +/// When a pointer that will trigger a tap has stopped contacting +/// the screen. +/// +/// The position at which the pointer stopped contacting the screen is available +/// in the [details]. +/// Inspired from [GestureTapUpCallback] +class FlTapUpEvent extends FlTouchEvent { + const FlTapUpEvent(this.details); + + /// Contains information of happened touch gesture + final TapUpDetails details; + + /// Represents the position of happened touch/pointer event + @override + Offset get localPosition => details.localPosition; +} + +/// Called When a pointer has remained in contact with the screen at the +/// same location for a long period of time. +/// +/// Details are available in the [details]. +/// +/// Inspired from [GestureLongPressStartCallback] +class FlLongPressStart extends FlTouchEvent { + const FlLongPressStart(this.details); + + /// Contains information of happened touch gesture + final LongPressStartDetails details; + + /// Represents the position of happened touch/pointer event + @override + Offset get localPosition => details.localPosition; +} + +/// When a pointer is moving after being held in contact at the same +/// location for a long period of time. Reports the new position and its offset +/// from the original down position. +/// +/// Details are available in the [details] +/// +/// Inspired from [GestureLongPressMoveUpdateCallback] +class FlLongPressMoveUpdate extends FlTouchEvent { + const FlLongPressMoveUpdate(this.details); + + /// Contains information of happened touch gesture + final LongPressMoveUpdateDetails details; + + /// Represents the position of happened touch/pointer event + @override + Offset get localPosition => details.localPosition; +} + +/// When a pointer stops contacting the screen after a long press +/// gesture was detected. Also reports the position where the pointer stopped +/// contacting the screen. +/// +/// Details are available in the [details] +/// +/// Inspired from [GestureLongPressEndCallback] +class FlLongPressEnd extends FlTouchEvent { + const FlLongPressEnd(this.details); + + /// Contains information of happened touch gesture + final LongPressEndDetails details; + + /// Represents the position of happened touch/pointer event + @override + Offset get localPosition => details.localPosition; +} + +/// The pointer has moved with respect to the device while the pointer is or is +/// not in contact with the device, and it has entered our chart. +/// +/// Details are available in the [event] +/// +/// Inspired from [PointerEnterEventListener] +class FlPointerEnterEvent extends FlTouchEvent { + const FlPointerEnterEvent(this.event); + + /// Contains information of happened pointer event + final PointerEnterEvent event; + + /// Represents the position of happened touch/pointer event + @override + Offset get localPosition => event.localPosition; +} + +/// The pointer has moved with respect to the device while the pointer is not +/// in contact with the device. +/// +/// Details are available in the [event] +/// +/// Inspired from [PointerHoverEventListener] +class FlPointerHoverEvent extends FlTouchEvent { + const FlPointerHoverEvent(this.event); + + /// Contains information of happened pointer event + final PointerHoverEvent event; + + /// Represents the position of happened touch/pointer event + @override + Offset get localPosition => event.localPosition; +} + +/// The pointer has moved with respect to the device while the pointer is or is +/// not in contact with the device, and exited our chart. +/// +/// Inspired from [PointerExitEventListener] which contains [PointerExitEvent] +class FlPointerExitEvent extends FlTouchEvent { + const FlPointerExitEvent(this.event); + + /// Contains information of happened pointer event + final PointerExitEvent event; + + /// Represents the position of happened touch/pointer event + @override + Offset get localPosition => event.localPosition; +} diff --git a/lib/src/chart/base/base_chart/render_base_chart.dart b/lib/src/chart/base/base_chart/render_base_chart.dart new file mode 100644 index 0000000..fbd34ab --- /dev/null +++ b/lib/src/chart/base/base_chart/render_base_chart.dart @@ -0,0 +1,207 @@ +// coverage:ignore-file +import 'package:fl_chart/src/chart/base/base_chart/base_chart_data.dart'; +import 'package:fl_chart/src/chart/base/base_chart/fl_touch_event.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; + +/// It implements shared logics between our renderers such as touch/pointer events recognition, size, layout, ... +abstract class RenderBaseChart extends RenderBox + implements MouseTrackerAnnotation { + /// We use [FlTouchData] to retrieve [FlTouchData.touchCallback] and [FlTouchData.mouseCursorResolver] + /// to invoke them when touch happens. + RenderBaseChart( + FlTouchData? touchData, + BuildContext context, { + required bool canBeScaled, + }) : _canBeScaled = canBeScaled, + _buildContext = context { + updateBaseTouchData(touchData); + initGestureRecognizers(); + } + + bool get canBeScaled => _canBeScaled; + bool _canBeScaled; + set canBeScaled(bool value) { + if (_canBeScaled == value) return; + _canBeScaled = value; + markNeedsPaint(); + } + + // We use buildContext to retrieve Theme data + BuildContext get buildContext => _buildContext; + BuildContext _buildContext; + set buildContext(BuildContext value) { + _buildContext = value; + markNeedsPaint(); + } + + void updateBaseTouchData(FlTouchData? value) { + _touchCallback = value?.touchCallback; + _mouseCursorResolver = value?.mouseCursorResolver; + _longPressDuration = value?.longPressDuration; + } + + BaseTouchCallback? _touchCallback; + MouseCursorResolver? _mouseCursorResolver; + Duration? _longPressDuration; + + MouseCursor _latestMouseCursor = MouseCursor.defer; + + late bool _validForMouseTracker; + + /// Recognizes pan gestures, such as onDown, onStart, onUpdate, onCancel, ... + @visibleForTesting + late PanGestureRecognizer panGestureRecognizer; + + /// Recognizes tap gestures, such as onTapDown, onTapCancel and onTapUp + @visibleForTesting + late TapGestureRecognizer tapGestureRecognizer; + + /// Recognizes longPress gestures, such as onLongPressStart, onLongPressMoveUpdate and onLongPressEnd + @visibleForTesting + late LongPressGestureRecognizer longPressGestureRecognizer; + + /// Initializes our recognizers and implement their callbacks. + void initGestureRecognizers() { + panGestureRecognizer = PanGestureRecognizer(); + panGestureRecognizer + ..onDown = (dragDownDetails) { + _notifyTouchEvent(FlPanDownEvent(dragDownDetails)); + } + ..onStart = (dragStartDetails) { + _notifyTouchEvent(FlPanStartEvent(dragStartDetails)); + } + ..onUpdate = (dragUpdateDetails) { + _notifyTouchEvent(FlPanUpdateEvent(dragUpdateDetails)); + } + ..onCancel = () { + _notifyTouchEvent(const FlPanCancelEvent()); + } + ..onEnd = (dragEndDetails) { + _notifyTouchEvent(FlPanEndEvent(dragEndDetails)); + }; + + tapGestureRecognizer = TapGestureRecognizer(); + tapGestureRecognizer + ..onTapDown = (tapDownDetails) { + _notifyTouchEvent(FlTapDownEvent(tapDownDetails)); + } + ..onTapCancel = () { + _notifyTouchEvent(const FlTapCancelEvent()); + } + ..onTapUp = (tapUpDetails) { + _notifyTouchEvent(FlTapUpEvent(tapUpDetails)); + }; + + longPressGestureRecognizer = + LongPressGestureRecognizer(duration: _longPressDuration); + longPressGestureRecognizer + ..onLongPressStart = (longPressStartDetails) { + _notifyTouchEvent(FlLongPressStart(longPressStartDetails)); + } + ..onLongPressMoveUpdate = (longPressMoveUpdateDetails) { + _notifyTouchEvent( + FlLongPressMoveUpdate(longPressMoveUpdateDetails), + ); + } + ..onLongPressEnd = (longPressEndDetails) => + _notifyTouchEvent(FlLongPressEnd(longPressEndDetails)); + } + + @override + void performLayout() { + size = computeDryLayout(constraints); + } + + @override + Size computeDryLayout(BoxConstraints constraints) { + return Size(constraints.maxWidth, constraints.maxHeight); + } + + @override + bool hitTestSelf(Offset position) => true; + + /// Feeds our gesture recognizers for handling events, we also handle [PointerHoverEvent] here. + /// + /// Our gesture recognizers are responsible for notifying us about happened gestures (such as tap, panMove, ...) + /// we need to give them [PointerDownEvent] then they will listen to the global [GestureBinding] for further events. + /// + /// We need to handle [PointerHoverEvent] because there is no gesture recognizer + /// for mouse hover events (in fact they don't have any gestures, they are just events). + @override + void handleEvent(PointerEvent event, covariant BoxHitTestEntry entry) { + assert(debugHandleEvent(event, entry)); + if (_touchCallback == null) { + return; + } + if (event is PointerDownEvent) { + longPressGestureRecognizer.addPointer(event); + tapGestureRecognizer.addPointer(event); + if (!canBeScaled) { + panGestureRecognizer.addPointer(event); + } + } else if (event is PointerHoverEvent) { + _notifyTouchEvent(FlPointerHoverEvent(event)); + } + } + + /// Here we handle mouse hover enter event + @override + PointerEnterEventListener? get onEnter => + (event) => _notifyTouchEvent(FlPointerEnterEvent(event)); + + /// Here we handle mouse hover exit event + @override + PointerExitEventListener? get onExit => + (event) => _notifyTouchEvent(FlPointerExitEvent(event)); + + /// Invokes the [_touchCallback] to notify listeners of this [FlTouchEvent] + /// + /// We get a [BaseTouchResponse] using [getResponseAtLocation] for events which contains a localPosition. + /// Then we invoke [_touchCallback] using the [event] and [response]. + void _notifyTouchEvent(FlTouchEvent event) { + if (_touchCallback == null) { + return; + } + final localPosition = event.localPosition; + R? response; + if (localPosition != null) { + response = getResponseAtLocation(localPosition); + } + _touchCallback!(event, response); + + if (_mouseCursorResolver == null) { + _latestMouseCursor = MouseCursor.defer; + } else { + _latestMouseCursor = _mouseCursorResolver!(event, response); + } + } + + /// Represents the mouse cursor style when hovers on our chart + /// In the future we can change it runtime, for example we can turn it to + /// [SystemMouseCursors.click] when mouse hovers a specific point of our chart. + @override + MouseCursor get cursor => _latestMouseCursor; + + /// [MouseTracker] will catch us if this variable is true + @override + bool get validForMouseTracker => _validForMouseTracker; + + /// Charts need to implement this class to tell us what [BaseTouchResponse] is available at provided [localPosition] + /// When touch/pointer event happens, we send it to the user alongside the [FlTouchEvent] using [_touchCallback] + R getResponseAtLocation(Offset localPosition); + + @override + void attach(PipelineOwner owner) { + super.attach(owner); + _validForMouseTracker = true; + } + + @override + void detach() { + _validForMouseTracker = false; + super.detach(); + } +} diff --git a/lib/src/chart/base/custom_interactive_viewer.dart b/lib/src/chart/base/custom_interactive_viewer.dart new file mode 100644 index 0000000..4369146 --- /dev/null +++ b/lib/src/chart/base/custom_interactive_viewer.dart @@ -0,0 +1,1179 @@ +// coverage:ignore-file +// This file is copied from Flutter's InteractiveViewer widget. +// The only change is that the child is not wrapped in a `Transform` so +// we can react to the transformation ourselves. +// +// This should be removed once the official InteractiveViewer allows to disable +// the Transform widget. + +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math' as math; + +import 'package:flutter/foundation.dart' show clampDouble; +import 'package:flutter/gestures.dart'; +import 'package:flutter/physics.dart'; +import 'package:flutter/widgets.dart'; +import 'package:vector_math/vector_math_64.dart' show Matrix4, Quad, Vector3; + +typedef CustomInteractiveViewerWidgetBuilder = Widget Function( + BuildContext context, + Quad viewport, +); + +@immutable +class CustomInteractiveViewer extends StatefulWidget { + CustomInteractiveViewer({ + super.key, + this.clipBehavior = Clip.hardEdge, + this.panAxis = PanAxis.free, + this.boundaryMargin = EdgeInsets.zero, + this.constrained = true, + this.maxScale = 2.5, + this.minScale = 0.8, + this.interactionEndFrictionCoefficient = _kDrag, + this.onInteractionEnd, + this.onInteractionStart, + this.onInteractionUpdate, + this.panEnabled = true, + this.scaleEnabled = true, + this.scaleFactor = kDefaultMouseScrollToScaleFactor, + this.transformationController, + this.alignment, + this.trackpadScrollCausesScale = false, + required this.child, + }) : assert(minScale > 0), + assert(interactionEndFrictionCoefficient > 0), + assert(minScale.isFinite), + assert(maxScale > 0), + assert(!maxScale.isNaN), + assert(maxScale >= minScale), + assert( + (boundaryMargin.horizontal.isInfinite && + boundaryMargin.vertical.isInfinite) || + (boundaryMargin.top.isFinite && + boundaryMargin.right.isFinite && + boundaryMargin.bottom.isFinite && + boundaryMargin.left.isFinite), + ), + builder = null; + + CustomInteractiveViewer.builder({ + super.key, + this.clipBehavior = Clip.hardEdge, + this.panAxis = PanAxis.free, + this.boundaryMargin = EdgeInsets.zero, + this.maxScale = 2.5, + this.minScale = 0.8, + this.interactionEndFrictionCoefficient = _kDrag, + this.onInteractionEnd, + this.onInteractionStart, + this.onInteractionUpdate, + this.panEnabled = true, + this.scaleEnabled = true, + this.scaleFactor = 200.0, + this.transformationController, + this.alignment, + this.trackpadScrollCausesScale = false, + required CustomInteractiveViewerWidgetBuilder this.builder, + }) : assert(minScale > 0), + assert(interactionEndFrictionCoefficient > 0), + assert(minScale.isFinite), + assert(maxScale > 0), + assert(!maxScale.isNaN), + assert(maxScale >= minScale), + assert( + (boundaryMargin.horizontal.isInfinite && + boundaryMargin.vertical.isInfinite) || + (boundaryMargin.top.isFinite && + boundaryMargin.right.isFinite && + boundaryMargin.bottom.isFinite && + boundaryMargin.left.isFinite), + ), + constrained = false, + child = null; + + final Alignment? alignment; + final Clip clipBehavior; + + final PanAxis panAxis; + + final EdgeInsets boundaryMargin; + + final CustomInteractiveViewerWidgetBuilder? builder; + + final Widget? child; + + final bool constrained; + + final bool panEnabled; + + final bool scaleEnabled; + + final bool trackpadScrollCausesScale; + + final double scaleFactor; + + final double maxScale; + + final double minScale; + + final double interactionEndFrictionCoefficient; + + final GestureScaleEndCallback? onInteractionEnd; + + final GestureScaleStartCallback? onInteractionStart; + + final GestureScaleUpdateCallback? onInteractionUpdate; + + final TransformationController? transformationController; + + static const double _kDrag = 0.0000135; + + /// Returns the closest point to the given point on the given line segment. + @visibleForTesting + static Vector3 getNearestPointOnLine(Vector3 point, Vector3 l1, Vector3 l2) { + final lengthSquared = math.pow(l2.x - l1.x, 2.0).toDouble() + + math.pow(l2.y - l1.y, 2.0).toDouble(); + + // In this case, l1 == l2. + if (lengthSquared == 0) { + return l1; + } + + // Calculate how far down the line segment the closest point is and return + // the point. + final l1P = point - l1; + final l1L2 = l2 - l1; + final fraction = clampDouble(l1P.dot(l1L2) / lengthSquared, 0, 1); + return l1 + l1L2 * fraction; + } + + /// Returns the axis aligned bounding box for the given Quad, which might not + /// be axis aligned. + static Rect axisAlignedBoundingBox(Quad quad) { + var xMin = quad.point0.x; + var xMax = quad.point0.x; + var yMin = quad.point0.y; + var yMax = quad.point0.y; + for (final point in [ + quad.point1, + quad.point2, + quad.point3, + ]) { + if (point.x < xMin) { + xMin = point.x; + } else if (point.x > xMax) { + xMax = point.x; + } + + if (point.y < yMin) { + yMin = point.y; + } else if (point.y > yMax) { + yMax = point.y; + } + } + + return Rect.fromLTRB(xMin, yMin, xMax, yMax); + } + + /// Given a quad, return its axis aligned bounding box. + @visibleForTesting + static Quad getAxisAlignedBoundingBox(Quad quad) { + final double minX = math.min( + quad.point0.x, + math.min( + quad.point1.x, + math.min( + quad.point2.x, + quad.point3.x, + ), + ), + ); + final double minY = math.min( + quad.point0.y, + math.min( + quad.point1.y, + math.min( + quad.point2.y, + quad.point3.y, + ), + ), + ); + final double maxX = math.max( + quad.point0.x, + math.max( + quad.point1.x, + math.max( + quad.point2.x, + quad.point3.x, + ), + ), + ); + final double maxY = math.max( + quad.point0.y, + math.max( + quad.point1.y, + math.max( + quad.point2.y, + quad.point3.y, + ), + ), + ); + return Quad.points( + Vector3(minX, minY, 0), + Vector3(maxX, minY, 0), + Vector3(maxX, maxY, 0), + Vector3(minX, maxY, 0), + ); + } + + /// Returns true iff the point is inside the rectangle given by the Quad, + /// inclusively. + /// Algorithm from https://math.stackexchange.com/a/190373. + @visibleForTesting + static bool pointIsInside(Vector3 point, Quad quad) { + final aM = point - quad.point0; + final aB = quad.point1 - quad.point0; + final aD = quad.point3 - quad.point0; + + final aMAB = aM.dot(aB); + final aBAB = aB.dot(aB); + final aMAD = aM.dot(aD); + final aDAD = aD.dot(aD); + + return 0 <= aMAB && aMAB <= aBAB && 0 <= aMAD && aMAD <= aDAD; + } + + /// Get the point inside (inclusively) the given Quad that is nearest to the + /// given Vector3. + @visibleForTesting + static Vector3 getNearestPointInside(Vector3 point, Quad quad) { + // If the point is inside the axis aligned bounding box, then it's ok where + // it is. + if (pointIsInside(point, quad)) { + return point; + } + + // Otherwise, return the nearest point on the quad. + final closestPoints = [ + CustomInteractiveViewer.getNearestPointOnLine( + point, + quad.point0, + quad.point1, + ), + CustomInteractiveViewer.getNearestPointOnLine( + point, + quad.point1, + quad.point2, + ), + CustomInteractiveViewer.getNearestPointOnLine( + point, + quad.point2, + quad.point3, + ), + CustomInteractiveViewer.getNearestPointOnLine( + point, + quad.point3, + quad.point0, + ), + ]; + var minDistance = double.infinity; + late Vector3 closestOverall; + for (final closePoint in closestPoints) { + final distance = math.sqrt( + math.pow(point.x - closePoint.x, 2) + + math.pow(point.y - closePoint.y, 2), + ); + if (distance < minDistance) { + minDistance = distance; + closestOverall = closePoint; + } + } + return closestOverall; + } + + /// Transform the four corners of the viewport by the inverse of the given + /// matrix. This gives the viewport after the child has been transformed by the + /// given matrix. The viewport transforms as the inverse of the child (i.e. + /// moving the child left is equivalent to moving the viewport right). + static Quad transformViewport(Matrix4 matrix, Rect viewport) { + final inverseMatrix = matrix.clone()..invert(); + return Quad.points( + inverseMatrix.transform3( + Vector3( + viewport.topLeft.dx, + viewport.topLeft.dy, + 0, + ), + ), + inverseMatrix.transform3( + Vector3( + viewport.topRight.dx, + viewport.topRight.dy, + 0, + ), + ), + inverseMatrix.transform3( + Vector3( + viewport.bottomRight.dx, + viewport.bottomRight.dy, + 0, + ), + ), + inverseMatrix.transform3( + Vector3( + viewport.bottomLeft.dx, + viewport.bottomLeft.dy, + 0, + ), + ), + ); + } + + @override + State createState() => + _CustomInteractiveViewerState(); +} + +class _CustomInteractiveViewerState extends State + with TickerProviderStateMixin { + TransformationController? _transformationController; + + final GlobalKey _childKey = GlobalKey(); + final GlobalKey _parentKey = GlobalKey(); + Animation? _animation; + Animation? _scaleAnimation; + late Offset _scaleAnimationFocalPoint; + late AnimationController _controller; + late AnimationController _scaleController; + Axis? _currentAxis; // Used with panAxis. + Offset? _referenceFocalPoint; // Point where the current gesture began. + double? _scaleStart; // Scale value at start of scaling gesture. + final double _currentRotation = + 0; // Rotation of _transformationController.value. + _GestureType? _gestureType; + + // The _boundaryRect is calculated by adding the boundaryMargin to the size of + // the child. + Rect get _boundaryRect { + assert(_childKey.currentContext != null); + assert(!widget.boundaryMargin.left.isNaN); + assert(!widget.boundaryMargin.right.isNaN); + assert(!widget.boundaryMargin.top.isNaN); + assert(!widget.boundaryMargin.bottom.isNaN); + + final childRenderBox = + _childKey.currentContext!.findRenderObject()! as RenderBox; + final childSize = childRenderBox.size; + final boundaryRect = + widget.boundaryMargin.inflateRect(Offset.zero & childSize); + assert( + !boundaryRect.isEmpty, + "CustomInteractiveViewer's child must have nonzero dimensions.", + ); + // Boundaries that are partially infinite are not allowed because Matrix4's + // rotation and translation methods don't handle infinite well. + assert( + boundaryRect.isFinite || + (boundaryRect.left.isInfinite && + boundaryRect.top.isInfinite && + boundaryRect.right.isInfinite && + boundaryRect.bottom.isInfinite), + 'boundaryRect must either be infinite in all directions or finite in all directions.', + ); + return boundaryRect; + } + + // The Rect representing the child's parent. + Rect get _viewport { + assert(_parentKey.currentContext != null); + final parentRenderBox = + _parentKey.currentContext!.findRenderObject()! as RenderBox; + return Offset.zero & parentRenderBox.size; + } + + // Return a new matrix representing the given matrix after applying the given + // translation. + Matrix4 _matrixTranslate(Matrix4 matrix, Offset translation) { + if (translation == Offset.zero) { + return matrix.clone(); + } + + final Offset alignedTranslation; + + if (_currentAxis != null) { + alignedTranslation = switch (widget.panAxis) { + PanAxis.horizontal => _alignAxis(translation, Axis.horizontal), + PanAxis.vertical => _alignAxis(translation, Axis.vertical), + PanAxis.aligned => _alignAxis(translation, _currentAxis!), + PanAxis.free => translation, + }; + } else { + alignedTranslation = translation; + } + + final nextMatrix = matrix.clone() + ..translate( + alignedTranslation.dx, + alignedTranslation.dy, + ); + + // Transform the viewport to determine where its four corners will be after + // the child has been transformed. + final nextViewport = CustomInteractiveViewer.transformViewport( + nextMatrix, + _viewport, + ); + + // If the boundaries are infinite, then no need to check if the translation + // fits within them. + if (_boundaryRect.isInfinite) { + return nextMatrix; + } + + // Expand the boundaries with rotation. This prevents the problem where a + // mismatch in orientation between the viewport and boundaries effectively + // limits translation. With this approach, all points that are visible with + // no rotation are visible after rotation. + final boundariesAabbQuad = _getAxisAlignedBoundingBoxWithRotation( + _boundaryRect, + _currentRotation, + ); + + // If the given translation fits completely within the boundaries, allow it. + final offendingDistance = _exceedsBy(boundariesAabbQuad, nextViewport); + if (offendingDistance == Offset.zero) { + return nextMatrix; + } + + // Desired translation goes out of bounds, so translate to the nearest + // in-bounds point instead. + final nextTotalTranslation = _getMatrixTranslation(nextMatrix); + final currentScale = matrix.getMaxScaleOnAxis(); + final correctedTotalTranslation = Offset( + nextTotalTranslation.dx - offendingDistance.dx * currentScale, + nextTotalTranslation.dy - offendingDistance.dy * currentScale, + ); + + final correctedMatrix = matrix.clone() + ..setTranslation( + Vector3( + correctedTotalTranslation.dx, + correctedTotalTranslation.dy, + 0, + ), + ); + + // Double check that the corrected translation fits. + final correctedViewport = CustomInteractiveViewer.transformViewport( + correctedMatrix, + _viewport, + ); + final offendingCorrectedDistance = + _exceedsBy(boundariesAabbQuad, correctedViewport); + if (offendingCorrectedDistance == Offset.zero) { + return correctedMatrix; + } + + // If the corrected translation doesn't fit in either direction, don't allow + // any translation at all. This happens when the viewport is larger than the + // entire boundary. + if (offendingCorrectedDistance.dx != 0.0 && + offendingCorrectedDistance.dy != 0.0) { + return matrix.clone(); + } + + // Otherwise, allow translation in only the direction that fits. This + // happens when the viewport is larger than the boundary in one direction. + final unidirectionalCorrectedTotalTranslation = Offset( + offendingCorrectedDistance.dx == 0.0 ? correctedTotalTranslation.dx : 0.0, + offendingCorrectedDistance.dy == 0.0 ? correctedTotalTranslation.dy : 0.0, + ); + return matrix.clone() + ..setTranslation( + Vector3( + unidirectionalCorrectedTotalTranslation.dx, + unidirectionalCorrectedTotalTranslation.dy, + 0, + ), + ); + } + + // Return a new matrix representing the given matrix after applying the given + // scale. + Matrix4 _matrixScale(Matrix4 matrix, double scale) { + if (scale == 1.0) { + return matrix.clone(); + } + assert(scale != 0.0); + + // Don't allow a scale that results in an overall scale beyond min/max + // scale. + final currentScale = _transformationController!.value.getMaxScaleOnAxis(); + final double totalScale = math.max( + currentScale * scale, + // Ensure that the scale cannot make the child so big that it can't fit + // inside the boundaries (in either direction). + math.max( + _viewport.width / _boundaryRect.width, + _viewport.height / _boundaryRect.height, + ), + ); + final clampedTotalScale = clampDouble( + totalScale, + widget.minScale, + widget.maxScale, + ); + final clampedScale = clampedTotalScale / currentScale; + return matrix.clone()..scale(clampedScale); + } + + // Returns true iff the given _GestureType is enabled. + bool _gestureIsSupported(_GestureType? gestureType) { + return switch (gestureType) { + _GestureType.scale => widget.scaleEnabled, + _GestureType.pan || null => widget.panEnabled, + }; + } + + // Decide which type of gesture this is by comparing the amount of scale + // and rotation in the gesture, if any. Scale starts at 1 and rotation + // starts at 0. Pan will have no scale and no rotation because it uses only one + // finger. + _GestureType _getGestureType(ScaleUpdateDetails details) { + final scale = !widget.scaleEnabled ? 1.0 : details.scale; + if (scale != 1) { + return _GestureType.scale; + } else { + return _GestureType.pan; + } + } + + // Handle the start of a gesture. All of pan, scale, and rotate are handled + // with GestureDetector's scale gesture. + void _onScaleStart(ScaleStartDetails details) { + widget.onInteractionStart?.call(details); + + if (_controller.isAnimating) { + _controller + ..stop() + ..reset(); + _animation?.removeListener(_onAnimate); + _animation = null; + } + if (_scaleController.isAnimating) { + _scaleController + ..stop() + ..reset(); + _scaleAnimation?.removeListener(_onScaleAnimate); + _scaleAnimation = null; + } + + _gestureType = null; + _currentAxis = null; + _scaleStart = _transformationController!.value.getMaxScaleOnAxis(); + _referenceFocalPoint = _transformationController!.toScene( + details.localFocalPoint, + ); + } + + // Handle an update to an ongoing gesture. All of pan, scale, and rotate are + // handled with GestureDetector's scale gesture. + void _onScaleUpdate(ScaleUpdateDetails details) { + final scale = _transformationController!.value.getMaxScaleOnAxis(); + _scaleAnimationFocalPoint = details.localFocalPoint; + final focalPointScene = _transformationController!.toScene( + details.localFocalPoint, + ); + + if (_gestureType == _GestureType.pan) { + // When a gesture first starts, it sometimes has no change in scale and + // rotation despite being a two-finger gesture. Here the gesture is + // allowed to be reinterpreted as its correct type after originally + // being marked as a pan. + _gestureType = _getGestureType(details); + } else { + _gestureType ??= _getGestureType(details); + } + if (!_gestureIsSupported(_gestureType)) { + widget.onInteractionUpdate?.call(details); + return; + } + + switch (_gestureType!) { + case _GestureType.scale: + assert(_scaleStart != null); + // details.scale gives us the amount to change the scale as of the + // start of this gesture, so calculate the amount to scale as of the + // previous call to _onScaleUpdate. + final desiredScale = _scaleStart! * details.scale; + final scaleChange = desiredScale / scale; + _transformationController!.value = _matrixScale( + _transformationController!.value, + scaleChange, + ); + + // While scaling, translate such that the user's two fingers stay on + // the same places in the scene. That means that the focal point of + // the scale should be on the same place in the scene before and after + // the scale. + final focalPointSceneScaled = _transformationController!.toScene( + details.localFocalPoint, + ); + _transformationController!.value = _matrixTranslate( + _transformationController!.value, + focalPointSceneScaled - _referenceFocalPoint!, + ); + + // details.localFocalPoint should now be at the same location as the + // original _referenceFocalPoint point. If it's not, that's because + // the translate came in contact with a boundary. In that case, update + // _referenceFocalPoint so subsequent updates happen in relation to + // the new effective focal point. + final focalPointSceneCheck = _transformationController!.toScene( + details.localFocalPoint, + ); + if (_round(_referenceFocalPoint!) != _round(focalPointSceneCheck)) { + _referenceFocalPoint = focalPointSceneCheck; + } + + case _GestureType.pan: + assert(_referenceFocalPoint != null); + // details may have a change in scale here when scaleEnabled is false. + // In an effort to keep the behavior similar whether or not scaleEnabled + // is true, these gestures are thrown away. + if (details.scale != 1.0) { + widget.onInteractionUpdate?.call(details); + return; + } + _currentAxis ??= _getPanAxis(_referenceFocalPoint!, focalPointScene); + // Translate so that the same point in the scene is underneath the + // focal point before and after the movement. + final translationChange = focalPointScene - _referenceFocalPoint!; + _transformationController!.value = _matrixTranslate( + _transformationController!.value, + translationChange, + ); + _referenceFocalPoint = _transformationController!.toScene( + details.localFocalPoint, + ); + } + widget.onInteractionUpdate?.call(details); + } + + // Handle the end of a gesture of _GestureType. All of pan, scale, and rotate + // are handled with GestureDetector's scale gesture. + void _onScaleEnd(ScaleEndDetails details) { + widget.onInteractionEnd?.call(details); + _scaleStart = null; + _referenceFocalPoint = null; + + _animation?.removeListener(_onAnimate); + _scaleAnimation?.removeListener(_onScaleAnimate); + _controller.reset(); + _scaleController.reset(); + + if (!_gestureIsSupported(_gestureType)) { + _currentAxis = null; + return; + } + + switch (_gestureType) { + case _GestureType.pan: + if (details.velocity.pixelsPerSecond.distance < kMinFlingVelocity) { + _currentAxis = null; + return; + } + final translationVector = + _transformationController!.value.getTranslation(); + final translation = Offset(translationVector.x, translationVector.y); + final frictionSimulationX = FrictionSimulation( + widget.interactionEndFrictionCoefficient, + translation.dx, + details.velocity.pixelsPerSecond.dx, + ); + final frictionSimulationY = FrictionSimulation( + widget.interactionEndFrictionCoefficient, + translation.dy, + details.velocity.pixelsPerSecond.dy, + ); + final tFinal = _getFinalTime( + details.velocity.pixelsPerSecond.distance, + widget.interactionEndFrictionCoefficient, + ); + _animation = Tween( + begin: translation, + end: Offset(frictionSimulationX.finalX, frictionSimulationY.finalX), + ).animate( + CurvedAnimation( + parent: _controller, + curve: Curves.decelerate, + ), + ); + _controller.duration = Duration(milliseconds: (tFinal * 1000).round()); + _animation!.addListener(_onAnimate); + _controller.forward(); + case _GestureType.scale: + if (details.scaleVelocity.abs() < 0.1) { + _currentAxis = null; + return; + } + final scale = _transformationController!.value.getMaxScaleOnAxis(); + final frictionSimulation = FrictionSimulation( + widget.interactionEndFrictionCoefficient * widget.scaleFactor, + scale, + details.scaleVelocity / 10, + ); + final tFinal = _getFinalTime( + details.scaleVelocity.abs(), + widget.interactionEndFrictionCoefficient, + effectivelyMotionless: 0.1, + ); + _scaleAnimation = + Tween(begin: scale, end: frictionSimulation.x(tFinal)) + .animate( + CurvedAnimation( + parent: _scaleController, + curve: Curves.decelerate, + ), + ); + _scaleController.duration = + Duration(milliseconds: (tFinal * 1000).round()); + _scaleAnimation!.addListener(_onScaleAnimate); + _scaleController.forward(); + case null: + break; + } + } + + // Handle mousewheel and web trackpad scroll events. + void _receivedPointerSignal(PointerSignalEvent event) { + final double scaleChange; + if (event is PointerScrollEvent) { + if (event.kind == PointerDeviceKind.trackpad && + !widget.trackpadScrollCausesScale) { + // Trackpad scroll, so treat it as a pan. + widget.onInteractionStart?.call( + ScaleStartDetails( + focalPoint: event.position, + localFocalPoint: event.localPosition, + ), + ); + + final localDelta = PointerEvent.transformDeltaViaPositions( + untransformedEndPosition: event.position + event.scrollDelta, + untransformedDelta: event.scrollDelta, + transform: event.transform, + ); + + if (!_gestureIsSupported(_GestureType.pan)) { + widget.onInteractionUpdate?.call( + ScaleUpdateDetails( + focalPoint: event.position - event.scrollDelta, + localFocalPoint: event.localPosition - event.scrollDelta, + focalPointDelta: -localDelta, + ), + ); + widget.onInteractionEnd?.call(ScaleEndDetails()); + return; + } + + final focalPointScene = _transformationController!.toScene( + event.localPosition, + ); + + final newFocalPointScene = _transformationController!.toScene( + event.localPosition - localDelta, + ); + + _transformationController!.value = _matrixTranslate( + _transformationController!.value, + newFocalPointScene - focalPointScene, + ); + + widget.onInteractionUpdate?.call( + ScaleUpdateDetails( + focalPoint: event.position - event.scrollDelta, + localFocalPoint: event.localPosition - localDelta, + focalPointDelta: -localDelta, + ), + ); + widget.onInteractionEnd?.call(ScaleEndDetails()); + return; + } + // Ignore left and right mouse wheel scroll. + if (event.scrollDelta.dy == 0.0) { + return; + } + scaleChange = math.exp(-event.scrollDelta.dy / widget.scaleFactor); + } else if (event is PointerScaleEvent) { + scaleChange = event.scale; + } else { + return; + } + widget.onInteractionStart?.call( + ScaleStartDetails( + focalPoint: event.position, + localFocalPoint: event.localPosition, + ), + ); + + if (!_gestureIsSupported(_GestureType.scale)) { + widget.onInteractionUpdate?.call( + ScaleUpdateDetails( + focalPoint: event.position, + localFocalPoint: event.localPosition, + scale: scaleChange, + ), + ); + widget.onInteractionEnd?.call(ScaleEndDetails()); + return; + } + + final focalPointScene = _transformationController!.toScene( + event.localPosition, + ); + + _transformationController!.value = _matrixScale( + _transformationController!.value, + scaleChange, + ); + + // After scaling, translate such that the event's position is at the + // same scene point before and after the scale. + final focalPointSceneScaled = _transformationController!.toScene( + event.localPosition, + ); + _transformationController!.value = _matrixTranslate( + _transformationController!.value, + focalPointSceneScaled - focalPointScene, + ); + + widget.onInteractionUpdate?.call( + ScaleUpdateDetails( + focalPoint: event.position, + localFocalPoint: event.localPosition, + scale: scaleChange, + ), + ); + widget.onInteractionEnd?.call(ScaleEndDetails()); + } + + // Handle inertia drag animation. + void _onAnimate() { + if (!_controller.isAnimating) { + _currentAxis = null; + _animation?.removeListener(_onAnimate); + _animation = null; + _controller.reset(); + return; + } + // Translate such that the resulting translation is _animation.value. + final translationVector = _transformationController!.value.getTranslation(); + final translation = Offset(translationVector.x, translationVector.y); + final translationScene = _transformationController!.toScene( + translation, + ); + final animationScene = _transformationController!.toScene( + _animation!.value, + ); + final translationChangeScene = animationScene - translationScene; + _transformationController!.value = _matrixTranslate( + _transformationController!.value, + translationChangeScene, + ); + } + + // Handle inertia scale animation. + void _onScaleAnimate() { + if (!_scaleController.isAnimating) { + _currentAxis = null; + _scaleAnimation?.removeListener(_onScaleAnimate); + _scaleAnimation = null; + _scaleController.reset(); + return; + } + final desiredScale = _scaleAnimation!.value; + final scaleChange = + desiredScale / _transformationController!.value.getMaxScaleOnAxis(); + final referenceFocalPoint = _transformationController!.toScene( + _scaleAnimationFocalPoint, + ); + _transformationController!.value = _matrixScale( + _transformationController!.value, + scaleChange, + ); + + // While scaling, translate such that the user's two fingers stay on + // the same places in the scene. That means that the focal point of + // the scale should be on the same place in the scene before and after + // the scale. + final focalPointSceneScaled = _transformationController!.toScene( + _scaleAnimationFocalPoint, + ); + _transformationController!.value = _matrixTranslate( + _transformationController!.value, + focalPointSceneScaled - referenceFocalPoint, + ); + } + + void _onTransformationControllerChange() { + // A change to the TransformationController's value is a change to the + // state. + setState(() {}); + } + + @override + void initState() { + super.initState(); + + _transformationController = + widget.transformationController ?? TransformationController(); + _transformationController!.addListener(_onTransformationControllerChange); + _controller = AnimationController( + vsync: this, + ); + _scaleController = AnimationController(vsync: this); + } + + @override + void didUpdateWidget(CustomInteractiveViewer oldWidget) { + super.didUpdateWidget(oldWidget); + // Handle all cases of needing to dispose and initialize + // transformationControllers. + if (oldWidget.transformationController == null) { + if (widget.transformationController != null) { + _transformationController! + .removeListener(_onTransformationControllerChange); + _transformationController!.dispose(); + _transformationController = widget.transformationController; + _transformationController! + .addListener(_onTransformationControllerChange); + } + } else { + if (widget.transformationController == null) { + _transformationController! + .removeListener(_onTransformationControllerChange); + _transformationController = TransformationController(); + _transformationController! + .addListener(_onTransformationControllerChange); + } else if (widget.transformationController != + oldWidget.transformationController) { + _transformationController! + .removeListener(_onTransformationControllerChange); + _transformationController = widget.transformationController; + _transformationController! + .addListener(_onTransformationControllerChange); + } + } + } + + @override + void dispose() { + _controller.dispose(); + _scaleController.dispose(); + _transformationController! + .removeListener(_onTransformationControllerChange); + if (widget.transformationController == null) { + _transformationController!.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + Widget child; + if (widget.child != null) { + child = _CustomInteractiveViewerBuilt( + childKey: _childKey, + clipBehavior: widget.clipBehavior, + constrained: widget.constrained, + matrix: _transformationController!.value, + alignment: widget.alignment, + child: widget.child!, + ); + } else { + // When using CustomInteractiveViewer.builder, then constrained is false and the + // viewport is the size of the constraints. + assert(widget.builder != null); + assert(!widget.constrained); + child = LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + final matrix = _transformationController!.value; + return _CustomInteractiveViewerBuilt( + childKey: _childKey, + clipBehavior: widget.clipBehavior, + constrained: widget.constrained, + alignment: widget.alignment, + matrix: matrix, + child: widget.builder!( + context, + CustomInteractiveViewer.transformViewport( + matrix, + Offset.zero & constraints.biggest, + ), + ), + ); + }, + ); + } + + return Listener( + key: _parentKey, + onPointerSignal: _receivedPointerSignal, + child: GestureDetector( + behavior: HitTestBehavior.opaque, // Necessary when panning off screen. + onScaleEnd: _onScaleEnd, + onScaleStart: _onScaleStart, + onScaleUpdate: _onScaleUpdate, + trackpadScrollCausesScale: widget.trackpadScrollCausesScale, + trackpadScrollToScaleFactor: Offset(0, -1 / widget.scaleFactor), + child: child, + ), + ); + } +} + +// This widget allows us to easily swap in and out the LayoutBuilder in +// CustomInteractiveViewer's depending on if it's using a builder or a child. +class _CustomInteractiveViewerBuilt extends StatelessWidget { + const _CustomInteractiveViewerBuilt({ + required this.child, + required this.childKey, + required this.clipBehavior, + required this.constrained, + required this.matrix, + required this.alignment, + }); + + final Widget child; + final GlobalKey childKey; + final Clip clipBehavior; + final bool constrained; + final Matrix4 matrix; + final Alignment? alignment; + + @override + Widget build(BuildContext context) { + Widget child = KeyedSubtree( + key: childKey, + child: this.child, + ); + + if (!constrained) { + child = OverflowBox( + alignment: Alignment.topLeft, + minWidth: 0, + minHeight: 0, + maxWidth: double.infinity, + maxHeight: double.infinity, + child: child, + ); + } + + return ClipRect( + clipBehavior: clipBehavior, + child: child, + ); + } +} + +// A classification of relevant user gestures. Each contiguous user gesture is +// represented by exactly one _GestureType. +enum _GestureType { + pan, + scale, +} + +// Given a velocity and drag, calculate the time at which motion will come to +// a stop, within the margin of effectivelyMotionless. +double _getFinalTime( + double velocity, + double drag, { + double effectivelyMotionless = 10, +}) { + return math.log(effectivelyMotionless / velocity) / math.log(drag / 100); +} + +// Return the translation from the given Matrix4 as an Offset. +Offset _getMatrixTranslation(Matrix4 matrix) { + final nextTranslation = matrix.getTranslation(); + return Offset(nextTranslation.x, nextTranslation.y); +} + +// Find the axis aligned bounding box for the rect rotated about its center by +// the given amount. +Quad _getAxisAlignedBoundingBoxWithRotation(Rect rect, double rotation) { + final rotationMatrix = Matrix4.identity() + ..translate(rect.size.width / 2, rect.size.height / 2) + ..rotateZ(rotation) + ..translate(-rect.size.width / 2, -rect.size.height / 2); + final boundariesRotated = Quad.points( + rotationMatrix.transform3(Vector3(rect.left, rect.top, 0)), + rotationMatrix.transform3(Vector3(rect.right, rect.top, 0)), + rotationMatrix.transform3(Vector3(rect.right, rect.bottom, 0)), + rotationMatrix.transform3(Vector3(rect.left, rect.bottom, 0)), + ); + return CustomInteractiveViewer.getAxisAlignedBoundingBox(boundariesRotated); +} + +// Return the amount that viewport lies outside of boundary. If the viewport +// is completely contained within the boundary (inclusively), then returns +// Offset.zero. +Offset _exceedsBy(Quad boundary, Quad viewport) { + final viewportPoints = [ + viewport.point0, + viewport.point1, + viewport.point2, + viewport.point3, + ]; + var largestExcess = Offset.zero; + for (final point in viewportPoints) { + final pointInside = + CustomInteractiveViewer.getNearestPointInside(point, boundary); + final excess = Offset( + pointInside.x - point.x, + pointInside.y - point.y, + ); + if (excess.dx.abs() > largestExcess.dx.abs()) { + largestExcess = Offset(excess.dx, largestExcess.dy); + } + if (excess.dy.abs() > largestExcess.dy.abs()) { + largestExcess = Offset(largestExcess.dx, excess.dy); + } + } + + return _round(largestExcess); +} + +// Round the output values. This works around a precision problem where +// values that should have been zero were given as within 10^-10 of zero. +Offset _round(Offset offset) { + return Offset( + double.parse(offset.dx.toStringAsFixed(9)), + double.parse(offset.dy.toStringAsFixed(9)), + ); +} + +// Align the given offset to the given axis by allowing movement only in the +// axis direction. +Offset _alignAxis(Offset offset, Axis axis) { + return switch (axis) { + Axis.horizontal => Offset(offset.dx, 0), + Axis.vertical => Offset(0, offset.dy), + }; +} + +// Given two points, return the axis where the distance between the points is +// greatest. If they are equal, return null. +Axis? _getPanAxis(Offset point1, Offset point2) { + if (point1 == point2) { + return null; + } + final x = point2.dx - point1.dx; + final y = point2.dy - point1.dy; + return x.abs() > y.abs() ? Axis.horizontal : Axis.vertical; +} diff --git a/lib/src/chart/base/line.dart b/lib/src/chart/base/line.dart new file mode 100644 index 0000000..7a42eca --- /dev/null +++ b/lib/src/chart/base/line.dart @@ -0,0 +1,34 @@ +import 'dart:math' as math; + +import 'package:flutter/painting.dart'; + +/// Describes a line model (contains [from], and end [to]) +class Line { + const Line(this.from, this.to); + + /// Start of the line + final Offset from; + + /// End of the line + final Offset to; + + /// Returns the length of line + double magnitude() { + final diff = to - from; + final dx = diff.dx; + final dy = diff.dy; + return math.sqrt(dx * dx + dy * dy); + } + + /// Returns angle of the line in radians + double direction() { + final diff = to - from; + return math.atan(diff.dy / diff.dx); + } + + /// Returns the line in magnitude of 1.0 + Offset normalize() { + final diffOffset = to - from; + return diffOffset * (1.0 / magnitude()); + } +} diff --git a/lib/src/chart/candlestick_chart/candlestick_chart.dart b/lib/src/chart/candlestick_chart/candlestick_chart.dart new file mode 100644 index 0000000..a87b480 --- /dev/null +++ b/lib/src/chart/candlestick_chart/candlestick_chart.dart @@ -0,0 +1,184 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/base/axis_chart/axis_chart_scaffold_widget.dart'; +import 'package:fl_chart/src/chart/candlestick_chart/candlestick_chart_renderer.dart'; +import 'package:flutter/material.dart'; + +/// Renders a pie chart as a widget, using provided [CandlestickChartData]. +class CandlestickChart extends ImplicitlyAnimatedWidget { + /// [data] determines how the [CandlestickChart] should be look like, + /// when you make any change in the [CandlestickChartData], it updates + /// new values with animation, and duration is [duration]. + /// also you can change the [curve] + /// which default is [Curves.linear]. + const CandlestickChart( + this.data, { + this.chartRendererKey, + super.key, + @Deprecated('Please use [duration] instead') + Duration? swapAnimationDuration, + Duration duration = const Duration(milliseconds: 150), + @Deprecated('Please use [curve] instead') Curve? swapAnimationCurve, + Curve curve = Curves.linear, + this.transformationConfig = const FlTransformationConfig(), + }) : super( + duration: swapAnimationDuration ?? duration, + curve: swapAnimationCurve ?? curve, + ); + + /// Determines how the [CandlestickChart] should be look like. + final CandlestickChartData data; + + /// {@macro fl_chart.AxisChartScaffoldWidget.transformationConfig} + final FlTransformationConfig transformationConfig; + + /// We pass this key to our renderers which are responsible to + /// render the chart itself (without anything around the chart). + final Key? chartRendererKey; + + /// Creates a [_CandlestickChartState] + @override + _CandlestickChartState createState() => _CandlestickChartState(); +} + +class _CandlestickChartState extends AnimatedWidgetBaseState { + /// we handle under the hood animations (implicit animations) via this tween, + /// it lerps between the old [CandlestickChartData] to the new one. + CandlestickChartDataTween? _candlestickChartDataTween; + + /// If [CandlestickTouchData.handleBuiltInTouches] is true, we override the callback to handle touches internally, + /// but we need to keep the provided callback to notify it too. + BaseTouchCallback? _providedTouchCallback; + + ({ + Offset axisCoordinate, + int spotIndex, + })? touchedSpots; + + @override + Widget build(BuildContext context) { + final showingData = _getData(); + + return AxisChartScaffoldWidget( + data: showingData, + transformationConfig: widget.transformationConfig, + chartBuilder: (context, chartVirtualRect) => CandlestickChartLeaf( + data: _withTouchedIndicators( + _candlestickChartDataTween!.evaluate(animation), + ), + targetData: _withTouchedIndicators(showingData), + key: widget.chartRendererKey, + chartVirtualRect: chartVirtualRect, + canBeScaled: widget.transformationConfig.scaleAxis != FlScaleAxis.none, + ), + ); + } + + CandlestickChartData _withTouchedIndicators( + CandlestickChartData candlestickChartData, + ) { + if (!candlestickChartData.candlestickTouchData.enabled || + !candlestickChartData.candlestickTouchData.handleBuiltInTouches) { + return candlestickChartData; + } + + final spot = touchedSpots != null && touchedSpots!.spotIndex != -1 + ? candlestickChartData.candlestickSpots[touchedSpots!.spotIndex] + : null; + final touchInsideChart = touchedSpots != null && + touchedSpots!.axisCoordinate.dx >= candlestickChartData.minX && + touchedSpots!.axisCoordinate.dy >= candlestickChartData.minY && + touchedSpots!.axisCoordinate.dx <= candlestickChartData.maxX && + touchedSpots!.axisCoordinate.dy <= candlestickChartData.maxY; + + final providedPainter = candlestickChartData.touchedPointIndicator?.painter; + final providedX = candlestickChartData.touchedPointIndicator?.x; + final providedY = candlestickChartData.touchedPointIndicator?.y; + return candlestickChartData.copyWith( + showingTooltipIndicators: + touchedSpots != null ? [touchedSpots!.spotIndex] : [], + touchedPointIndicator: touchedSpots != null + ? AxisSpotIndicator( + x: providedX ?? spot?.x, + y: providedY ?? + (touchInsideChart ? touchedSpots!.axisCoordinate.dy : null), + painter: providedPainter ?? + AxisLinesIndicatorPainter( + horizontalLineProvider: (y) => HorizontalLine( + y: y, + color: Theme.of(context).colorScheme.outline.withValues( + alpha: 0.5, + ), + strokeWidth: 1, + ), + verticalLineProvider: (x) => VerticalLine( + x: x, + color: Theme.of(context).colorScheme.outline.withValues( + alpha: 0.5, + ), + strokeWidth: 1, + ), + ), + ) + : null, + ); + } + + CandlestickChartData _getData() { + final candlestickTouchData = widget.data.candlestickTouchData; + if (candlestickTouchData.enabled && + candlestickTouchData.handleBuiltInTouches) { + _providedTouchCallback = candlestickTouchData.touchCallback; + return widget.data.copyWith( + candlestickTouchData: widget.data.candlestickTouchData + .copyWith(touchCallback: _handleBuiltInTouch), + ); + } + return widget.data; + } + + void _handleBuiltInTouch( + FlTouchEvent event, + CandlestickTouchResponse? touchResponse, + ) { + if (!mounted) { + return; + } + _providedTouchCallback?.call(event, touchResponse); + + final desiredTouch = event.isInterestedForInteractions; + + if (!desiredTouch || + touchResponse == null || + touchResponse.touchedSpot == null) { + setState(() { + if (desiredTouch) { + touchedSpots = ( + axisCoordinate: touchResponse?.touchChartCoordinate ?? Offset.zero, + spotIndex: -1, + ); + } else { + touchedSpots = null; + } + }); + return; + } + setState(() { + touchedSpots = ( + axisCoordinate: touchResponse.touchChartCoordinate, + spotIndex: touchResponse.touchedSpot!.spotIndex, + ); + }); + } + + @override + void forEachTween(TweenVisitor visitor) { + _candlestickChartDataTween = visitor( + _candlestickChartDataTween, + _getData(), + (dynamic value) => CandlestickChartDataTween( + begin: value as CandlestickChartData, + end: widget.data, + ), + ) as CandlestickChartDataTween?; + } +} diff --git a/lib/src/chart/candlestick_chart/candlestick_chart_data.dart b/lib/src/chart/candlestick_chart/candlestick_chart_data.dart new file mode 100644 index 0000000..b05b642 --- /dev/null +++ b/lib/src/chart/candlestick_chart/candlestick_chart_data.dart @@ -0,0 +1,999 @@ +// coverage:ignore-file +import 'dart:math'; +import 'dart:ui'; + +import 'package:equatable/equatable.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/candlestick_chart/candlestick_chart_helper.dart'; +import 'package:fl_chart/src/extensions/color_extension.dart'; +import 'package:fl_chart/src/utils/lerp.dart'; +import 'package:flutter/material.dart'; + +/// [CandlestickChart] needs this class to render itself. +/// +/// It holds data needed to draw a candlestick chart, +/// including background color, Candlestick spots, ... +class CandlestickChartData extends AxisChartData with EquatableMixin { + /// [CandlestickChart] draws some candlesticks on the chart based on + /// the provided [candlestickSpots], + /// + /// It draws some titles on left, top, right, bottom sides per each axis number, + /// you can modify [titlesData] to have your custom titles, + /// + /// It draws a color as a background behind everything you can set it using [backgroundColor], + /// then a grid over it, you can customize it using [gridData], + /// and it draws 4 borders around your chart, you can customize it using [borderData]. + /// + /// You can modify [candlestickTouchData] to customize touch behaviors and responses. + /// + /// You can show some tooltipIndicators (a popup with an information) + /// on top of each [CandlestickChartData.candleSpots] using [showingTooltipIndicators], + /// just put spot indices you want to show it on top of them. + /// + /// [clipData] forces the [CandlestickChart] to draw lines inside the chart bounding box. + CandlestickChartData({ + List? candlestickSpots, + FlCandlestickPainter? candlestickPainter, + FlTitlesData? titlesData, + CandlestickTouchData? candlestickTouchData, + List? showingTooltipIndicators, + FlGridData? gridData, + super.borderData, + double? minX, + double? maxX, + super.baselineX, + double? minY, + double? maxY, + super.baselineY, + super.rangeAnnotations, + FlClipData? clipData, + super.backgroundColor, + super.rotationQuarterTurns, + this.touchedPointIndicator, + }) : candlestickSpots = candlestickSpots ?? const [], + candlestickPainter = candlestickPainter ?? DefaultCandlestickPainter(), + candlestickTouchData = candlestickTouchData ?? CandlestickTouchData(), + showingTooltipIndicators = showingTooltipIndicators ?? const [], + super( + gridData: gridData ?? const FlGridData(), + titlesData: titlesData ?? const FlTitlesData(), + clipData: clipData ?? const FlClipData.none(), + minX: minX ?? + CandlestickChartHelper.calculateMaxAxisValues( + candlestickSpots ?? const [], + ).$1, + maxX: maxX ?? + CandlestickChartHelper.calculateMaxAxisValues( + candlestickSpots ?? const [], + ).$2, + minY: minY ?? + CandlestickChartHelper.calculateMaxAxisValues( + candlestickSpots ?? const [], + ).$3, + maxY: maxY ?? + CandlestickChartHelper.calculateMaxAxisValues( + candlestickSpots ?? const [], + ).$4, + ); + + /// Contains the data for the candlestick chart. + /// + /// Each [CandlestickSpot] represents a candlestick in the chart + /// that contains [open, high, low, close] values. + final List candlestickSpots; + + /// The painter used to draw the candlestick. + /// You can use the [DefaultCandlestickPainter] or implement your own. + final FlCandlestickPainter candlestickPainter; + + /// Handles touch behaviors and responses. + final CandlestickTouchData candlestickTouchData; + + /// you can show some tooltipIndicators (a popup with an information) + /// on top of each [CandlestickSpot] using [showingTooltipIndicators], + /// just put indices you want to show it on top of them. + /// + /// An important point is that you have to disable the default touch behaviour + /// to show the tooltip manually, see [CandlestickTouchData.handleBuiltInTouches]. + final List showingTooltipIndicators; + + /// Shows an indicator on the touched / hovered point + /// + /// We manage to set it by default + /// when [candlestickTouchData.handleBuiltInTouches] is true, + /// so nothing happens if you change this property as long as + /// the handleBuiltInTouches property is true. + /// + /// But you can set [candlestickTouchData.handleBuiltInTouches] to false + /// if you want to have customized [touchedPointIndicator] + final AxisSpotIndicator? touchedPointIndicator; + + /// Lerps a [CandlestickChartData] based on [t] value, check [Tween.lerp]. + @override + CandlestickChartData lerp(BaseChartData a, BaseChartData b, double t) { + if (a is CandlestickChartData && b is CandlestickChartData) { + return CandlestickChartData( + candlestickSpots: + lerpCandleSpotList(a.candlestickSpots, b.candlestickSpots, t), + candlestickPainter: b.candlestickPainter.lerp( + a.candlestickPainter, + b.candlestickPainter, + t, + ), + titlesData: FlTitlesData.lerp(a.titlesData, b.titlesData, t), + candlestickTouchData: b.candlestickTouchData, + showingTooltipIndicators: lerpIntList( + a.showingTooltipIndicators, + b.showingTooltipIndicators, + t, + ), + gridData: FlGridData.lerp(a.gridData, b.gridData, t), + borderData: FlBorderData.lerp(a.borderData, b.borderData, t), + minX: lerpDouble(a.minX, b.minX, t), + maxX: lerpDouble(a.maxX, b.maxX, t), + baselineX: lerpDouble(a.baselineX, b.baselineX, t), + minY: lerpDouble(a.minY, b.minY, t), + maxY: lerpDouble(a.maxY, b.maxY, t), + baselineY: lerpDouble(a.baselineY, b.baselineY, t), + rangeAnnotations: RangeAnnotations.lerp( + a.rangeAnnotations, + b.rangeAnnotations, + t, + ), + clipData: b.clipData, + backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), + rotationQuarterTurns: b.rotationQuarterTurns, + touchedPointIndicator: b.touchedPointIndicator, + ); + } else { + throw Exception('Illegal State'); + } + } + + /// Copies current [CandlestickChartData] to a new [CandlestickChartData], + /// and replaces provided values. + CandlestickChartData copyWith({ + List? candlestickSpots, + FlCandlestickPainter? candlestickPainter, + FlTitlesData? titlesData, + CandlestickTouchData? candlestickTouchData, + List? showingTooltipIndicators, + FlGridData? gridData, + FlBorderData? borderData, + double? minX, + double? maxX, + double? baselineX, + double? minY, + double? maxY, + double? baselineY, + RangeAnnotations? rangeAnnotations, + FlClipData? clipData, + Color? backgroundColor, + int? rotationQuarterTurns, + AxisSpotIndicator? touchedPointIndicator, + }) => + CandlestickChartData( + candlestickSpots: candlestickSpots ?? this.candlestickSpots, + candlestickPainter: candlestickPainter ?? this.candlestickPainter, + titlesData: titlesData ?? this.titlesData, + candlestickTouchData: candlestickTouchData ?? this.candlestickTouchData, + showingTooltipIndicators: + showingTooltipIndicators ?? this.showingTooltipIndicators, + gridData: gridData ?? this.gridData, + borderData: borderData ?? this.borderData, + minX: minX ?? this.minX, + maxX: maxX ?? this.maxX, + baselineX: baselineX ?? this.baselineX, + minY: minY ?? this.minY, + maxY: maxY ?? this.maxY, + baselineY: baselineY ?? this.baselineY, + rangeAnnotations: rangeAnnotations ?? this.rangeAnnotations, + clipData: clipData ?? this.clipData, + backgroundColor: backgroundColor ?? this.backgroundColor, + rotationQuarterTurns: rotationQuarterTurns ?? this.rotationQuarterTurns, + touchedPointIndicator: + touchedPointIndicator ?? this.touchedPointIndicator, + ); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + candlestickSpots, + candlestickPainter, + candlestickTouchData, + showingTooltipIndicators, + gridData, + titlesData, + minX, + maxX, + baselineX, + minY, + maxY, + baselineY, + rangeAnnotations, + clipData, + backgroundColor, + borderData, + rotationQuarterTurns, + touchedPointIndicator, + ]; +} + +/// Defines information about a spot in the [CandlestickChart] +class CandlestickSpot extends FlSpot with EquatableMixin { + /// You can change [show] value to show or hide the spot, + /// [x] determines the location of [CandlestickChart] in the x-axis, + /// [open], [high], [low], and [close] defines the values of the spot + /// based on the standard [OHLC chart](https://en.wikipedia.org/wiki/Open-high-low-close_chart). + /// + /// You can temporarily hide the spot by setting [show] to false. + /// + /// The [candlestickPainter] is used to customize the appearance of each candlestick. + /// We use the [DefaultCandlestickPainter] by default, but you can implement + /// your own painter with your shiny UI + CandlestickSpot({ + required double x, + required this.open, + required this.high, + required this.low, + required this.close, + bool? show, + }) : show = show ?? true, + super(x, high); + + /// The open value of a specific candlestick. + final double open; + + /// The high value of a specific candlestick. + final double high; + + /// The low value of a specific candlestick. + final double low; + + /// The close value of a specific candlestick. + final double close; + + /// Determines show or hide the spot. + final bool show; + + /// Checks if the candlestick is up (close > open). + /// + /// It is the same as bullish and bearish definitions in the stock market. + bool get isUp => close > open; + + /// Returns the middle point of the candlestick + double get midPoint => (open + close) / 2; + + @override + CandlestickSpot copyWith({ + double? x, + double? y, + FlErrorRange? xError, + FlErrorRange? yError, + double? open, + double? high, + double? low, + double? close, + bool? show, + }) { + if (y != null) { + throw Exception( + 'y value is not used in CandlestickSpot and it does not do anything. Please use open, high, low, close values.', + ); + } + + if (xError != null || yError != null) { + throw Exception( + 'xError and yError values are not used in CandlestickSpot and it does not do anything.', + ); + } + + return CandlestickSpot( + x: x ?? this.x, + open: open ?? this.open, + high: high ?? this.high, + low: low ?? this.low, + close: close ?? this.close, + show: show ?? this.show, + ); + } + + /// Lerps a [CandlestickSpot] based on [t] value, check [Tween.lerp]. + static CandlestickSpot lerp(CandlestickSpot a, CandlestickSpot b, double t) => + CandlestickSpot( + x: lerpDouble(a.x, b.x, t)!, + open: lerpDouble(a.open, b.open, t)!, + high: lerpDouble(a.high, b.high, t)!, + low: lerpDouble(a.low, b.low, t)!, + close: lerpDouble(a.close, b.close, t)!, + show: b.show, + ); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + x, + open, + high, + low, + close, + show, + ]; +} + +/// Holds data to handle touch events, and touch responses in the [CandlestickChart]. +/// +/// There is a touch flow, explained [here](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/handle_touches.md) +/// in a simple way, each chart's renderer captures the touch events, and passes the pointerEvent +/// to the painter, and gets touched spot, and wraps it into a concrete [CandlestickTouchResponse]. +class CandlestickTouchData extends FlTouchData + with EquatableMixin { + /// You can disable or enable the touch system using [enabled] flag, + /// + /// [touchCallback] notifies you about the happened touch/pointer events. + /// It gives you a [FlTouchEvent] which is the happened event such as [FlPointerHoverEvent], [FlTapUpEvent], ... + /// It also gives you a [CandlestickTouchResponse] which contains information + /// about the elements that has touched. + /// + /// Using [mouseCursorResolver] you can change the mouse cursor + /// based on the provided [FlTouchEvent] and [CandlestickTouchResponse] + /// + /// if [handleBuiltInTouches] is true, [CandlestickChart] shows a tooltip popup on top of the spots if + /// touch occurs (or you can show it manually using, [CandlestickChartData.showingTooltipIndicators]) + /// You can customize this tooltip using [touchTooltipData], + /// + /// If you need to have a distance threshold for handling touches, use [touchSpotThreshold]. + CandlestickTouchData({ + bool? enabled, + BaseTouchCallback? touchCallback, + MouseCursorResolver? mouseCursorResolver, + Duration? longPressDuration, + CandlestickTouchTooltipData? touchTooltipData, + bool? handleBuiltInTouches, + double? touchSpotThreshold, + }) : touchTooltipData = touchTooltipData ?? CandlestickTouchTooltipData(), + handleBuiltInTouches = handleBuiltInTouches ?? true, + touchSpotThreshold = touchSpotThreshold ?? 4, + super( + enabled ?? true, + touchCallback, + mouseCursorResolver, + longPressDuration, + ); + + /// show a tooltip on touched spots + final CandlestickTouchTooltipData touchTooltipData; + + /// set this true if you want the built in touch handling + /// (show a tooltip bubble and an indicator on touched spots) + final bool handleBuiltInTouches; + + /// we find the nearest spots on touched position based on this threshold + final double touchSpotThreshold; + + /// Copies current [CandlestickTouchData] to a new [CandlestickTouchData], + /// and replaces provided values. + CandlestickTouchData copyWith({ + bool? enabled, + BaseTouchCallback? touchCallback, + MouseCursorResolver? mouseCursorResolver, + Duration? longPressDuration, + CandlestickTouchTooltipData? touchTooltipData, + bool? handleBuiltInTouches, + double? touchSpotThreshold, + }) => + CandlestickTouchData( + enabled: enabled ?? this.enabled, + touchCallback: touchCallback ?? this.touchCallback, + mouseCursorResolver: mouseCursorResolver ?? this.mouseCursorResolver, + longPressDuration: longPressDuration ?? this.longPressDuration, + touchTooltipData: touchTooltipData ?? this.touchTooltipData, + handleBuiltInTouches: handleBuiltInTouches ?? this.handleBuiltInTouches, + touchSpotThreshold: touchSpotThreshold ?? this.touchSpotThreshold, + ); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + enabled, + touchCallback, + mouseCursorResolver, + longPressDuration, + touchTooltipData, + handleBuiltInTouches, + touchSpotThreshold, + ]; +} + +/// [CandlestickChart]'s touch callback. +typedef CandlestickTouchCallback = void Function(CandlestickTouchResponse); + +/// Holds information about touch response in the [CandlestickChart]. +/// +/// You can override [CandlestickTouchData.touchCallback] to handle touch events, +/// it gives you a [CandlestickTouchResponse] and you can do whatever you want. +class CandlestickTouchResponse extends AxisBaseTouchResponse { + /// If touch happens, [CandlestickChart] processes it internally and + /// passes out a [CandlestickTouchResponse], it gives you information about the touched spot. + /// + /// [touchedSpot] tells you + /// in which spot (of [CandlestickChartData.candleSpots]) touch happened. + CandlestickTouchResponse({ + required super.touchLocation, + required super.touchChartCoordinate, + required this.touchedSpot, + }); + + final CandlestickTouchedSpot? touchedSpot; + + /// Copies current [CandlestickTouchResponse] to a new [CandlestickTouchResponse], + /// and replaces provided values. + CandlestickTouchResponse copyWith({ + Offset? touchLocation, + Offset? touchChartCoordinate, + CandlestickTouchedSpot? touchedSpot, + }) => + CandlestickTouchResponse( + touchLocation: touchLocation ?? this.touchLocation, + touchChartCoordinate: touchChartCoordinate ?? this.touchChartCoordinate, + touchedSpot: touchedSpot ?? this.touchedSpot, + ); +} + +/// Holds the touched spot data +class CandlestickTouchedSpot with EquatableMixin { + /// [spot], and [spotIndex] tells you + /// in which spot (of [CandlestickChartData.candleSpots]) touch happened. + const CandlestickTouchedSpot(this.spot, this.spotIndex); + + /// Touch happened on this spot + final CandlestickSpot spot; + + /// Touch happened on this spot index + final int spotIndex; + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + spot, + spotIndex, + ]; + + /// Copies current [CandlestickTouchedSpot] to a new [CandlestickTouchedSpot], + /// and replaces provided values. + CandlestickTouchedSpot copyWith({ + CandlestickSpot? spot, + int? spotIndex, + }) => + CandlestickTouchedSpot(spot ?? this.spot, spotIndex ?? this.spotIndex); +} + +/// Holds representation data for showing tooltip popup on top of spots. +class CandlestickTouchTooltipData with EquatableMixin { + /// if [CandlestickTouchData.handleBuiltInTouches] is true, + /// [CandlestickChart] shows a tooltip popup on top of spots automatically when touch happens, + /// otherwise you can show it manually using [CandlestickChartData.showingTooltipIndicators]. + /// Tooltip shows on top of rods, with [getTooltipColor] as a background color. + /// You can set the corner radius using [tooltipBorderRadius]. + /// If you want to have a padding inside the tooltip, fill [tooltipPadding]. + /// Content of the tooltip will provide using [getTooltipItems] callback, you can override it + /// and pass your custom data to show in the tooltip. + /// You can restrict the tooltip's width using [maxContentWidth]. + /// Sometimes, [CandlestickChart] shows the tooltip outside of the chart, + /// you can set [fitInsideHorizontally] true to force it to shift inside the chart horizontally, + /// also you can set [fitInsideVertically] true to force it to shift inside the chart vertically. + CandlestickTouchTooltipData({ + BorderRadius? tooltipBorderRadius, + EdgeInsets? tooltipPadding, + FLHorizontalAlignment? tooltipHorizontalAlignment, + double? tooltipHorizontalOffset, + double? maxContentWidth, + GetCandlestickTooltipItems? getTooltipItems, + bool? fitInsideHorizontally, + bool? fitInsideVertically, + bool? showOnTopOfTheChartBoxArea, + double? rotateAngle, + BorderSide? tooltipBorder, + GetCandlestickTooltipColor? getTooltipColor, + }) : tooltipBorderRadius = tooltipBorderRadius ?? BorderRadius.circular(4), + tooltipPadding = tooltipPadding ?? + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + tooltipHorizontalAlignment = + tooltipHorizontalAlignment ?? FLHorizontalAlignment.center, + tooltipHorizontalOffset = tooltipHorizontalOffset ?? 0, + maxContentWidth = maxContentWidth ?? 120, + getTooltipItems = getTooltipItems ?? defaultCandlestickTooltipItem, + fitInsideHorizontally = fitInsideHorizontally ?? false, + fitInsideVertically = fitInsideVertically ?? false, + showOnTopOfTheChartBoxArea = showOnTopOfTheChartBoxArea ?? false, + rotateAngle = rotateAngle ?? 0.0, + tooltipBorder = tooltipBorder ?? BorderSide.none, + getTooltipColor = getTooltipColor ?? defaultCandlestickTooltipColor, + super(); + + /// Sets a border radius for the tooltip. + final BorderRadius tooltipBorderRadius; + + /// Applies a padding for showing contents inside the tooltip. + final EdgeInsets tooltipPadding; + + /// Controls showing tooltip on left side, right side or center aligned with spot, default is center + final FLHorizontalAlignment tooltipHorizontalAlignment; + + /// Applies horizontal offset for showing tooltip, default is zero. + final double tooltipHorizontalOffset; + + /// Restricts the tooltip's width. + final double maxContentWidth; + + /// Retrieves data for showing content inside the tooltip. + final GetCandlestickTooltipItems getTooltipItems; + + /// Forces the tooltip to shift horizontally inside the chart, if overflow happens. + final bool fitInsideHorizontally; + + /// Forces the tooltip to shift vertically inside the chart, if overflow happens. + final bool fitInsideVertically; + + /// Forces the tooltip container to top of the line, default 'false' + final bool showOnTopOfTheChartBoxArea; + + /// Controls the rotation of the tooltip. + final double rotateAngle; + + /// The tooltip border color. + final BorderSide tooltipBorder; + + /// Retrieves data for showing content inside the tooltip. + final GetCandlestickTooltipColor getTooltipColor; + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + tooltipBorderRadius, + tooltipPadding, + tooltipHorizontalAlignment, + tooltipHorizontalOffset, + maxContentWidth, + getTooltipItems, + fitInsideHorizontally, + fitInsideVertically, + showOnTopOfTheChartBoxArea, + rotateAngle, + tooltipBorder, + getTooltipColor, + ]; + + /// Copies current [CandlestickTouchTooltipData] to a new [CandlestickTouchTooltipData], + /// and replaces provided values. + CandlestickTouchTooltipData copyWith({ + BorderRadius? tooltipBorderRadius, + EdgeInsets? tooltipPadding, + FLHorizontalAlignment? tooltipHorizontalAlignment, + double? tooltipHorizontalOffset, + double? maxContentWidth, + GetCandlestickTooltipItems? getTooltipItems, + bool? fitInsideHorizontally, + bool? fitInsideVertically, + double? rotateAngle, + BorderSide? tooltipBorder, + GetCandlestickTooltipColor? getTooltipColor, + }) => + CandlestickTouchTooltipData( + tooltipBorderRadius: tooltipBorderRadius ?? this.tooltipBorderRadius, + tooltipPadding: tooltipPadding ?? this.tooltipPadding, + tooltipHorizontalAlignment: + tooltipHorizontalAlignment ?? this.tooltipHorizontalAlignment, + tooltipHorizontalOffset: + tooltipHorizontalOffset ?? this.tooltipHorizontalOffset, + maxContentWidth: maxContentWidth ?? this.maxContentWidth, + getTooltipItems: getTooltipItems ?? this.getTooltipItems, + fitInsideHorizontally: + fitInsideHorizontally ?? this.fitInsideHorizontally, + fitInsideVertically: fitInsideVertically ?? this.fitInsideVertically, + rotateAngle: rotateAngle ?? this.rotateAngle, + tooltipBorder: tooltipBorder ?? this.tooltipBorder, + getTooltipColor: getTooltipColor ?? this.getTooltipColor, + ); +} + +/// Provides a [CandlestickTooltipItem] for showing content inside the [CandlestickTouchTooltipData]. +/// +/// You can override [CandlestickTouchTooltipData.getTooltipItems], it gives you +/// [touchedSpot] that touch happened on, +/// then you should and pass your custom [CandlestickTooltipItem] +/// to show it inside the tooltip popup. +typedef GetCandlestickTooltipItems = CandlestickTooltipItem? Function( + FlCandlestickPainter painter, + CandlestickSpot touchedSpot, + int spotIndex, +); + +/// Default implementation for [CandlestickTouchTooltipData.getTooltipItems]. +CandlestickTooltipItem? defaultCandlestickTooltipItem( + FlCandlestickPainter painter, + CandlestickSpot touchedSpot, + int spotIndex, +) { + final textStyle = TextStyle( + color: painter.getMainColor( + spot: touchedSpot, + spotIndex: spotIndex, + ), + fontSize: 14, + ); + final valueStyle = TextStyle( + color: painter.getMainColor( + spot: touchedSpot, + spotIndex: spotIndex, + ), + fontWeight: FontWeight.bold, + fontSize: 14, + ); + return CandlestickTooltipItem( + '', + textStyle: textStyle, + children: [ + TextSpan( + text: 'open: ', + style: textStyle, + ), + TextSpan( + text: '${touchedSpot.open.toInt()}\n', + style: valueStyle, + ), + TextSpan( + text: 'high: ', + style: textStyle, + ), + TextSpan( + text: '${touchedSpot.high.toInt()}\n', + style: valueStyle, + ), + TextSpan( + text: 'low: ', + style: textStyle, + ), + TextSpan( + text: '${touchedSpot.low.toInt()}\n', + style: valueStyle, + ), + TextSpan( + text: 'close: ', + style: textStyle, + ), + TextSpan( + text: '${touchedSpot.close.toInt()}', + style: valueStyle, + ), + ], + ); +} + +/// Provides a [Color] to show different background color inside the [CandlestickTouchTooltipData]. +/// +/// You can override [CandlestickTouchTooltipData.getTooltipColor], it gives you +/// [touchedSpot] that touch happened on, +/// then you should and pass your custom [Color] +/// to show it inside the tooltip popup. +typedef GetCandlestickTooltipColor = Color Function( + CandlestickSpot touchedSpot, +); + +/// Default implementation for [CandlestickTouchTooltipData.getTooltipItems]. +Color defaultCandlestickTooltipColor(CandlestickSpot touchedSpot) => + Colors.blueGrey.darken(80); + +/// Holds data of showing each item in the tooltip popup. +class CandlestickTooltipItem with EquatableMixin { + /// Shows a [text] with [textStyle], [textDirection], and optional [children] in the tooltip popup, + /// [bottomMargin] is the bottom space from spot. + CandlestickTooltipItem( + this.text, { + this.textStyle, + double? bottomMargin, + TextAlign? textAlign, + TextDirection? textDirection, + this.children, + }) : bottomMargin = bottomMargin ?? 8, + textAlign = textAlign ?? TextAlign.center, + textDirection = textDirection ?? TextDirection.ltr; + + /// Showing text. + final String text; + + /// Style of showing text. + final TextStyle? textStyle; + + /// Defines bottom space from spot. + final double bottomMargin; + + /// TextAlign of the showing content. + final TextAlign textAlign; + + /// Direction of showing text. + final TextDirection textDirection; + + /// Add further style and format to the text of the tooltip + final List? children; + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + text, + textStyle, + bottomMargin, + textAlign, + textDirection, + children, + ]; + + /// Copies current [CandlestickTooltipItem] to a new [CandlestickTooltipItem], + /// and replaces provided values. + CandlestickTooltipItem copyWith({ + String? text, + TextStyle? textStyle, + double? bottomMargin, + TextAlign? textAlign, + TextDirection? textDirection, + List? children, + }) => + CandlestickTooltipItem( + text ?? this.text, + textStyle: textStyle ?? this.textStyle, + bottomMargin: bottomMargin ?? this.bottomMargin, + textAlign: textAlign ?? this.textAlign, + textDirection: textDirection ?? this.textDirection, + children: children ?? this.children, + ); +} + +/// This class contains the interface for drawing the candlestick shape. +abstract class FlCandlestickPainter with EquatableMixin { + const FlCandlestickPainter(); + + /// This method should be overridden to draw the candlestick shape + void paint( + Canvas canvas, + ValueInCanvasProvider xInCanvasProvider, + ValueInCanvasProvider yInCanvasProvider, + CandlestickSpot spot, + int spotIndex, + ); + + /// Used to show default UIs, for example [defaultCandlestickTooltipItem] + Color getMainColor({ + required CandlestickSpot spot, + required int spotIndex, + }); + + FlCandlestickPainter lerp( + FlCandlestickPainter a, + FlCandlestickPainter b, + double t, + ); + + /// Used to implement touch behaviour of this dot, for example, + /// it behaves like a square of [getSize] + /// Check [DefaultCandlestickPainter.hitTest] for an example of an implementation + (bool, double) hitTest( + CandlestickSpot spot, + double touchedX, + double spotX, + double extraTouchThreshold, + ) { + final distance = (touchedX - spotX).abs(); + final hit = distance <= extraTouchThreshold; + return (hit, distance); + } +} + +/// [CandlestickChart]'s touch callback. +typedef CandlestickStyleProvider = CandlestickStyle Function( + CandlestickSpot spot, + int index, +); + +CandlestickStyleProvider get _defaultStrokeColorProvider => (spot, _) { + final generalColor = + spot.isUp ? const Color(0xFF4CAF50) : const Color(0xFFEF5350); + return CandlestickStyle( + lineColor: generalColor, + lineWidth: 1.5, + bodyStrokeColor: generalColor, + bodyStrokeWidth: 0, + bodyFillColor: generalColor, + bodyWidth: 4, + bodyRadius: 0, + ); + }; + +/// Default implementation of [FlCandlestickPainter]. +/// +/// It draws the candlestick shape with a line and a body (just like a standard +/// candlestick chart). +/// +/// You can customize the appearance of the candlestick +/// using [CandlestickStyleProvider]. +class DefaultCandlestickPainter extends FlCandlestickPainter { + DefaultCandlestickPainter({ + CandlestickStyleProvider? candlestickStyleProvider, + }) : candlestickStyleProvider = + candlestickStyleProvider ?? _defaultStrokeColorProvider; + + final CandlestickStyleProvider candlestickStyleProvider; + + final _linePainter = Paint(); + final _bodyPainter = Paint(); + final _bodyStrokePainter = Paint(); + + @override + void paint( + Canvas canvas, + ValueInCanvasProvider xInCanvasProvider, + ValueInCanvasProvider yInCanvasProvider, + CandlestickSpot spot, + int spotIndex, + ) { + final style = candlestickStyleProvider(spot, spotIndex); + + final xOffsetInCanvas = xInCanvasProvider(spot.x); + final openYOffsetInCanvas = yInCanvasProvider(spot.open); + final highYOffsetInCanvas = yInCanvasProvider(spot.high); + final lowOYOffsetInCanvas = yInCanvasProvider(spot.low); + final closeYOffsetInCanvas = yInCanvasProvider(spot.close); + + final bodyHighCanvas = min(openYOffsetInCanvas, closeYOffsetInCanvas); + final bodyLowCanvas = max(openYOffsetInCanvas, closeYOffsetInCanvas); + + if (style.lineWidth > 0 && style.lineColor.a > 0) { + canvas + // Bottom line + ..drawLine( + Offset(xOffsetInCanvas, lowOYOffsetInCanvas), + Offset(xOffsetInCanvas, bodyLowCanvas), + _linePainter + ..color = style.lineColor + ..strokeWidth = style.lineWidth, + ) + // Top line + ..drawLine( + Offset(xOffsetInCanvas, highYOffsetInCanvas), + Offset(xOffsetInCanvas, bodyHighCanvas), + _linePainter + ..color = style.lineColor + ..strokeWidth = style.lineWidth, + ); + } + + // Body + final bodyRect = Rect.fromLTRB( + xOffsetInCanvas - style.bodyWidth / 2, + bodyHighCanvas, + xOffsetInCanvas + style.bodyWidth / 2, + bodyLowCanvas, + ); + if (style.bodyFillColor.a > 0 && style.bodyWidth > 0) { + canvas.drawRRect( + RRect.fromRectAndRadius( + bodyRect, + Radius.circular(style.bodyRadius), + ), + _bodyPainter + ..color = style.bodyFillColor + ..style = PaintingStyle.fill, + ); + } + if (style.bodyStrokeWidth > 0 && style.bodyStrokeColor.a > 0) { + canvas.drawRRect( + RRect.fromRectAndRadius( + bodyRect, + Radius.circular(style.bodyRadius), + ), + _bodyStrokePainter + ..color = style.bodyStrokeColor + ..strokeWidth = style.bodyStrokeWidth + ..style = PaintingStyle.stroke, + ); + } + } + + @override + FlCandlestickPainter lerp( + FlCandlestickPainter a, + FlCandlestickPainter b, + double t, + ) { + if (a is! DefaultCandlestickPainter || b is! DefaultCandlestickPainter) { + return b; + } + return DefaultCandlestickPainter( + candlestickStyleProvider: b.candlestickStyleProvider, + ); + } + + @override + Color getMainColor({ + required CandlestickSpot spot, + required int spotIndex, + }) => + candlestickStyleProvider(spot, spotIndex).lineColor; + + @override + List get props => [ + candlestickStyleProvider, + ]; +} + +/// Holds data for drawing each candlestick shape. +class CandlestickStyle with EquatableMixin { + const CandlestickStyle({ + required this.lineColor, + required this.lineWidth, + required this.bodyStrokeColor, + required this.bodyStrokeWidth, + required this.bodyFillColor, + required this.bodyWidth, + required this.bodyRadius, + }); + + /// The color of the candlestick line. + final Color lineColor; + + /// The width of the candlestick line. + final double lineWidth; + + /// The color of the candlestick body stroke. + final Color bodyStrokeColor; + + /// The width of the candlestick body stroke. + final double bodyStrokeWidth; + + /// The fill color of the candlestick body. + final Color bodyFillColor; + + /// The width of the candlestick body. + final double bodyWidth; + + /// The radius of the corners of the candlestick body. + final double bodyRadius; + + /// Lerps a [CandlestickStyle] based on [t] value, check [Tween.lerp]. + static CandlestickStyle lerp( + CandlestickStyle a, + CandlestickStyle b, + double t, + ) => + CandlestickStyle( + lineColor: Color.lerp(a.lineColor, b.lineColor, t)!, + lineWidth: lerpDouble(a.lineWidth, b.lineWidth, t)!, + bodyStrokeColor: Color.lerp(a.bodyStrokeColor, b.bodyStrokeColor, t)!, + bodyStrokeWidth: lerpDouble(a.bodyStrokeWidth, b.bodyStrokeWidth, t)!, + bodyFillColor: Color.lerp(a.bodyFillColor, b.bodyFillColor, t)!, + bodyWidth: lerpDouble(a.bodyWidth, b.bodyWidth, t)!, + bodyRadius: lerpDouble(a.bodyRadius, b.bodyRadius, t)!, + ); + + @override + List get props => [ + lineColor, + lineWidth, + bodyStrokeColor, + bodyStrokeWidth, + bodyFillColor, + bodyWidth, + bodyRadius, + ]; +} + +/// It lerps a [CandlestickChartData] to another [CandlestickChartData] (handles animation for updating values) +class CandlestickChartDataTween extends Tween { + CandlestickChartDataTween({ + required CandlestickChartData begin, + required CandlestickChartData end, + }) : super(begin: begin, end: end); + + /// Lerps a [CandlestickChartData] based on [t] value, check [Tween.lerp]. + @override + CandlestickChartData lerp(double t) => begin!.lerp(begin!, end!, t); +} diff --git a/lib/src/chart/candlestick_chart/candlestick_chart_helper.dart b/lib/src/chart/candlestick_chart/candlestick_chart_helper.dart new file mode 100644 index 0000000..0f49f88 --- /dev/null +++ b/lib/src/chart/candlestick_chart/candlestick_chart_helper.dart @@ -0,0 +1,43 @@ +import 'package:fl_chart/src/chart/candlestick_chart/candlestick_chart_data.dart'; + +/// Contains anything that helps CandlestickChart works +class CandlestickChartHelper { + /// Calculates minX, maxX, minY, and maxY based on [candleSpots], + /// returns cached values, to prevent redundant calculations. + static ( + double minX, + double maxX, + double minY, + double maxY, + ) calculateMaxAxisValues( + List candleSpots, + ) { + if (candleSpots.isEmpty) { + return (0, 0, 0, 0); + } + + var minX = candleSpots[0].x; + var maxX = candleSpots[0].x; + var minY = candleSpots[0].low; + var maxY = candleSpots[0].high; + for (var j = 0; j < candleSpots.length; j++) { + final spot = candleSpots[j]; + if (spot.x > maxX) { + maxX = spot.x; + } + + if (spot.x < minX) { + minX = spot.x; + } + + if (spot.high > maxY) { + maxY = spot.high; + } + + if (spot.low < minY) { + minY = spot.low; + } + } + return (minX, maxX, minY, maxY); + } +} diff --git a/lib/src/chart/candlestick_chart/candlestick_chart_painter.dart b/lib/src/chart/candlestick_chart/candlestick_chart_painter.dart new file mode 100644 index 0000000..109e4a8 --- /dev/null +++ b/lib/src/chart/candlestick_chart/candlestick_chart_painter.dart @@ -0,0 +1,381 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/base/axis_chart/axis_chart_painter.dart'; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart'; +import 'package:fl_chart/src/utils/canvas_wrapper.dart'; +import 'package:fl_chart/src/utils/utils.dart'; +import 'package:flutter/material.dart'; + +/// Paints [CandlestickChartData] in the canvas, it can be used in a [CustomPainter] +class CandlestickChartPainter extends AxisChartPainter { + /// Paints [CandlestickChartData] in the canvas + CandlestickChartPainter() : super() { + _bgTouchTooltipPaint = Paint() + ..style = PaintingStyle.fill + ..color = Colors.white; + + _borderTouchTooltipPaint = Paint() + ..style = PaintingStyle.stroke + ..color = Colors.transparent + ..strokeWidth = 1.0; + + _clipPaint = Paint(); + } + + late Paint _bgTouchTooltipPaint; + late Paint _borderTouchTooltipPaint; + late Paint _clipPaint; + + /// Paints [CandlestickChartData] into the provided canvas. + @override + void paint( + BuildContext context, + CanvasWrapper canvasWrapper, + PaintHolder holder, + ) { + if (holder.chartVirtualRect != null) { + canvasWrapper + ..saveLayer( + Offset.zero & canvasWrapper.size, + _clipPaint, + ) + ..clipRect(Offset.zero & canvasWrapper.size); + } + super.paint(context, canvasWrapper, holder); + drawAxisSpotIndicator(context, canvasWrapper, holder); + drawCandlesticks(context, canvasWrapper, holder); + + if (holder.chartVirtualRect != null) { + canvasWrapper.restore(); + } + + drawTouchTooltips(context, canvasWrapper, holder); + } + + @visibleForTesting + void drawCandlesticks( + BuildContext context, + CanvasWrapper canvasWrapper, + PaintHolder holder, + ) { + final data = holder.data; + final viewSize = canvasWrapper.size; + final clip = data.clipData; + final border = data.borderData.show ? data.borderData.border : null; + + if (data.clipData.any) { + canvasWrapper.saveLayer( + Rect.fromLTRB( + 0, + 0, + canvasWrapper.size.width, + canvasWrapper.size.height, + ), + _clipPaint, + ); + + var left = 0.0; + var top = 0.0; + var right = viewSize.width; + var bottom = viewSize.height; + + if (clip.left) { + final borderWidth = border?.left.width ?? 0; + left = borderWidth / 2; + } + if (clip.top) { + final borderWidth = border?.top.width ?? 0; + top = borderWidth / 2; + } + if (clip.right) { + final borderWidth = border?.right.width ?? 0; + right = viewSize.width - (borderWidth / 2); + } + if (clip.bottom) { + final borderWidth = border?.bottom.width ?? 0; + bottom = viewSize.height - (borderWidth / 2); + } + + canvasWrapper.clipRect(Rect.fromLTRB(left, top, right, bottom)); + } + + for (var i = 0; i < data.candlestickSpots.length; i++) { + final candlestickSpot = data.candlestickSpots[i]; + + if (!candlestickSpot.show) { + continue; + } + holder.data.candlestickPainter.paint( + canvasWrapper.canvas, + (x) => getPixelX(x, viewSize, holder), + (y) => getPixelY(y, viewSize, holder), + candlestickSpot, + i, + ); + } + + if (data.clipData.any) { + canvasWrapper.restore(); + } + } + + @visibleForTesting + void drawTouchTooltips( + BuildContext context, + CanvasWrapper canvasWrapper, + PaintHolder holder, + ) { + final targetData = holder.targetData; + for (var i = 0; i < targetData.candlestickSpots.length; i++) { + if (!targetData.showingTooltipIndicators.contains(i)) { + continue; + } + + final candlestickSpot = targetData.candlestickSpots[i]; + drawTouchTooltip( + context, + canvasWrapper, + targetData.candlestickTouchData.touchTooltipData, + candlestickSpot, + i, + holder, + ); + } + } + + @visibleForTesting + void drawTouchTooltip( + BuildContext context, + CanvasWrapper canvasWrapper, + CandlestickTouchTooltipData tooltipData, + CandlestickSpot showOnSpot, + int spotIndex, + PaintHolder holder, + ) { + final viewSize = canvasWrapper.size; + + final tooltipItem = tooltipData.getTooltipItems( + holder.data.candlestickPainter, + showOnSpot, + spotIndex, + ); + + if (tooltipItem == null) { + return; + } + + final span = TextSpan( + style: Utils().getThemeAwareTextStyle(context, tooltipItem.textStyle), + text: tooltipItem.text, + children: tooltipItem.children, + ); + + final drawingTextPainter = TextPainter( + text: span, + textAlign: tooltipItem.textAlign, + textDirection: tooltipItem.textDirection, + textScaler: holder.textScaler, + )..layout(maxWidth: tooltipData.maxContentWidth); + + final width = drawingTextPainter.width; + final height = drawingTextPainter.height; + + final tooltipOriginPoint = Offset( + getPixelX(showOnSpot.x, viewSize, holder), + getPixelY( + showOnSpot.high, + viewSize, + holder, + ), + ); + + final tooltipWidth = width + tooltipData.tooltipPadding.horizontal; + final tooltipHeight = height + tooltipData.tooltipPadding.vertical; + + double tooltipTopPosition; + if (tooltipData.showOnTopOfTheChartBoxArea) { + tooltipTopPosition = 0 - tooltipHeight - tooltipItem.bottomMargin; + } else { + tooltipTopPosition = + tooltipOriginPoint.dy - tooltipHeight - tooltipItem.bottomMargin; + } + + final tooltipLeftPosition = getTooltipLeft( + tooltipOriginPoint.dx, + tooltipWidth, + tooltipData.tooltipHorizontalAlignment, + tooltipData.tooltipHorizontalOffset, + ); + + /// draw the background rect with rounded radius + var rect = Rect.fromLTWH( + tooltipLeftPosition, + tooltipTopPosition, + tooltipWidth, + tooltipHeight, + ); + + if (tooltipData.fitInsideHorizontally) { + if (rect.left < 0) { + final shiftAmount = 0 - rect.left; + rect = Rect.fromLTRB( + rect.left + shiftAmount, + rect.top, + rect.right + shiftAmount, + rect.bottom, + ); + } + + if (rect.right > viewSize.width) { + final shiftAmount = rect.right - viewSize.width; + rect = Rect.fromLTRB( + rect.left - shiftAmount, + rect.top, + rect.right - shiftAmount, + rect.bottom, + ); + } + } + + if (tooltipData.fitInsideVertically) { + if (rect.top < 0) { + final shiftAmount = 0 - rect.top; + rect = Rect.fromLTRB( + rect.left, + rect.top + shiftAmount, + rect.right, + rect.bottom + shiftAmount, + ); + } + + if (rect.bottom > viewSize.height) { + final shiftAmount = rect.bottom - viewSize.height; + rect = Rect.fromLTRB( + rect.left, + rect.top - shiftAmount, + rect.right, + rect.bottom - shiftAmount, + ); + } + } + + final roundedRect = RRect.fromRectAndCorners( + rect, + topLeft: tooltipData.tooltipBorderRadius.topLeft, + topRight: tooltipData.tooltipBorderRadius.topRight, + bottomLeft: tooltipData.tooltipBorderRadius.bottomLeft, + bottomRight: tooltipData.tooltipBorderRadius.bottomRight, + ); + + _bgTouchTooltipPaint.color = tooltipData.getTooltipColor(showOnSpot); + + final rotateAngle = tooltipData.rotateAngle; + final rectRotationOffset = + Offset(0, Utils().calculateRotationOffset(rect.size, rotateAngle).dy); + final rectDrawOffset = Offset(roundedRect.left, roundedRect.top); + + final textRotationOffset = + Utils().calculateRotationOffset(drawingTextPainter.size, rotateAngle); + + final drawOffset = Offset( + rect.center.dx - (drawingTextPainter.width / 2), + rect.topCenter.dy + + tooltipData.tooltipPadding.top - + textRotationOffset.dy + + rectRotationOffset.dy, + ); + + if (tooltipData.tooltipBorder != BorderSide.none) { + _borderTouchTooltipPaint + ..color = tooltipData.tooltipBorder.color + ..strokeWidth = tooltipData.tooltipBorder.width; + } + + final reverseQuarterTurnsAngle = -holder.data.rotationQuarterTurns * 90; + canvasWrapper.drawRotated( + size: rect.size, + rotationOffset: rectRotationOffset, + drawOffset: rectDrawOffset, + angle: reverseQuarterTurnsAngle + rotateAngle, + drawCallback: () { + canvasWrapper + ..drawRRect(roundedRect, _bgTouchTooltipPaint) + ..drawRRect(roundedRect, _borderTouchTooltipPaint) + ..drawText(drawingTextPainter, drawOffset); + }, + ); + } + + @visibleForTesting + void drawAxisSpotIndicator( + BuildContext context, + CanvasWrapper canvasWrapper, + PaintHolder holder, + ) { + final pointIndicator = holder.data.touchedPointIndicator; + if (pointIndicator == null) { + return; + } + + final viewSize = canvasWrapper.size; + pointIndicator.painter.paint( + context, + canvasWrapper.canvas, + canvasWrapper.size, + pointIndicator, + (x) => getPixelX(x, viewSize, holder), + (y) => getPixelY(y, viewSize, holder), + holder.data, + ); + } + + /// Makes a [CandlestickTouchedSpot] based on the provided [localPosition] + /// + /// Processes [localPosition] and checks + /// the elements of the chart that are near the offset, + /// then makes a [CandlestickTouchedSpot] from the elements that has been touched. + /// + /// Returns null if finds nothing! + CandlestickTouchedSpot? handleTouch( + Offset localPosition, + Size viewSize, + PaintHolder holder, + ) { + final data = holder.data; + + final touchedSpots = + <({CandlestickSpot spot, int index, double distance})>[]; + for (var i = data.candlestickSpots.length - 1; i >= 0; i--) { + // Reverse the loop to check the topmost spot first + final spot = data.candlestickSpots[i]; + if (!spot.show) { + continue; + } + + final spotPixelX = getPixelX(spot.x, viewSize, holder); + + final (hit, distance) = holder.targetData.candlestickPainter.hitTest( + spot, + spotPixelX, + localPosition.dx, + holder.data.candlestickTouchData.touchSpotThreshold, + ); + if (hit) { + touchedSpots.add( + ( + spot: spot, + index: i, + distance: distance, + ), + ); + } + } + + if (touchedSpots.isEmpty) { + return null; + } + // Sort the touched spots by distance + touchedSpots.sort((a, b) => a.distance.compareTo(b.distance)); + final closestSpot = touchedSpots.first; + return CandlestickTouchedSpot(closestSpot.spot, closestSpot.index); + } +} diff --git a/lib/src/chart/candlestick_chart/candlestick_chart_renderer.dart b/lib/src/chart/candlestick_chart/candlestick_chart_renderer.dart new file mode 100644 index 0000000..9d6e97c --- /dev/null +++ b/lib/src/chart/candlestick_chart/candlestick_chart_renderer.dart @@ -0,0 +1,148 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart'; +import 'package:fl_chart/src/chart/base/base_chart/render_base_chart.dart'; +import 'package:fl_chart/src/chart/candlestick_chart/candlestick_chart_painter.dart'; +import 'package:fl_chart/src/utils/canvas_wrapper.dart'; +import 'package:flutter/cupertino.dart'; + +// coverage:ignore-start + +/// Low level ScatterChart Widget. +class CandlestickChartLeaf extends LeafRenderObjectWidget { + const CandlestickChartLeaf({ + super.key, + required this.data, + required this.targetData, + required this.chartVirtualRect, + required this.canBeScaled, + }); + + final CandlestickChartData data; + final CandlestickChartData targetData; + final Rect? chartVirtualRect; + final bool canBeScaled; + + @override + RenderCandlestickChart createRenderObject(BuildContext context) => + RenderCandlestickChart( + context, + data, + targetData, + MediaQuery.of(context).textScaler, + chartVirtualRect, + canBeScaled: canBeScaled, + ); + + @override + void updateRenderObject( + BuildContext context, + RenderCandlestickChart renderObject, + ) { + renderObject + ..data = data + ..targetData = targetData + ..textScaler = MediaQuery.of(context).textScaler + ..buildContext = context + ..chartVirtualRect = chartVirtualRect + ..canBeScaled = canBeScaled; + } +} +// coverage:ignore-end + +/// Renders our ScatterChart, also handles hitTest. +class RenderCandlestickChart extends RenderBaseChart { + RenderCandlestickChart( + BuildContext context, + CandlestickChartData data, + CandlestickChartData targetData, + TextScaler textScaler, + Rect? chartVirtualRect, { + required bool canBeScaled, + }) : _data = data, + _targetData = targetData, + _textScaler = textScaler, + _chartVirtualRect = chartVirtualRect, + super( + targetData.candlestickTouchData, + context, + canBeScaled: canBeScaled, + ); + + CandlestickChartData get data => _data; + CandlestickChartData _data; + + set data(CandlestickChartData value) { + if (_data == value) return; + _data = value; + markNeedsPaint(); + } + + CandlestickChartData get targetData => _targetData; + CandlestickChartData _targetData; + + set targetData(CandlestickChartData value) { + if (_targetData == value) return; + _targetData = value; + super.updateBaseTouchData(_targetData.candlestickTouchData); + markNeedsPaint(); + } + + TextScaler get textScaler => _textScaler; + TextScaler _textScaler; + + set textScaler(TextScaler value) { + if (_textScaler == value) return; + _textScaler = value; + markNeedsPaint(); + } + + Rect? get chartVirtualRect => _chartVirtualRect; + Rect? _chartVirtualRect; + + set chartVirtualRect(Rect? value) { + if (_chartVirtualRect == value) return; + _chartVirtualRect = value; + markNeedsPaint(); + } + + // We couldn't mock [size] property of this class, that's why we have this + @visibleForTesting + Size? mockTestSize; + + @visibleForTesting + CandlestickChartPainter painter = CandlestickChartPainter(); + + PaintHolder get paintHolder => + PaintHolder(data, targetData, textScaler, chartVirtualRect); + + @override + void paint(PaintingContext context, Offset offset) { + final canvas = context.canvas + ..save() + ..translate(offset.dx, offset.dy); + painter.paint( + buildContext, + CanvasWrapper(canvas, mockTestSize ?? size), + paintHolder, + ); + canvas.restore(); + } + + @override + CandlestickTouchResponse getResponseAtLocation(Offset localPosition) { + final chartSize = mockTestSize ?? size; + return CandlestickTouchResponse( + touchLocation: localPosition, + touchChartCoordinate: painter.getChartCoordinateFromPixel( + localPosition, + chartSize, + paintHolder, + ), + touchedSpot: painter.handleTouch( + localPosition, + chartSize, + paintHolder, + ), + ); + } +} diff --git a/lib/src/chart/line_chart/line_chart.dart b/lib/src/chart/line_chart/line_chart.dart new file mode 100644 index 0000000..5b46be0 --- /dev/null +++ b/lib/src/chart/line_chart/line_chart.dart @@ -0,0 +1,171 @@ +import 'package:fl_chart/src/chart/base/axis_chart/axis_chart_scaffold_widget.dart'; +import 'package:fl_chart/src/chart/base/axis_chart/scale_axis.dart'; +import 'package:fl_chart/src/chart/base/axis_chart/transformation_config.dart'; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_data.dart'; +import 'package:fl_chart/src/chart/base/base_chart/fl_touch_event.dart'; +import 'package:fl_chart/src/chart/line_chart/line_chart_data.dart'; +import 'package:fl_chart/src/chart/line_chart/line_chart_helper.dart'; +import 'package:fl_chart/src/chart/line_chart/line_chart_renderer.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +/// Renders a line chart as a widget, using provided [LineChartData]. +class LineChart extends ImplicitlyAnimatedWidget { + /// [data] determines how the [LineChart] should be look like, + /// when you make any change in the [LineChartData], it updates + /// new values with animation, and duration is [duration]. + /// also you can change the [curve] + /// which default is [Curves.linear]. + const LineChart( + this.data, { + this.chartRendererKey, + super.key, + super.duration = const Duration(milliseconds: 150), + super.curve = Curves.linear, + this.transformationConfig = const FlTransformationConfig(), + }); + + /// Determines how the [LineChart] should be look like. + final LineChartData data; + + /// {@macro fl_chart.AxisChartScaffoldWidget.transformationConfig} + final FlTransformationConfig transformationConfig; + + /// We pass this key to our renderers which are supposed to + /// render the chart itself (without anything around the chart). + final Key? chartRendererKey; + + /// Creates a [_LineChartState] + @override + _LineChartState createState() => _LineChartState(); +} + +class _LineChartState extends AnimatedWidgetBaseState { + /// we handle under the hood animations (implicit animations) via this tween, + /// it lerps between the old [LineChartData] to the new one. + LineChartDataTween? _lineChartDataTween; + + /// If [LineTouchData.handleBuiltInTouches] is true, we override the callback to handle touches internally, + /// but we need to keep the provided callback to notify it too. + BaseTouchCallback? _providedTouchCallback; + + final List _showingTouchedTooltips = []; + + final Map> _showingTouchedIndicators = {}; + + final _lineChartHelper = LineChartHelper(); + + @override + Widget build(BuildContext context) { + final showingData = _getData(); + + return AxisChartScaffoldWidget( + transformationConfig: widget.transformationConfig, + chartBuilder: (context, chartVirtualRect) => LineChartLeaf( + data: _withTouchedIndicators( + _lineChartDataTween!.evaluate(animation), + ), + targetData: _withTouchedIndicators(showingData), + key: widget.chartRendererKey, + chartVirtualRect: chartVirtualRect, + canBeScaled: widget.transformationConfig.scaleAxis != FlScaleAxis.none, + ), + data: showingData, + ); + } + + LineChartData _withTouchedIndicators(LineChartData lineChartData) { + if (!lineChartData.lineTouchData.enabled || + !lineChartData.lineTouchData.handleBuiltInTouches) { + return lineChartData; + } + + return lineChartData.copyWith( + showingTooltipIndicators: _showingTouchedTooltips, + lineBarsData: lineChartData.lineBarsData.map((barData) { + final index = lineChartData.lineBarsData.indexOf(barData); + return barData.copyWith( + showingIndicators: _showingTouchedIndicators[index] ?? [], + ); + }).toList(), + ); + } + + LineChartData _getData() { + var newData = widget.data; + + /// Calculate minX, maxX, minY, maxY for [LineChartData] if they are null, + /// it is necessary to render the chart correctly. + if (newData.minX.isNaN || + newData.maxX.isNaN || + newData.minY.isNaN || + newData.maxY.isNaN) { + final (minX, maxX, minY, maxY) = _lineChartHelper.calculateMaxAxisValues( + newData.lineBarsData, + ); + newData = newData.copyWith( + minX: newData.minX.isNaN ? minX : newData.minX, + maxX: newData.maxX.isNaN ? maxX : newData.maxX, + minY: newData.minY.isNaN ? minY : newData.minY, + maxY: newData.maxY.isNaN ? maxY : newData.maxY, + ); + } + + final lineTouchData = newData.lineTouchData; + if (lineTouchData.enabled && lineTouchData.handleBuiltInTouches) { + _providedTouchCallback = lineTouchData.touchCallback; + newData = newData.copyWith( + lineTouchData: + newData.lineTouchData.copyWith(touchCallback: _handleBuiltInTouch), + ); + } + + return newData; + } + + void _handleBuiltInTouch( + FlTouchEvent event, + LineTouchResponse? touchResponse, + ) { + if (!mounted) { + return; + } + _providedTouchCallback?.call(event, touchResponse); + + if (!event.isInterestedForInteractions || + touchResponse?.lineBarSpots == null || + touchResponse!.lineBarSpots!.isEmpty) { + setState(() { + _showingTouchedTooltips.clear(); + _showingTouchedIndicators.clear(); + }); + return; + } + + setState(() { + final sortedLineSpots = List.of(touchResponse.lineBarSpots!) + ..sort((spot1, spot2) => spot2.y.compareTo(spot1.y)); + + _showingTouchedIndicators.clear(); + for (var i = 0; i < touchResponse.lineBarSpots!.length; i++) { + final touchedBarSpot = touchResponse.lineBarSpots![i]; + final barPos = touchedBarSpot.barIndex; + _showingTouchedIndicators[barPos] = [touchedBarSpot.spotIndex]; + } + + _showingTouchedTooltips + ..clear() + ..add(ShowingTooltipIndicators(sortedLineSpots)); + }); + } + + @override + void forEachTween(TweenVisitor visitor) { + _lineChartDataTween = visitor( + _lineChartDataTween, + _getData(), + (dynamic value) => + LineChartDataTween(begin: value as LineChartData, end: widget.data), + ) as LineChartDataTween?; + } +} diff --git a/lib/src/chart/line_chart/line_chart_data.dart b/lib/src/chart/line_chart/line_chart_data.dart new file mode 100644 index 0000000..22ee479 --- /dev/null +++ b/lib/src/chart/line_chart/line_chart_data.dart @@ -0,0 +1,1374 @@ +// coverage:ignore-file +import 'dart:ui'; + +import 'package:equatable/equatable.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/extensions/color_extension.dart'; +import 'package:fl_chart/src/extensions/gradient_extension.dart'; +import 'package:fl_chart/src/utils/lerp.dart'; +import 'package:flutter/material.dart' hide Image; + +/// [LineChart] needs this class to render itself. +/// +/// It holds data needed to draw a line chart, +/// including bar lines, spots, colors, touches, ... +class LineChartData extends AxisChartData with EquatableMixin { + /// [LineChart] draws some lines in various shapes and overlaps them. + /// lines are defined in [lineBarsData], sometimes you need to fill space between two bars + /// with a color or gradient, you can use [betweenBarsData] to achieve that. + /// + /// It draws some titles on left, top, right, bottom sides per each axis number, + /// you can modify [titlesData] to have your custom titles, + /// also you can define the axis title (one text per axis) for each side + /// using [axisTitleData], you can restrict the y axis using [minY] and [maxY] value, + /// and restrict x axis using [minX] and [maxX]. + /// + /// It draws a color as a background behind everything you can set it using [backgroundColor], + /// then a grid over it, you can customize it using [gridData], + /// and it draws 4 borders around your chart, you can customize it using [borderData]. + /// + /// You can annotate some regions with a highlight color using [rangeAnnotations]. + /// + /// You can modify [lineTouchData] to customize touch behaviors and responses. + /// + /// you can show some tooltipIndicators (a popup with an information) + /// on top of each [LineChartBarData.spots] using [showingTooltipIndicators], + /// just put line indicator number and spots indices you want to show it on top of them. + /// + /// [LineChart] draws some horizontal or vertical lines on above or below of everything, + /// they are useful in some scenarios, for example you can show average line, you can fill + /// [extraLinesData] property to have your extra lines. + /// + /// [clipData] forces the [LineChart] to draw lines inside the chart bounding box. + LineChartData({ + this.lineBarsData = const [], + this.betweenBarsData = const [], + super.titlesData = const FlTitlesData(), + super.extraLinesData = const ExtraLinesData(), + this.lineTouchData = const LineTouchData(), + this.showingTooltipIndicators = const [], + super.gridData = const FlGridData(), + super.borderData, + super.rangeAnnotations = const RangeAnnotations(), + double? minX, + double? maxX, + super.baselineX, + double? minY, + double? maxY, + super.baselineY, + super.clipData = const FlClipData.none(), + super.backgroundColor, + super.rotationQuarterTurns, + }) : super( + minX: minX ?? double.nan, + maxX: maxX ?? double.nan, + minY: minY ?? double.nan, + maxY: maxY ?? double.nan, + ); + + /// [LineChart] draws some lines in various shapes and overlaps them. + final List lineBarsData; + + /// Fills area between two [LineChartBarData] with a color or gradient. + final List betweenBarsData; + + /// Handles touch behaviors and responses. + final LineTouchData lineTouchData; + + /// You can show some tooltipIndicators (a popup with an information) + /// on top of each [LineChartBarData.spots] using [showingTooltipIndicators], + /// just put line indicator number and spots indices you want to show it on top of them. + /// + /// An important point is that you have to disable the default touch behaviour + /// to show the tooltip manually, see [LineTouchData.handleBuiltInTouches]. + final List showingTooltipIndicators; + + /// Lerps a [BaseChartData] based on [t] value, check [Tween.lerp]. + @override + LineChartData lerp(BaseChartData a, BaseChartData b, double t) { + if (a is LineChartData && b is LineChartData) { + return LineChartData( + minX: lerpDouble(a.minX, b.minX, t), + maxX: lerpDouble(a.maxX, b.maxX, t), + baselineX: lerpDouble(a.baselineX, b.baselineX, t), + minY: lerpDouble(a.minY, b.minY, t), + maxY: lerpDouble(a.maxY, b.maxY, t), + baselineY: lerpDouble(a.baselineY, b.baselineY, t), + backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), + borderData: FlBorderData.lerp(a.borderData, b.borderData, t), + clipData: b.clipData, + extraLinesData: + ExtraLinesData.lerp(a.extraLinesData, b.extraLinesData, t), + gridData: FlGridData.lerp(a.gridData, b.gridData, t), + titlesData: FlTitlesData.lerp(a.titlesData, b.titlesData, t), + rangeAnnotations: + RangeAnnotations.lerp(a.rangeAnnotations, b.rangeAnnotations, t), + lineBarsData: + lerpLineChartBarDataList(a.lineBarsData, b.lineBarsData, t)!, + betweenBarsData: + lerpBetweenBarsDataList(a.betweenBarsData, b.betweenBarsData, t)!, + lineTouchData: b.lineTouchData, + showingTooltipIndicators: b.showingTooltipIndicators, + rotationQuarterTurns: b.rotationQuarterTurns, + ); + } else { + throw Exception('Illegal State'); + } + } + + /// Copies current [LineChartData] to a new [LineChartData], + /// and replaces provided values. + LineChartData copyWith({ + List? lineBarsData, + List? betweenBarsData, + FlTitlesData? titlesData, + RangeAnnotations? rangeAnnotations, + ExtraLinesData? extraLinesData, + LineTouchData? lineTouchData, + List? showingTooltipIndicators, + FlGridData? gridData, + FlBorderData? borderData, + double? minX, + double? maxX, + double? baselineX, + double? minY, + double? maxY, + double? baselineY, + FlClipData? clipData, + Color? backgroundColor, + int? rotationQuarterTurns, + }) => + LineChartData( + lineBarsData: lineBarsData ?? this.lineBarsData, + betweenBarsData: betweenBarsData ?? this.betweenBarsData, + titlesData: titlesData ?? this.titlesData, + rangeAnnotations: rangeAnnotations ?? this.rangeAnnotations, + extraLinesData: extraLinesData ?? this.extraLinesData, + lineTouchData: lineTouchData ?? this.lineTouchData, + showingTooltipIndicators: + showingTooltipIndicators ?? this.showingTooltipIndicators, + gridData: gridData ?? this.gridData, + borderData: borderData ?? this.borderData, + minX: minX ?? this.minX, + maxX: maxX ?? this.maxX, + baselineX: baselineX ?? this.baselineX, + minY: minY ?? this.minY, + maxY: maxY ?? this.maxY, + baselineY: baselineY ?? this.baselineY, + clipData: clipData ?? this.clipData, + backgroundColor: backgroundColor ?? this.backgroundColor, + rotationQuarterTurns: rotationQuarterTurns ?? this.rotationQuarterTurns, + ); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + lineBarsData, + betweenBarsData, + titlesData, + extraLinesData, + lineTouchData, + showingTooltipIndicators, + gridData, + borderData, + rangeAnnotations, + minX, + maxX, + baselineX, + minY, + maxY, + baselineY, + clipData, + backgroundColor, + rotationQuarterTurns, + ]; +} + +enum LineChartGradientArea { + /// The gradient area will be around the line only, meaning + /// the gradient will exactly wrap around the curve. + rectAroundTheLine, + + /// The entire chart area will be used as the gradient area for the curve. + wholeChart; +} + +/// Holds data for drawing each individual line in the [LineChart] +class LineChartBarData with EquatableMixin { + /// [BarChart] draws some lines and overlaps them in the chart's view, + /// You can have multiple lines by splitting them, + /// put a [FlSpot.nullSpot] between each section. + /// each line passes through [spots], with hard edges by default, + /// [isCurved] makes it curve for drawing, and [curveSmoothness] determines the curve smoothness. + /// + /// [show] determines the drawing, if set to false, it draws nothing. + /// + /// [mainColors] determines the color of drawing line, if one color provided it applies a solid color, + /// otherwise it gradients between provided colors for drawing the line. + /// Gradient happens using provided [colorStops], [gradientFrom], [gradientTo]. + /// if you want it draw normally, don't touch them, + /// check [LinearGradient] for understanding [colorStops] + /// + /// [barWidth] determines the thickness of drawing line, + /// + /// if [isCurved] is true, in some situations if the spots changes are in high values, + /// an overshooting will happen, we don't have any idea to solve this at the moment, + /// but you can set [preventCurveOverShooting] true, and update the threshold + /// using [preventCurveOvershootingThreshold] to achieve an acceptable curve, + /// check this [issue](https://github.com/imaNNeo/fl_chart/issues/25) + /// to overshooting understand the problem. + /// + /// [isStrokeCapRound] determines the shape of line's cap. + /// + /// [isStrokeJoinRound] determines the shape of the line joins. + /// + /// [belowBarData], and [aboveBarData] used to fill the space below or above the drawn line, + /// you can fill with a solid color or a linear gradient. + /// + /// [LineChart] draws points that the line is going through [spots], + /// you can customize it's appearance using [dotData]. + /// + /// there are some indicators with a line and bold point on each spot, + /// you can show them by filling [showingIndicators] with indices + /// you want to show indicator on them. + /// + /// [LineChart] draws the lines with dashed effect if you fill [dashArray]. + /// + /// If you want to have a Step Line Chart style, just set [isStepLineChart] true, + /// also you can tweak the [LineChartBarData.lineChartStepData]. + LineChartBarData({ + this.spots = const [], + this.show = true, + Color? color, + this.gradient, + this.gradientArea = LineChartGradientArea.rectAroundTheLine, + this.barWidth = 2.0, + this.isCurved = false, + this.curveSmoothness = 0.35, + this.preventCurveOverShooting = false, + this.preventCurveOvershootingThreshold = 10.0, + this.isStrokeCapRound = false, + this.isStrokeJoinRound = false, + BarAreaData? belowBarData, + BarAreaData? aboveBarData, + this.dotData = const FlDotData(), + this.errorIndicatorData = + const FlErrorIndicatorData(), + this.showingIndicators = const [], + this.dashArray, + this.shadow = const Shadow(color: Colors.transparent), + this.isStepLineChart = false, + this.lineChartStepData = const LineChartStepData(), + }) : color = + color ?? ((color == null && gradient == null) ? Colors.cyan : null), + belowBarData = belowBarData ?? BarAreaData(), + aboveBarData = aboveBarData ?? BarAreaData() { + FlSpot? mostLeft; + FlSpot? mostTop; + FlSpot? mostRight; + FlSpot? mostBottom; + + FlSpot? firstValidSpot; + try { + firstValidSpot = + spots.firstWhere((element) => element != FlSpot.nullSpot); + } catch (_) { + // There is no valid spot + } + if (firstValidSpot != null) { + for (final spot in spots) { + if (spot.isNull()) { + continue; + } + if (mostLeft == null || spot.x < mostLeft.x) { + mostLeft = spot; + } + + if (mostRight == null || spot.x > mostRight.x) { + mostRight = spot; + } + + if (mostTop == null || spot.y > mostTop.y) { + mostTop = spot; + } + + if (mostBottom == null || spot.y < mostBottom.y) { + mostBottom = spot; + } + } + mostLeftSpot = mostLeft!; + mostTopSpot = mostTop!; + mostRightSpot = mostRight!; + mostBottomSpot = mostBottom!; + } + } + + /// This line goes through this spots. + /// + /// You can have multiple lines by splitting them, + /// put a [FlSpot.nullSpot] between each section. + final List spots; + + /// We keep the most left spot to prevent redundant calculations + late final FlSpot mostLeftSpot; + + /// We keep the most top spot to prevent redundant calculations + late final FlSpot mostTopSpot; + + /// We keep the most right spot to prevent redundant calculations + late final FlSpot mostRightSpot; + + /// We keep the most bottom spot to prevent redundant calculations + late final FlSpot mostBottomSpot; + + /// Determines to show or hide the line. + final bool show; + + /// If provided, this [LineChartBarData] draws with this [color] + /// Otherwise we use [gradient] to draw the background. + /// It throws an exception if you provide both [color] and [gradient] + final Color? color; + + /// If provided, this [LineChartBarData] draws with this [gradient]. + /// Otherwise we use [color] to draw the background. + /// It throws an exception if you provide both [color] and [gradient] + final Gradient? gradient; + + /// Only effective if [gradient] is provided. + /// + /// It will be used to determine the area of the gradient. + final LineChartGradientArea gradientArea; + + /// Determines thickness of drawing line. + final double barWidth; + + /// If it's true, [LineChart] draws the line with curved edges, + /// otherwise it draws line with hard edges. + final bool isCurved; + + /// If [isCurved] is true, it determines smoothness of the curved edges. + final double curveSmoothness; + + /// Prevent overshooting when draw curve line with high value changes. + /// check this [issue](https://github.com/imaNNeo/fl_chart/issues/25) + final bool preventCurveOverShooting; + + /// Applies threshold for [preventCurveOverShooting] algorithm. + final double preventCurveOvershootingThreshold; + + /// Determines the style of line's cap. + final bool isStrokeCapRound; + + /// Determines the style of line joins. + final bool isStrokeJoinRound; + + /// Fills the space blow the line, using a color or gradient. + final BarAreaData belowBarData; + + /// Fills the space above the line, using a color or gradient. + final BarAreaData aboveBarData; + + /// Responsible to showing [spots] on the line as a circular point. + final FlDotData dotData; + + /// Holds data for showing error indicators on the spots in this line. + final FlErrorIndicatorData + errorIndicatorData; + + /// Show indicators based on provided indexes + final List showingIndicators; + + /// Determines the dash length and space respectively, fill it if you want to have dashed line. + final List? dashArray; + + /// Drops a shadow behind the bar line. + final Shadow shadow; + + /// If sets true, it draws the chart in Step Line Chart style, using [LineChartBarData.lineChartStepData]. + final bool isStepLineChart; + + /// Holds data for representing a Step Line Chart, and works only if [isStepChart] is true. + final LineChartStepData lineChartStepData; + + /// Lerps a [LineChartBarData] based on [t] value, check [Tween.lerp]. + static LineChartBarData lerp( + LineChartBarData a, + LineChartBarData b, + double t, + ) => + LineChartBarData( + show: b.show, + barWidth: lerpDouble(a.barWidth, b.barWidth, t)!, + belowBarData: BarAreaData.lerp(a.belowBarData, b.belowBarData, t), + aboveBarData: BarAreaData.lerp(a.aboveBarData, b.aboveBarData, t), + curveSmoothness: b.curveSmoothness, + isCurved: b.isCurved, + isStrokeCapRound: b.isStrokeCapRound, + isStrokeJoinRound: b.isStrokeJoinRound, + preventCurveOverShooting: b.preventCurveOverShooting, + preventCurveOvershootingThreshold: lerpDouble( + a.preventCurveOvershootingThreshold, + b.preventCurveOvershootingThreshold, + t, + )!, + dotData: FlDotData.lerp(a.dotData, b.dotData, t), + errorIndicatorData: FlErrorIndicatorData.lerp( + a.errorIndicatorData, + b.errorIndicatorData, + t, + ), + dashArray: lerpIntList(a.dashArray, b.dashArray, t), + color: Color.lerp(a.color, b.color, t), + gradient: Gradient.lerp(a.gradient, b.gradient, t), + gradientArea: b.gradientArea, + spots: lerpFlSpotList(a.spots, b.spots, t)!, + showingIndicators: b.showingIndicators, + shadow: Shadow.lerp(a.shadow, b.shadow, t)!, + isStepLineChart: b.isStepLineChart, + lineChartStepData: + LineChartStepData.lerp(a.lineChartStepData, b.lineChartStepData, t), + ); + + /// Copies current [LineChartBarData] to a new [LineChartBarData], + /// and replaces provided values. + LineChartBarData copyWith({ + List? spots, + bool? show, + Color? color, + Gradient? gradient, + LineChartGradientArea? gradientArea, + double? barWidth, + bool? isCurved, + double? curveSmoothness, + bool? preventCurveOverShooting, + double? preventCurveOvershootingThreshold, + bool? isStrokeCapRound, + bool? isStrokeJoinRound, + BarAreaData? belowBarData, + BarAreaData? aboveBarData, + FlDotData? dotData, + FlErrorIndicatorData? + errorIndicatorData, + List? dashArray, + List? showingIndicators, + Shadow? shadow, + bool? isStepLineChart, + LineChartStepData? lineChartStepData, + }) => + LineChartBarData( + spots: spots ?? this.spots, + show: show ?? this.show, + color: color ?? this.color, + gradient: gradient ?? this.gradient, + gradientArea: gradientArea ?? this.gradientArea, + barWidth: barWidth ?? this.barWidth, + isCurved: isCurved ?? this.isCurved, + curveSmoothness: curveSmoothness ?? this.curveSmoothness, + preventCurveOverShooting: + preventCurveOverShooting ?? this.preventCurveOverShooting, + preventCurveOvershootingThreshold: preventCurveOvershootingThreshold ?? + this.preventCurveOvershootingThreshold, + isStrokeCapRound: isStrokeCapRound ?? this.isStrokeCapRound, + isStrokeJoinRound: isStrokeJoinRound ?? this.isStrokeJoinRound, + belowBarData: belowBarData ?? this.belowBarData, + aboveBarData: aboveBarData ?? this.aboveBarData, + dashArray: dashArray ?? this.dashArray, + dotData: dotData ?? this.dotData, + errorIndicatorData: errorIndicatorData ?? this.errorIndicatorData, + showingIndicators: showingIndicators ?? this.showingIndicators, + shadow: shadow ?? this.shadow, + isStepLineChart: isStepLineChart ?? this.isStepLineChart, + lineChartStepData: lineChartStepData ?? this.lineChartStepData, + ); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + spots, + show, + color, + gradient, + gradientArea, + barWidth, + isCurved, + curveSmoothness, + preventCurveOverShooting, + preventCurveOvershootingThreshold, + isStrokeCapRound, + isStrokeJoinRound, + belowBarData, + aboveBarData, + dotData, + errorIndicatorData, + showingIndicators, + dashArray, + shadow, + isStepLineChart, + lineChartStepData, + ]; +} + +/// Holds data for representing a Step Line Chart, and works only if [LineChartBarData.isStepChart] is true. +class LineChartStepData with EquatableMixin { + /// Determines the [stepDirection] of each step; + const LineChartStepData({this.stepDirection = stepDirectionMiddle}); + + /// Go to the next spot directly, with the current point's y value. + static const stepDirectionForward = 0.0; + + /// Go to the half with the current spot y, and with the next spot y for the rest. + static const stepDirectionMiddle = 0.5; + + /// Go to the next spot y and direct line to the next spot. + static const stepDirectionBackward = 1.0; + + /// Determines the direction of each step; + final double stepDirection; + + /// Lerps a [LineChartStepData] based on [t] value, check [Tween.lerp]. + static LineChartStepData lerp( + LineChartStepData a, + LineChartStepData b, + double t, + ) => + LineChartStepData( + stepDirection: lerpDouble(a.stepDirection, b.stepDirection, t)!, + ); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [stepDirection]; +} + +/// Holds data for filling an area (above or below) of the line with a color or gradient. +class BarAreaData with EquatableMixin { + /// if [show] is true, [LineChart] fills above and below area of each line + /// with a color or gradient. + /// + /// [color] determines the color of above or below space area, + /// if one color provided it applies a solid color, + /// otherwise it gradients between provided colors for drawing the line. + /// Gradient happens using provided [gradientColorStops], [gradientFrom], [gradientTo]. + /// if you want it draw normally, don't touch them, + /// check [LinearGradient] for understanding [gradientColorStops] + /// + /// If [spotsLine] is provided, it draws some lines from each spot + /// to the bottom or top of the chart. + /// + /// If [applyCutOffY] is true, it cuts the drawing by the [cutOffY] line. + BarAreaData({ + this.show = false, + Color? color, + this.gradient, + this.spotsLine = const BarAreaSpotsLine(), + this.cutOffY = 0, + this.applyCutOffY = false, + }) : color = color ?? + ((color == null && gradient == null) + ? Colors.blueGrey.withValues(alpha: 0.5) + : null); + + final bool show; + + /// If provided, this [BarAreaData] draws with this [color] + /// Otherwise we use [gradient] to draw the background. + /// It throws an exception if you provide both [color] and [gradient] + final Color? color; + + /// If provided, this [BarAreaData] draws with this [gradient]. + /// Otherwise we use [color] to draw the background. + /// It throws an exception if you provide both [color] and [gradient] + final Gradient? gradient; + + /// holds data for drawing a line from each spot the the bottom, or top of the chart + final BarAreaSpotsLine spotsLine; + + /// cut the drawing below or above area to this y value + final double cutOffY; + + /// determines should or shouldn't apply cutOffY + final bool applyCutOffY; + + /// Lerps a [BarAreaData] based on [t] value, check [Tween.lerp]. + static BarAreaData lerp(BarAreaData a, BarAreaData b, double t) => + BarAreaData( + show: b.show, + spotsLine: BarAreaSpotsLine.lerp(a.spotsLine, b.spotsLine, t), + color: Color.lerp(a.color, b.color, t), + gradient: Gradient.lerp(a.gradient, b.gradient, t), + cutOffY: lerpDouble(a.cutOffY, b.cutOffY, t)!, + applyCutOffY: b.applyCutOffY, + ); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + show, + color, + gradient, + spotsLine, + cutOffY, + applyCutOffY, + ]; +} + +/// Holds data about filling below or above space of the bar line, +class BetweenBarsData with EquatableMixin { + BetweenBarsData({ + required this.fromIndex, + required this.toIndex, + Color? color, + this.gradient, + }) : color = color ?? + ((color == null && gradient == null) + ? Colors.blueGrey.withValues(alpha: 0.5) + : null); + + /// The index of the lineBarsData from where the area has to be rendered + final int fromIndex; + + /// The index of the lineBarsData until where the area has to be rendered + final int toIndex; + + /// If provided, this [BetweenBarsData] draws with this [color] + /// Otherwise we use [gradient] to draw the background. + /// It throws an exception if you provide both [color] and [gradient] + final Color? color; + + /// If provided, this [BetweenBarsData] draws with this [gradient]. + /// Otherwise we use [color] to draw the background. + /// It throws an exception if you provide both [color] and [gradient] + final Gradient? gradient; + + /// Lerps a [BetweenBarsData] based on [t] value, check [Tween.lerp]. + static BetweenBarsData lerp(BetweenBarsData a, BetweenBarsData b, double t) { + return BetweenBarsData( + fromIndex: b.fromIndex, + toIndex: b.toIndex, + color: Color.lerp(a.color, b.color, t), + gradient: Gradient.lerp(a.gradient, b.gradient, t), + ); + } + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + fromIndex, + toIndex, + color, + gradient, + ]; +} + +/// Holds data for drawing line on the spots under the [BarAreaData]. +class BarAreaSpotsLine with EquatableMixin { + /// If [show] is true, [LineChart] draws some lines on above or below the spots, + /// you can customize the appearance of the lines using [flLineStyle] + /// and you can decide to show or hide the lines on each spot using [checkToShowSpotLine]. + const BarAreaSpotsLine({ + this.show = false, + this.flLineStyle = const FlLine(), + this.checkToShowSpotLine = showAllSpotsBelowLine, + this.applyCutOffY = true, + }); + + /// Determines to show or hide all the lines. + final bool show; + + /// Holds appearance of drawing line on the spots. + final FlLine flLineStyle; + + /// Checks to show or hide lines on the spots. + final CheckToShowSpotLine checkToShowSpotLine; + + /// Determines to inherit the cutOff properties from its parent [BarAreaData] + final bool applyCutOffY; + + /// Lerps a [BarAreaSpotsLine] based on [t] value, check [Tween.lerp]. + static BarAreaSpotsLine lerp( + BarAreaSpotsLine a, + BarAreaSpotsLine b, + double t, + ) => + BarAreaSpotsLine( + show: b.show, + checkToShowSpotLine: b.checkToShowSpotLine, + flLineStyle: FlLine.lerp(a.flLineStyle, b.flLineStyle, t), + applyCutOffY: b.applyCutOffY, + ); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + show, + flLineStyle, + checkToShowSpotLine, + applyCutOffY, + ]; +} + +/// It used for determine showing or hiding [BarAreaSpotsLine]s +/// +/// Gives you the checking spot, and you have to decide to +/// show or not show the line on the provided spot. +typedef CheckToShowSpotLine = bool Function(FlSpot spot); + +/// Shows all spot lines. +bool showAllSpotsBelowLine(FlSpot spot) => true; + +/// The callback passed to get the color of a [FlSpot] +/// +/// The callback receives [FlSpot], which is the target spot, +/// [double] is the percentage of spot along the bar line, +/// [LineChartBarData] is the chart's bar. +/// It should return a [Color] that needs to be used for drawing target. +typedef GetDotColorCallback = Color Function(FlSpot, double, LineChartBarData); + +/// If there is one color in [LineChartBarData.mainColors], it returns that color, +/// otherwise it returns the color along the gradient colors based on the [xPercentage]. +Color _defaultGetDotColor(FlSpot _, double xPercentage, LineChartBarData bar) { + if (bar.gradient != null && bar.gradient is LinearGradient) { + return lerpGradient( + bar.gradient!.colors, + bar.gradient!.getSafeColorStops(), + xPercentage / 100, + ); + } + return bar.gradient?.colors.first ?? bar.color ?? Colors.blueGrey; +} + +/// If there is one color in [LineChartBarData.mainColors], it returns that color in a darker mode, +/// otherwise it returns the color along the gradient colors based on the [xPercentage] in a darker mode. +Color _defaultGetDotStrokeColor( + FlSpot spot, + double xPercentage, + LineChartBarData bar, +) { + Color color; + if (bar.gradient != null && bar.gradient is LinearGradient) { + color = lerpGradient( + bar.gradient!.colors, + bar.gradient!.getSafeColorStops(), + xPercentage / 100, + ); + } else { + color = bar.gradient?.colors.first ?? bar.color ?? Colors.blueGrey; + } + return color.darken(); +} + +/// The callback passed to get the painter of a [FlSpot] +/// +/// The callback receives [FlSpot], which is the target spot, +/// [LineChartBarData] is the chart's bar. +/// [int] is the index position of the spot. +/// It should return a [FlDotPainter] that needs to be used for drawing target. +typedef GetDotPainterCallback = FlDotPainter Function( + FlSpot, + double, + LineChartBarData, + int, +); + +FlDotPainter _defaultGetDotPainter( + FlSpot spot, + double xPercentage, + LineChartBarData bar, + int index, { + double? size, +}) => + FlDotCirclePainter( + radius: size, + color: _defaultGetDotColor(spot, xPercentage, bar), + strokeColor: _defaultGetDotStrokeColor(spot, xPercentage, bar), + ); + +/// This class holds data about drawing spot dots on the drawing bar line. +class FlDotData with EquatableMixin { + /// set [show] false to prevent dots from drawing, + /// if you want to show or hide dots in some spots, + /// override [checkToShowDot] to handle it in your way. + const FlDotData({ + this.show = true, + this.checkToShowDot = showAllDots, + this.getDotPainter = _defaultGetDotPainter, + }); + + /// Determines show or hide all dots. + final bool show; + + /// Checks to show or hide an individual dot. + final CheckToShowDot checkToShowDot; + + /// Callback which is called to set the painter of the given [FlSpot]. + /// The [FlSpot] is provided as parameter to this callback + final GetDotPainterCallback getDotPainter; + + /// Lerps a [FlDotData] based on [t] value, check [Tween.lerp]. + static FlDotData lerp(FlDotData a, FlDotData b, double t) => FlDotData( + show: b.show, + checkToShowDot: b.checkToShowDot, + getDotPainter: b.getDotPainter, + ); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + show, + checkToShowDot, + getDotPainter, + ]; +} + +/// It determines showing or hiding [FlDotData] on the spots. +/// +/// It gives you the checking [FlSpot] and you should decide to +/// show or hide the dot on this spot by returning true or false. +typedef CheckToShowDot = bool Function(FlSpot spot, LineChartBarData barData); + +/// Shows all dots on spots. +bool showAllDots(FlSpot spot, LineChartBarData barData) => true; + +enum LabelDirection { horizontal, vertical } + +/// Shows a text label +abstract class FlLineLabel with EquatableMixin { + /// Draws a title on the line, align it with [alignment] over the line, + /// applies [padding] for spaces, and applies [style] for changing color, + /// size, ... of the text. + /// [show] determines showing label or not. + /// [direction] determines if the direction of the text should be horizontal or vertical. + const FlLineLabel({ + required this.show, + required this.padding, + required this.style, + required this.alignment, + required this.direction, + }); + + /// Determines showing label or not. + final bool show; + + /// Inner spaces around the drawing text. + final EdgeInsetsGeometry padding; + + /// Sets style of the drawing text. + final TextStyle? style; + + /// Aligns the text on the line. + final Alignment alignment; + + /// Determines the direction of the text. + final LabelDirection direction; + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + show, + padding, + style, + alignment, + direction, + ]; +} + +/// Holds data to handle touch events, and touch responses in the [LineChart]. +/// +/// There is a touch flow, explained [here](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/handle_touches.md) +/// in a simple way, each chart's renderer captures the touch events, and passes the pointerEvent +/// to the painter, and gets touched spot, and wraps it into a concrete [LineTouchResponse]. +class LineTouchData extends FlTouchData with EquatableMixin { + /// You can disable or enable the touch system using [enabled] flag, + /// + /// [touchCallback] notifies you about the happened touch/pointer events. + /// It gives you a [FlTouchEvent] which is the happened event such as [FlPointerHoverEvent], [FlTapUpEvent], ... + /// It also gives you a [LineTouchResponse] which contains information + /// about the elements that has touched. + /// + /// Using [mouseCursorResolver] you can change the mouse cursor + /// based on the provided [FlTouchEvent] and [LineTouchResponse] + /// + /// if [handleBuiltInTouches] is true, [LineChart] shows a tooltip popup on top of the spots if + /// touch occurs (or you can show it manually using, [LineChartData.showingTooltipIndicators]) + /// and also it shows an indicator (contains a thicker line and larger dot on the targeted spot), + /// You can define how this indicator looks like through [getTouchedSpotIndicator] callback, + /// You can customize this tooltip using [touchTooltipData], indicator lines starts from position + /// controlled by [getTouchLineStart] and ends at position controlled by [getTouchLineEnd]. + /// If you need to have a distance threshold for handling touches, use [touchSpotThreshold]. + const LineTouchData({ + bool enabled = true, + BaseTouchCallback? touchCallback, + MouseCursorResolver? mouseCursorResolver, + Duration? longPressDuration, + this.touchTooltipData = const LineTouchTooltipData(), + this.getTouchedSpotIndicator = defaultTouchedIndicators, + this.touchSpotThreshold = 10, + this.distanceCalculator = _xDistance, + this.handleBuiltInTouches = true, + this.getTouchLineStart = defaultGetTouchLineStart, + this.getTouchLineEnd = defaultGetTouchLineEnd, + }) : super( + enabled, + touchCallback, + mouseCursorResolver, + longPressDuration, + ); + + /// Configs of how touch tooltip popup. + final LineTouchTooltipData touchTooltipData; + + /// Configs of how touch indicator looks like. + final GetTouchedSpotIndicator getTouchedSpotIndicator; + + /// Distance threshold to handle the touch event. + final double touchSpotThreshold; + + /// Distance function used when finding closest points to touch point + final CalculateTouchDistance distanceCalculator; + + /// Determines to handle default built-in touch responses, + /// [LineTouchResponse] shows a tooltip popup above the touched spot. + final bool handleBuiltInTouches; + + /// The starting point on y axis of the touch line. By default, line starts on the bottom of + /// the chart. + final GetTouchLineY getTouchLineStart; + + /// The end point on y axis of the touch line. By default, line ends at the touched point. + /// If line end is overlap with the dot, it will be automatically adjusted to the edge of the dot. + final GetTouchLineY getTouchLineEnd; + + /// Copies current [LineTouchData] to a new [LineTouchData], + /// and replaces provided values. + LineTouchData copyWith({ + bool? enabled, + BaseTouchCallback? touchCallback, + MouseCursorResolver? mouseCursorResolver, + Duration? longPressDuration, + LineTouchTooltipData? touchTooltipData, + GetTouchedSpotIndicator? getTouchedSpotIndicator, + double? touchSpotThreshold, + CalculateTouchDistance? distanceCalculator, + GetTouchLineY? getTouchLineStart, + GetTouchLineY? getTouchLineEnd, + bool? handleBuiltInTouches, + }) => + LineTouchData( + enabled: enabled ?? this.enabled, + touchCallback: touchCallback ?? this.touchCallback, + mouseCursorResolver: mouseCursorResolver ?? this.mouseCursorResolver, + longPressDuration: longPressDuration ?? this.longPressDuration, + touchTooltipData: touchTooltipData ?? this.touchTooltipData, + getTouchedSpotIndicator: + getTouchedSpotIndicator ?? this.getTouchedSpotIndicator, + touchSpotThreshold: touchSpotThreshold ?? this.touchSpotThreshold, + distanceCalculator: distanceCalculator ?? this.distanceCalculator, + getTouchLineStart: getTouchLineStart ?? this.getTouchLineStart, + getTouchLineEnd: getTouchLineEnd ?? this.getTouchLineEnd, + handleBuiltInTouches: handleBuiltInTouches ?? this.handleBuiltInTouches, + ); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + enabled, + touchCallback, + mouseCursorResolver, + longPressDuration, + touchTooltipData, + getTouchedSpotIndicator, + touchSpotThreshold, + distanceCalculator, + handleBuiltInTouches, + getTouchLineStart, + getTouchLineEnd, + ]; +} + +/// Used for showing touch indicators (a thicker line and larger dot on the targeted spot). +/// +/// It gives you the [spotIndexes] that touch happened, or manually targeted, +/// in the given [barData], you should return a list of [TouchedSpotIndicatorData], +/// length of this list should be equal to the [spotIndexes.length], +/// each [TouchedSpotIndicatorData] determines the look of showing indicator. +typedef GetTouchedSpotIndicator = List Function( + LineChartBarData barData, + List spotIndexes, +); + +/// Used for determine the touch indicator line's starting/end point. +typedef GetTouchLineY = double Function( + LineChartBarData barData, + int spotIndex, +); + +/// Used to calculate the distance between coordinates of a touch event and a spot +typedef CalculateTouchDistance = double Function( + Offset touchPoint, + Offset spotPixelCoordinates, +); + +/// Default distanceCalculator only considers distance on x axis +double _xDistance(Offset touchPoint, Offset spotPixelCoordinates) => + (touchPoint.dx - spotPixelCoordinates.dx).abs(); + +/// Default presentation of touched indicators. +List defaultTouchedIndicators( + LineChartBarData barData, + List indicators, +) => + indicators.map((int index) { + /// Indicator Line + var lineColor = barData.gradient?.colors.first ?? barData.color; + if (barData.dotData.show) { + lineColor = _defaultGetDotColor(barData.spots[index], 0, barData); + } + const lineStrokeWidth = 4.0; + final flLine = FlLine(color: lineColor, strokeWidth: lineStrokeWidth); + + var dotSize = 10.0; + if (barData.dotData.show) { + dotSize = 4.0 * 1.8; + } + + final dotData = FlDotData( + getDotPainter: (spot, percent, bar, index) => + _defaultGetDotPainter(spot, percent, bar, index, size: dotSize), + ); + + return TouchedSpotIndicatorData(flLine, dotData); + }).toList(); + +/// By default line starts from the bottom of the chart. +double defaultGetTouchLineStart(LineChartBarData barData, int spotIndex) { + return -double.infinity; +} + +/// By default line ends at the touched point. +double defaultGetTouchLineEnd(LineChartBarData barData, int spotIndex) => + barData.spots[spotIndex].y; + +/// Holds representation data for showing tooltip popup on top of spots. +class LineTouchTooltipData with EquatableMixin { + /// if [LineTouchData.handleBuiltInTouches] is true, + /// [LineChart] shows a tooltip popup on top of spots automatically when touch happens, + /// otherwise you can show it manually using [LineChartData.showingTooltipIndicators]. + /// Tooltip shows on top of rods, with [getTooltipColor] as a background color. + /// You can set the corner radius using [tooltipBorderRadius], + /// If you want to have a padding inside the tooltip, fill [tooltipPadding], + /// or If you want to have a bottom margin, set [tooltipMargin]. + /// Content of the tooltip will provide using [getTooltipItems] callback, you can override it + /// and pass your custom data to show in the tooltip. + /// You can restrict the tooltip's width using [maxContentWidth]. + /// Sometimes, [LineChart] shows the tooltip outside of the chart, + /// you can set [fitInsideHorizontally] true to force it to shift inside the chart horizontally, + /// also you can set [fitInsideVertically] true to force it to shift inside the chart vertically. + const LineTouchTooltipData({ + BorderRadius? tooltipBorderRadius, + this.tooltipPadding = + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + this.tooltipMargin = 16, + this.tooltipHorizontalAlignment = FLHorizontalAlignment.center, + this.tooltipHorizontalOffset = 0, + this.maxContentWidth = 120, + this.getTooltipItems = defaultLineTooltipItem, + this.getTooltipColor = defaultLineTooltipColor, + this.fitInsideHorizontally = false, + this.fitInsideVertically = false, + this.showOnTopOfTheChartBoxArea = false, + this.rotateAngle = 0.0, + this.tooltipBorder = BorderSide.none, + }) : _tooltipBorderRadius = tooltipBorderRadius; + + /// Sets a rounded radius for the tooltip. + final BorderRadius? _tooltipBorderRadius; + + /// Sets a rounded radius for the tooltip. + BorderRadius get tooltipBorderRadius => + _tooltipBorderRadius ?? BorderRadius.circular(4); + + /// Applies a padding for showing contents inside the tooltip. + final EdgeInsets tooltipPadding; + + /// Applies a bottom margin for showing tooltip on top of rods. + final double tooltipMargin; + + /// Controls showing tooltip on left side, right side or center aligned with spot, default is center + final FLHorizontalAlignment tooltipHorizontalAlignment; + + /// Applies horizontal offset for showing tooltip, default is zero. + final double tooltipHorizontalOffset; + + /// Restricts the tooltip's width. + final double maxContentWidth; + + /// Retrieves data for showing content inside the tooltip. + final GetLineTooltipItems getTooltipItems; + + /// Forces the tooltip to shift horizontally inside the chart, if overflow happens. + final bool fitInsideHorizontally; + + /// Forces the tooltip to shift vertically inside the chart, if overflow happens. + final bool fitInsideVertically; + + /// Forces the tooltip container to top of the line, default 'false' + final bool showOnTopOfTheChartBoxArea; + + /// Controls the rotation of the tooltip. + final double rotateAngle; + + /// The tooltip border color. + final BorderSide tooltipBorder; + + // /// Retrieves data for setting background color of the tooltip. + final GetLineTooltipColor getTooltipColor; + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + _tooltipBorderRadius, + tooltipPadding, + tooltipMargin, + tooltipHorizontalAlignment, + tooltipHorizontalOffset, + maxContentWidth, + getTooltipItems, + fitInsideHorizontally, + fitInsideVertically, + showOnTopOfTheChartBoxArea, + rotateAngle, + tooltipBorder, + getTooltipColor, + ]; +} + +/// Provides a [LineTooltipItem] for showing content inside the [LineTouchTooltipData]. +/// +/// You can override [LineTouchTooltipData.getTooltipItems], it gives you +/// [touchedSpots] list that touch happened on, +/// then you should and pass your custom [LineTooltipItem] list +/// (length should be equal to the [touchedSpots.length]), +/// to show inside the tooltip popup. +typedef GetLineTooltipItems = List Function( + List touchedSpots, +); + +/// Default implementation for [LineTouchTooltipData.getTooltipItems]. +List defaultLineTooltipItem(List touchedSpots) => + touchedSpots.map((LineBarSpot touchedSpot) { + final textStyle = TextStyle( + color: touchedSpot.bar.gradient?.colors.first ?? + touchedSpot.bar.color ?? + Colors.blueGrey, + fontWeight: FontWeight.bold, + fontSize: 14, + ); + return LineTooltipItem(touchedSpot.y.toString(), textStyle); + }).toList(); + +//// Provides a [Color] to show different background color for each touched spot +/// +/// You can override [LineTouchTooltipData.getTooltipColor], it gives you +/// [touchedSpot] object that touch happened on, then you should and pass your custom [Color] list +/// (length should be equal to the [touchedSpots.length]), to set background color +/// of tooltip popup. +typedef GetLineTooltipColor = Color Function( + LineBarSpot touchedSpot, +); + +/// Default implementation for [LineTouchTooltipData.getTooltipColor]. +Color defaultLineTooltipColor(LineBarSpot touchedSpot) => + Colors.blueGrey.darken(15); + +/// Represent a targeted spot inside a line bar. +class LineBarSpot extends FlSpot with EquatableMixin { + /// [bar] is the [LineChartBarData] that this spot is inside of, + /// [barIndex] is the index of our [bar], in the [LineChartData.lineBarsData] list, + /// [spot] is the targeted spot. + /// [spotIndex] is the index this [FlSpot], in the [LineChartBarData.spots] list. + LineBarSpot( + this.bar, + this.barIndex, + FlSpot spot, + ) : spotIndex = bar.spots.indexOf(spot), + super(spot.x, spot.y); + + /// Is the [LineChartBarData] that this spot is inside of. + final LineChartBarData bar; + + /// Is the index of our [bar], in the [LineChartData.lineBarsData] list, + final int barIndex; + + /// Is the index of our [super.spot], in the [LineChartBarData.spots] list. + final int spotIndex; + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + bar, + barIndex, + spotIndex, + x, + y, + ]; +} + +/// A [LineBarSpot] that holds information about the event that selected it +class TouchLineBarSpot extends LineBarSpot { + TouchLineBarSpot( + super.bar, + super.barIndex, + super.spot, + this.distance, + ); + + /// Distance in pixels from where the user taped + final double distance; +} + +/// Holds data of showing each row item in the tooltip popup. +class LineTooltipItem with EquatableMixin { + /// Shows a [text] with [textStyle], [textDirection], + /// and optional [children] as a row in the tooltip popup. + const LineTooltipItem( + this.text, + this.textStyle, { + this.textAlign = TextAlign.center, + this.textDirection = TextDirection.ltr, + this.children, + }); + + /// Showing text. + final String text; + + /// Style of showing text. + final TextStyle textStyle; + + /// Align of showing text. + final TextAlign textAlign; + + /// Direction of showing text. + final TextDirection textDirection; + + /// Add further style and format to the text of the tooltip + final List? children; + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + text, + textStyle, + textAlign, + textDirection, + children, + ]; +} + +/// details of showing indicator when touch happened on [LineChart] +/// [indicatorBelowLine] we draw a vertical line below of the touched spot +/// [touchedSpotDotData] we draw a larger dot on the touched spot to bold it +class TouchedSpotIndicatorData with EquatableMixin { + /// if [LineTouchData.handleBuiltInTouches] is true, + /// [LineChart] shows a thicker line and larger spot as indicator automatically when touch happens, + /// otherwise you can show it manually using [LineChartBarData.showingIndicators]. + /// [indicatorBelowLine] determines line's style, and + /// [touchedSpotDotData] determines dot's style. + const TouchedSpotIndicatorData( + this.indicatorBelowLine, + this.touchedSpotDotData, + ); + + /// Determines line's style. + final FlLine indicatorBelowLine; + + /// Determines dot's style. + final FlDotData touchedSpotDotData; + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + indicatorBelowLine, + touchedSpotDotData, + ]; +} + +/// Holds data for showing tooltips over a line +class ShowingTooltipIndicators with EquatableMixin { + /// [LineChart] shows some tooltips over each [LineChartBarData], + /// and [showingSpots] determines in which spots this tooltip should be shown. + const ShowingTooltipIndicators(this.showingSpots); + + /// Determines the spots that each tooltip should be shown. + final List showingSpots; + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [showingSpots]; +} + +/// Holds information about touch response in the [LineChart]. +/// +/// You can override [LineTouchData.touchCallback] to handle touch events, +/// it gives you a [LineTouchResponse] and you can do whatever you want. +class LineTouchResponse extends AxisBaseTouchResponse { + /// If touch happens, [LineChart] processes it internally and + /// passes out a list of [lineBarSpots] it gives you information about the touched spot. + /// They are sorted based on their distance to the touch event + LineTouchResponse({ + required super.touchLocation, + required super.touchChartCoordinate, + this.lineBarSpots, + }); + + /// touch happened on these spots + /// (if a single line provided on the chart, [lineBarSpots]'s length will be 1 always) + final List? lineBarSpots; + + /// Copies current [LineTouchResponse] to a new [LineTouchResponse], + /// and replaces provided values. + LineTouchResponse copyWith({ + Offset? touchLocation, + Offset? touchChartCoordinate, + List? lineBarSpots, + }) => + LineTouchResponse( + touchLocation: touchLocation ?? this.touchLocation, + touchChartCoordinate: touchChartCoordinate ?? this.touchChartCoordinate, + lineBarSpots: lineBarSpots ?? this.lineBarSpots, + ); +} + +/// It is the input of the [GetSpotRangeErrorPainter] callback in +/// the [LineChartData.errorIndicatorData] +/// +/// So it contains the information about the spot, and the bar that the spot +/// is in. The callback should return a [FlSpotErrorRangePainter] that will draw +/// the error bars +class LineChartSpotErrorRangeCallbackInput + extends FlSpotErrorRangeCallbackInput { + LineChartSpotErrorRangeCallbackInput({ + required this.spot, + required this.bar, + required this.spotIndex, + }); + + final FlSpot spot; + final LineChartBarData bar; + final int spotIndex; + + @override + List get props => [ + spot, + bar, + spotIndex, + ]; +} + +/// It lerps a [LineChartData] to another [LineChartData] (handles animation for updating values) +class LineChartDataTween extends Tween { + LineChartDataTween({required super.begin, required super.end}); + + /// Lerps a [LineChartData] based on [t] value, check [Tween.lerp]. + @override + LineChartData lerp(double t) => begin!.lerp(begin!, end!, t); +} diff --git a/lib/src/chart/line_chart/line_chart_helper.dart b/lib/src/chart/line_chart/line_chart_helper.dart new file mode 100644 index 0000000..63766ad --- /dev/null +++ b/lib/src/chart/line_chart/line_chart_helper.dart @@ -0,0 +1,61 @@ +import 'package:fl_chart/fl_chart.dart'; + +/// Contains anything that helps LineChart works +class LineChartHelper { + /// Calculates the [minX], [maxX], [minY], and [maxY] values of + /// the provided [lineBarsData]. + (double minX, double maxX, double minY, double maxY) calculateMaxAxisValues( + List lineBarsData, + ) { + if (lineBarsData.isEmpty) { + return (0, 0, 0, 0); + } + + final LineChartBarData lineBarData; + try { + lineBarData = + lineBarsData.firstWhere((element) => element.spots.isNotEmpty); + } catch (_) { + // There is no lineBarData with at least one spot + return (0, 0, 0, 0); + } + + final FlSpot firstValidSpot; + try { + firstValidSpot = + lineBarData.spots.firstWhere((element) => element != FlSpot.nullSpot); + } catch (_) { + // There is no valid spot + return (0, 0, 0, 0); + } + + var minX = firstValidSpot.x; + var maxX = firstValidSpot.x; + var minY = firstValidSpot.y; + var maxY = firstValidSpot.y; + + for (final barData in lineBarsData) { + if (barData.spots.isEmpty) { + continue; + } + + if (barData.mostRightSpot.x > maxX) { + maxX = barData.mostRightSpot.x; + } + + if (barData.mostLeftSpot.x < minX) { + minX = barData.mostLeftSpot.x; + } + + if (barData.mostTopSpot.y > maxY) { + maxY = barData.mostTopSpot.y; + } + + if (barData.mostBottomSpot.y < minY) { + minY = barData.mostBottomSpot.y; + } + } + + return (minX, maxX, minY, maxY); + } +} diff --git a/lib/src/chart/line_chart/line_chart_painter.dart b/lib/src/chart/line_chart/line_chart_painter.dart new file mode 100644 index 0000000..6f47aee --- /dev/null +++ b/lib/src/chart/line_chart/line_chart_painter.dart @@ -0,0 +1,1473 @@ +import 'dart:math'; + +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/base/axis_chart/axis_chart_extensions.dart'; +import 'package:fl_chart/src/chart/base/axis_chart/axis_chart_painter.dart'; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart'; +import 'package:fl_chart/src/extensions/paint_extension.dart'; +import 'package:fl_chart/src/extensions/path_extension.dart'; +import 'package:fl_chart/src/extensions/text_align_extension.dart'; +import 'package:fl_chart/src/utils/canvas_wrapper.dart'; +import 'package:fl_chart/src/utils/utils.dart'; +import 'package:flutter/material.dart'; + +/// Paints [LineChartData] in the canvas, it can be used in a [CustomPainter] +class LineChartPainter extends AxisChartPainter { + /// Paints [dataList] into canvas, it is the animating [LineChartData], + /// [targetData] is the animation's target and remains the same + /// during animation, then we should use it when we need to show + /// tooltips or something like that, because [dataList] is changing constantly. + /// + /// [textScale] used for scaling texts inside the chart, + /// parent can use [MediaQuery.textScaleFactor] to respect + /// the system's font size. + LineChartPainter() : super() { + _barPaint = Paint()..style = PaintingStyle.stroke; + + _barAreaPaint = Paint()..style = PaintingStyle.fill; + + _barAreaLinesPaint = Paint()..style = PaintingStyle.stroke; + + _clearBarAreaPaint = Paint() + ..style = PaintingStyle.fill + ..color = const Color(0x00000000) + ..blendMode = BlendMode.dstIn; + + _touchLinePaint = Paint() + ..style = PaintingStyle.stroke + ..color = Colors.black; + + _bgTouchTooltipPaint = Paint() + ..style = PaintingStyle.fill + ..color = Colors.white; + + _borderTouchTooltipPaint = Paint() + ..style = PaintingStyle.stroke + ..color = Colors.transparent + ..strokeWidth = 1.0; + + _clipPaint = Paint(); + } + + late Paint _barPaint; + late Paint _barAreaPaint; + late Paint _barAreaLinesPaint; + late Paint _clearBarAreaPaint; + late Paint _touchLinePaint; + late Paint _bgTouchTooltipPaint; + late Paint _borderTouchTooltipPaint; + late Paint _clipPaint; + + /// Paints [LineChartData] into the provided canvas. + @override + void paint( + BuildContext context, + CanvasWrapper canvasWrapper, + PaintHolder holder, + ) { + final data = holder.data; + if (holder.chartVirtualRect != null) { + canvasWrapper + ..saveLayer( + Offset.zero & canvasWrapper.size, + _clipPaint, + ) + ..clipRect(Offset.zero & canvasWrapper.size); + } + super.paint(context, canvasWrapper, holder); + if (data.lineBarsData.isEmpty) { + return; + } + + if (data.clipData.any && holder.chartVirtualRect == null) { + canvasWrapper.saveLayer( + Rect.fromLTWH( + 0, + -40, + canvasWrapper.size.width + 40, + canvasWrapper.size.height + 40, + ), + _clipPaint, + ); + + clipToBorder(canvasWrapper, holder); + } + + for (final betweenBarsData in data.betweenBarsData) { + drawBetweenBarsArea(canvasWrapper, data, betweenBarsData, holder); + } + + if (!data.extraLinesData.extraLinesOnTop) { + super.drawExtraLines(context, canvasWrapper, holder); + } + + final lineIndexDrawingInfo = []; + + /// draw each line independently on the chart + for (var i = 0; i < data.lineBarsData.length; i++) { + final barData = data.lineBarsData[i]; + + if (!barData.show) { + continue; + } + + drawBarLine(canvasWrapper, barData, holder); + drawDots(canvasWrapper, barData, holder); + + if (data.extraLinesData.extraLinesOnTop) { + super.drawExtraLines(context, canvasWrapper, holder); + } + + final indicatorsData = data.lineTouchData + .getTouchedSpotIndicator(barData, barData.showingIndicators); + + if (indicatorsData.length != barData.showingIndicators.length) { + throw Exception( + 'indicatorsData and touchedSpotOffsets size should be same', + ); + } + + for (var j = 0; j < barData.showingIndicators.length; j++) { + final indicatorData = indicatorsData[j]; + final index = barData.showingIndicators[j]; + if (index < 0 || index >= barData.spots.length) { + continue; + } + final spot = barData.spots[index]; + + if (indicatorData == null) { + continue; + } + lineIndexDrawingInfo.add( + LineIndexDrawingInfo(barData, i, spot, index, indicatorData), + ); + } + } + + drawTouchedSpotsIndicator(canvasWrapper, lineIndexDrawingInfo, holder); + + if (data.clipData.any || holder.chartVirtualRect != null) { + canvasWrapper.restore(); + } + + // Draw error indicators + for (var i = 0; i < data.lineBarsData.length; i++) { + final barData = data.lineBarsData[i]; + + if (!barData.show) { + continue; + } + + drawErrorIndicatorData( + canvasWrapper, + barData, + holder, + ); + } + + // Draw touch tooltip on most top spot + for (var i = 0; i < data.showingTooltipIndicators.length; i++) { + var tooltipSpots = data.showingTooltipIndicators[i]; + + final showingBarSpots = tooltipSpots.showingSpots; + if (showingBarSpots.isEmpty) { + continue; + } + final barSpots = List.of(showingBarSpots); + FlSpot topSpot = barSpots[0]; + for (final barSpot in barSpots) { + if (barSpot.y > topSpot.y) { + topSpot = barSpot; + } + } + tooltipSpots = ShowingTooltipIndicators(barSpots); + + drawTouchTooltip( + context, + canvasWrapper, + data.lineTouchData.touchTooltipData, + topSpot, + tooltipSpots, + holder, + ); + } + } + + @visibleForTesting + void clipToBorder( + CanvasWrapper canvasWrapper, + PaintHolder holder, + ) { + final data = holder.data; + final viewSize = canvasWrapper.size; + final clip = data.clipData; + final border = data.borderData.show ? data.borderData.border : null; + + var left = 0.0; + var top = 0.0; + var right = viewSize.width; + var bottom = viewSize.height; + + if (clip.left) { + final borderWidth = border?.left.width ?? 0; + left = borderWidth / 2; + } + if (clip.top) { + final borderWidth = border?.top.width ?? 0; + top = borderWidth / 2; + } + if (clip.right) { + final borderWidth = border?.right.width ?? 0; + right = viewSize.width - (borderWidth / 2); + } + if (clip.bottom) { + final borderWidth = border?.bottom.width ?? 0; + bottom = viewSize.height - (borderWidth / 2); + } + + canvasWrapper.clipRect(Rect.fromLTRB(left, top, right, bottom)); + } + + @visibleForTesting + void drawBarLine( + CanvasWrapper canvasWrapper, + LineChartBarData barData, + PaintHolder holder, + ) { + final viewSize = holder.getChartUsableSize(canvasWrapper.size); + + final barList = barData.spots.splitByNullSpots(); + + // paint each sublist that was built above + // bar is passed in separately from barData + // because barData is the whole line + // and bar is a piece of that line + for (final bar in barList) { + final barPath = generateBarPath(viewSize, barData, bar, holder); + + final belowBarPath = + generateBelowBarPath(viewSize, barData, barPath, bar, holder); + final completelyFillBelowBarPath = generateBelowBarPath( + viewSize, + barData, + barPath, + bar, + holder, + fillCompletely: true, + ); + final aboveBarPath = + generateAboveBarPath(viewSize, barData, barPath, bar, holder); + final completelyFillAboveBarPath = generateAboveBarPath( + viewSize, + barData, + barPath, + bar, + holder, + fillCompletely: true, + ); + + drawBelowBar( + canvasWrapper, + belowBarPath, + completelyFillAboveBarPath, + holder, + barData, + ); + drawAboveBar( + canvasWrapper, + aboveBarPath, + completelyFillBelowBarPath, + holder, + barData, + ); + drawBarShadow(canvasWrapper, barPath, barData); + drawBar(canvasWrapper, barPath, barData, holder); + } + } + + @visibleForTesting + void drawBetweenBarsArea( + CanvasWrapper canvasWrapper, + LineChartData data, + BetweenBarsData betweenBarsData, + PaintHolder holder, + ) { + final viewSize = canvasWrapper.size; + final fromBarData = data.lineBarsData[betweenBarsData.fromIndex]; + final toBarData = data.lineBarsData[betweenBarsData.toIndex]; + + final fromBarSplitLines = fromBarData.spots.splitByNullSpots(); + final toBarSplitLines = toBarData.spots.splitByNullSpots(); + + if (fromBarSplitLines.length != toBarSplitLines.length) { + throw ArgumentError( + 'Cannot draw betWeenBarsArea when null spots are inconsistent.', + ); + } + + for (var i = 0; i < fromBarSplitLines.length; i++) { + final fromSpots = fromBarSplitLines[i]; + final toSpots = toBarSplitLines[i].reversed.toList(); + + final fromBarPath = generateBarPath( + viewSize, + fromBarData, + fromSpots, + holder, + ); + final barPath = generateBarPath( + viewSize, + toBarData.copyWith(spots: toSpots), + toSpots, + holder, + appendToPath: fromBarPath, + ); + final left = min(fromBarData.mostLeftSpot.x, toBarData.mostLeftSpot.x); + final top = max(fromBarData.mostTopSpot.y, toBarData.mostTopSpot.y); + final right = max(fromBarData.mostRightSpot.x, toBarData.mostRightSpot.x); + final bottom = min( + fromBarData.mostBottomSpot.y, + toBarData.mostBottomSpot.y, + ); + final aroundRect = Rect.fromLTRB( + getPixelX(left, viewSize, holder), + getPixelY(top, viewSize, holder), + getPixelX(right, viewSize, holder), + getPixelY(bottom, viewSize, holder), + ); + + drawBetweenBar( + canvasWrapper, + barPath, + betweenBarsData, + aroundRect, + holder, + ); + } + } + + @visibleForTesting + void drawDots( + CanvasWrapper canvasWrapper, + LineChartBarData barData, + PaintHolder holder, + ) { + if (!barData.dotData.show || barData.spots.isEmpty) { + return; + } + final viewSize = canvasWrapper.size; + + final barXDelta = getBarLineXLength(barData, viewSize, holder); + + for (var i = 0; i < barData.spots.length; i++) { + final spot = barData.spots[i]; + if (spot.isNotNull() && barData.dotData.checkToShowDot(spot, barData)) { + final x = getPixelX(spot.x, viewSize, holder); + final y = getPixelY(spot.y, viewSize, holder); + final xPercentInLine = (x / barXDelta) * 100; + final painter = + barData.dotData.getDotPainter(spot, xPercentInLine, barData, i); + + canvasWrapper.drawDot(painter, spot, Offset(x, y)); + } + } + } + + @visibleForTesting + void drawErrorIndicatorData( + CanvasWrapper canvasWrapper, + LineChartBarData barData, + PaintHolder holder, + ) { + final errorIndicatorData = barData.errorIndicatorData; + if (!errorIndicatorData.show) { + return; + } + + final viewSize = canvasWrapper.size; + + for (var i = 0; i < barData.spots.length; i++) { + final spot = barData.spots[i]; + if (spot.isNotNull()) { + final x = getPixelX(spot.x, viewSize, holder); + final y = getPixelY(spot.y, viewSize, holder); + if (spot.xError == null && spot.yError == null) { + continue; + } + + var left = 0.0; + var right = 0.0; + if (spot.xError != null) { + left = getPixelX(spot.x - spot.xError!.lowerBy, viewSize, holder) - x; + right = + getPixelX(spot.x + spot.xError!.upperBy, viewSize, holder) - x; + } + + var top = 0.0; + var bottom = 0.0; + if (spot.yError != null) { + top = getPixelY(spot.y + spot.yError!.lowerBy, viewSize, holder) - y; + bottom = + getPixelY(spot.y - spot.yError!.upperBy, viewSize, holder) - y; + } + final relativeErrorPixelsRect = Rect.fromLTRB( + left, + top, + right, + bottom, + ); + + final painter = errorIndicatorData.painter( + LineChartSpotErrorRangeCallbackInput( + spot: spot, + bar: barData, + spotIndex: i, + ), + ); + canvasWrapper.drawErrorIndicator( + painter, + spot, + Offset(x, y), + relativeErrorPixelsRect, + holder.data, + ); + } + } + } + + @visibleForTesting + void drawTouchedSpotsIndicator( + CanvasWrapper canvasWrapper, + List lineIndexDrawingInfo, + PaintHolder holder, + ) { + if (lineIndexDrawingInfo.isEmpty) { + return; + } + final viewSize = canvasWrapper.size; + + lineIndexDrawingInfo.sort((a, b) => b.spot.y.compareTo(a.spot.y)); + + for (final info in lineIndexDrawingInfo) { + final barData = info.line; + final barXDelta = getBarLineXLength(barData, viewSize, holder); + + final data = holder.data; + + final index = info.spotIndex; + final spot = info.spot; + final indicatorData = info.indicatorData; + + final touchedSpot = Offset( + getPixelX(spot.x, viewSize, holder), + getPixelY(spot.y, viewSize, holder), + ); + + /// For drawing the dot + final showingDots = indicatorData.touchedSpotDotData.show; + var dotHeight = 0.0; + late FlDotPainter dotPainter; + + if (showingDots) { + final xPercentInLine = (touchedSpot.dx / barXDelta) * 100; + dotPainter = indicatorData.touchedSpotDotData + .getDotPainter(spot, xPercentInLine, barData, index); + dotHeight = dotPainter.getSize(spot).height; + } + + /// For drawing the indicator line + final lineStartY = min( + data.maxY, + max(data.minY, data.lineTouchData.getTouchLineStart(barData, index)), + ); + final lineEndY = min( + data.maxY, + max(data.minY, data.lineTouchData.getTouchLineEnd(barData, index)), + ); + final lineStart = + Offset(touchedSpot.dx, getPixelY(lineStartY, viewSize, holder)); + var lineEnd = + Offset(touchedSpot.dx, getPixelY(lineEndY, viewSize, holder)); + + /// If line end is inside the dot, adjust it so that it doesn't overlap with the dot. + final dotMinY = touchedSpot.dy - dotHeight / 2; + final dotMaxY = touchedSpot.dy + dotHeight / 2; + if (lineEnd.dy > dotMinY && lineEnd.dy < dotMaxY) { + if (lineStart.dy < lineEnd.dy) { + lineEnd -= Offset(0, lineEnd.dy - dotMinY); + } else { + lineEnd += Offset(0, dotMaxY - lineEnd.dy); + } + } + + final indicatorLine = indicatorData.indicatorBelowLine; + _touchLinePaint + ..setColorOrGradientForLine( + indicatorLine.color, + indicatorLine.gradient, + from: lineStart, + to: lineEnd, + ) + ..strokeWidth = indicatorLine.strokeWidth + ..transparentIfWidthIsZero(); + + canvasWrapper.drawDashedLine( + lineStart, + lineEnd, + _touchLinePaint, + indicatorLine.dashArray, + ); + + /// Draw the indicator dot + if (showingDots) { + canvasWrapper.drawDot(dotPainter, spot, touchedSpot); + } + } + } + + /// Generates a path, based on [LineChartBarData.isStepChart] for step style, and normal style. + @visibleForTesting + Path generateBarPath( + Size viewSize, + LineChartBarData barData, + List barSpots, + PaintHolder holder, { + Path? appendToPath, + }) { + if (barData.isStepLineChart) { + return generateStepBarPath( + viewSize, + barData, + barSpots, + holder, + appendToPath: appendToPath, + ); + } else { + return generateNormalBarPath( + viewSize, + barData, + barSpots, + holder, + appendToPath: appendToPath, + ); + } + } + + /// firstly we generate the bar line that we should draw, + /// then we reuse it to fill below bar space. + /// there is two type of barPath that generate here, + /// first one is the sharp corners line on spot connections + /// second one is curved corners line on spot connections, + /// and we use isCurved to find out how we should generate it, + /// If you want to concatenate paths together for creating an area between + /// multiple bars for example, you can pass the appendToPath + @visibleForTesting + Path generateNormalBarPath( + Size viewSize, + LineChartBarData barData, + List barSpots, + PaintHolder holder, { + Path? appendToPath, + }) { + final path = appendToPath ?? Path(); + final size = barSpots.length; + + var temp = Offset.zero; + + final x = getPixelX(barSpots[0].x, viewSize, holder); + final y = getPixelY(barSpots[0].y, viewSize, holder); + if (appendToPath == null) { + path.moveTo(x, y); + if (size == 1) { + path.lineTo(x, y); + } + } else { + path.lineTo(x, y); + } + for (var i = 1; i < size; i++) { + /// CurrentSpot + final current = Offset( + getPixelX(barSpots[i].x, viewSize, holder), + getPixelY(barSpots[i].y, viewSize, holder), + ); + + /// previous spot + final previous = Offset( + getPixelX(barSpots[i - 1].x, viewSize, holder), + getPixelY(barSpots[i - 1].y, viewSize, holder), + ); + + /// next point + final next = Offset( + getPixelX(barSpots[i + 1 < size ? i + 1 : i].x, viewSize, holder), + getPixelY(barSpots[i + 1 < size ? i + 1 : i].y, viewSize, holder), + ); + + final controlPoint1 = previous + temp; + + /// if the isCurved is false, we set 0 for smoothness, + /// it means we should not have any smoothness then we face with + /// the sharped corners line + final smoothness = barData.isCurved ? barData.curveSmoothness : 0.0; + temp = ((next - previous) / 2) * smoothness; + + if (barData.preventCurveOverShooting) { + if ((next - current).dy <= barData.preventCurveOvershootingThreshold || + (current - previous).dy <= + barData.preventCurveOvershootingThreshold) { + temp = Offset(temp.dx, 0); + } + + if ((next - current).dx <= barData.preventCurveOvershootingThreshold || + (current - previous).dx <= + barData.preventCurveOvershootingThreshold) { + temp = Offset(0, temp.dy); + } + } + + final controlPoint2 = current - temp; + + path.cubicTo( + controlPoint1.dx, + controlPoint1.dy, + controlPoint2.dx, + controlPoint2.dy, + current.dx, + current.dy, + ); + } + + return path; + } + + /// generates a `Step Line Chart` bar style path. + @visibleForTesting + Path generateStepBarPath( + Size viewSize, + LineChartBarData barData, + List barSpots, + PaintHolder holder, { + Path? appendToPath, + }) { + final path = appendToPath ?? Path(); + final size = barSpots.length; + + final x = getPixelX(barSpots[0].x, viewSize, holder); + final y = getPixelY(barSpots[0].y, viewSize, holder); + if (appendToPath == null) { + path.moveTo(x, y); + } else { + path.lineTo(x, y); + } + for (var i = 0; i < size; i++) { + /// CurrentSpot + final current = Offset( + getPixelX(barSpots[i].x, viewSize, holder), + getPixelY(barSpots[i].y, viewSize, holder), + ); + + /// next point + final next = Offset( + getPixelX(barSpots[i + 1 < size ? i + 1 : i].x, viewSize, holder), + getPixelY(barSpots[i + 1 < size ? i + 1 : i].y, viewSize, holder), + ); + + final stepDirection = barData.lineChartStepData.stepDirection; + + // middle + if (current.dy == next.dy) { + path.lineTo(next.dx, next.dy); + } else { + final deltaX = next.dx - current.dx; + + path + ..lineTo(current.dx + deltaX - (deltaX * stepDirection), current.dy) + ..lineTo(current.dx + deltaX - (deltaX * stepDirection), next.dy) + ..lineTo(next.dx, next.dy); + } + } + + return path; + } + + /// it generates below area path using a copy of [barPath], + /// if cutOffY is provided by the [BarAreaData], it cut the area to the provided cutOffY value, + /// if [fillCompletely] is true, the cutOffY will be ignored, + /// and a completely filled path will return, + @visibleForTesting + Path generateBelowBarPath( + Size viewSize, + LineChartBarData barData, + Path barPath, + List barSpots, + PaintHolder holder, { + bool fillCompletely = false, + }) { + final belowBarPath = Path.from(barPath); + + /// Line To Bottom Right + var x = getPixelX(barSpots[barSpots.length - 1].x, viewSize, holder); + double y; + if (!fillCompletely && barData.belowBarData.applyCutOffY) { + y = getPixelY(barData.belowBarData.cutOffY, viewSize, holder); + } else { + y = viewSize.height; + } + belowBarPath.lineTo(x, y); + + /// Line To Bottom Left + x = getPixelX(barSpots[0].x, viewSize, holder); + if (!fillCompletely && barData.belowBarData.applyCutOffY) { + y = getPixelY(barData.belowBarData.cutOffY, viewSize, holder); + } else { + y = viewSize.height; + } + belowBarPath.lineTo(x, y); + + /// Line To Top Left + x = getPixelX(barSpots[0].x, viewSize, holder); + y = getPixelY(barSpots[0].y, viewSize, holder); + belowBarPath + ..lineTo(x, y) + ..close(); + + return belowBarPath; + } + + /// it generates above area path using a copy of [barPath], + /// if cutOffY is provided by the [BarAreaData], it cut the area to the provided cutOffY value, + /// if [fillCompletely] is true, the cutOffY will be ignored, + /// and a completely filled path will return, + @visibleForTesting + Path generateAboveBarPath( + Size viewSize, + LineChartBarData barData, + Path barPath, + List barSpots, + PaintHolder holder, { + bool fillCompletely = false, + }) { + final aboveBarPath = Path.from(barPath); + + /// Line To Top Right + var x = getPixelX(barSpots[barSpots.length - 1].x, viewSize, holder); + double y; + if (!fillCompletely && barData.aboveBarData.applyCutOffY) { + y = getPixelY(barData.aboveBarData.cutOffY, viewSize, holder); + } else { + y = 0.0; + } + aboveBarPath.lineTo(x, y); + + /// Line To Top Left + x = getPixelX(barSpots[0].x, viewSize, holder); + if (!fillCompletely && barData.aboveBarData.applyCutOffY) { + y = getPixelY(barData.aboveBarData.cutOffY, viewSize, holder); + } else { + y = 0.0; + } + aboveBarPath.lineTo(x, y); + + /// Line To Bottom Left + x = getPixelX(barSpots[0].x, viewSize, holder); + y = getPixelY(barSpots[0].y, viewSize, holder); + aboveBarPath + ..lineTo(x, y) + ..close(); + + return aboveBarPath; + } + + /// firstly we draw [belowBarPath], then if cutOffY value is provided in [BarAreaData], + /// [belowBarPath] maybe draw over the main bar line, + /// then to fix the problem we use [filledAboveBarPath] to clear the above section from this draw. + @visibleForTesting + void drawBelowBar( + CanvasWrapper canvasWrapper, + Path belowBarPath, + Path filledAboveBarPath, + PaintHolder holder, + LineChartBarData barData, + ) { + if (!barData.belowBarData.show) { + return; + } + + final viewSize = canvasWrapper.size; + + final belowBarLargestRect = Rect.fromLTRB( + getPixelX(barData.mostLeftSpot.x, viewSize, holder), + getPixelY(barData.mostTopSpot.y, viewSize, holder), + getPixelX(barData.mostRightSpot.x, viewSize, holder), + viewSize.height, + ); + + final belowBar = barData.belowBarData; + _barAreaPaint.setColorOrGradient( + belowBar.color, + belowBar.gradient, + belowBarLargestRect, + ); + + if (barData.belowBarData.applyCutOffY) { + canvasWrapper.saveLayer( + Rect.fromLTWH(0, 0, viewSize.width, viewSize.height), + _clipPaint, + ); + } + + canvasWrapper.drawPath(belowBarPath, _barAreaPaint); + + // clear the above area that get out of the bar line + if (barData.belowBarData.applyCutOffY) { + canvasWrapper + ..drawPath(filledAboveBarPath, _clearBarAreaPaint) + ..restore(); + } + + /// draw below spots line + if (barData.belowBarData.spotsLine.show) { + for (final spot in barData.spots) { + if (barData.belowBarData.spotsLine.checkToShowSpotLine(spot)) { + final from = Offset( + getPixelX(spot.x, viewSize, holder), + getPixelY(spot.y, viewSize, holder), + ); + + Offset to; + + // Check applyCutOffY + if (barData.belowBarData.spotsLine.applyCutOffY && + barData.belowBarData.applyCutOffY) { + to = Offset( + getPixelX(spot.x, viewSize, holder), + getPixelY(barData.belowBarData.cutOffY, viewSize, holder), + ); + } else { + to = Offset( + getPixelX(spot.x, viewSize, holder), + viewSize.height, + ); + } + + final lineStyle = barData.belowBarData.spotsLine.flLineStyle; + _barAreaLinesPaint + ..setColorOrGradientForLine( + lineStyle.color, + lineStyle.gradient, + from: from, + to: to, + ) + ..strokeWidth = lineStyle.strokeWidth + ..transparentIfWidthIsZero(); + + canvasWrapper.drawDashedLine( + from, + to, + _barAreaLinesPaint, + lineStyle.dashArray, + ); + } + } + } + } + + /// firstly we draw [aboveBarPath], then if cutOffY value is provided in [BarAreaData], + /// [aboveBarPath] maybe draw over the main bar line, + /// then to fix the problem we use [filledBelowBarPath] to clear the above section from this draw. + @visibleForTesting + void drawAboveBar( + CanvasWrapper canvasWrapper, + Path aboveBarPath, + Path filledBelowBarPath, + PaintHolder holder, + LineChartBarData barData, + ) { + if (!barData.aboveBarData.show) { + return; + } + + final viewSize = canvasWrapper.size; + + final aboveBarLargestRect = Rect.fromLTRB( + getPixelX(barData.mostLeftSpot.x, viewSize, holder), + 0, + getPixelX(barData.mostRightSpot.x, viewSize, holder), + getPixelY(barData.mostBottomSpot.y, viewSize, holder), + ); + + final aboveBar = barData.aboveBarData; + _barAreaPaint.setColorOrGradient( + aboveBar.color, + aboveBar.gradient, + aboveBarLargestRect, + ); + + if (barData.aboveBarData.applyCutOffY) { + canvasWrapper.saveLayer( + Rect.fromLTWH(0, 0, viewSize.width, viewSize.height), + _clipPaint, + ); + } + + canvasWrapper.drawPath(aboveBarPath, _barAreaPaint); + + // clear the above area that get out of the bar line + if (barData.aboveBarData.applyCutOffY) { + canvasWrapper + ..drawPath(filledBelowBarPath, _clearBarAreaPaint) + ..restore(); + } + + /// draw above spots line + if (barData.aboveBarData.spotsLine.show) { + for (final spot in barData.spots) { + if (barData.aboveBarData.spotsLine.checkToShowSpotLine(spot)) { + final from = Offset( + getPixelX(spot.x, viewSize, holder), + getPixelY(spot.y, viewSize, holder), + ); + + Offset to; + + // Check applyCutOffY + if (barData.aboveBarData.spotsLine.applyCutOffY && + barData.aboveBarData.applyCutOffY) { + to = Offset( + getPixelX(spot.x, viewSize, holder), + getPixelY(barData.aboveBarData.cutOffY, viewSize, holder), + ); + } else { + to = Offset( + getPixelX(spot.x, viewSize, holder), + 0, + ); + } + + final lineStyle = barData.aboveBarData.spotsLine.flLineStyle; + _barAreaLinesPaint + ..setColorOrGradientForLine( + lineStyle.color, + lineStyle.gradient, + from: from, + to: to, + ) + ..strokeWidth = lineStyle.strokeWidth + ..transparentIfWidthIsZero(); + + canvasWrapper.drawDashedLine( + from, + to, + _barAreaLinesPaint, + lineStyle.dashArray, + ); + } + } + } + } + + @visibleForTesting + void drawBetweenBar( + CanvasWrapper canvasWrapper, + Path barPath, + BetweenBarsData betweenBarsData, + Rect aroundRect, + PaintHolder holder, + ) { + final viewSize = canvasWrapper.size; + + _barAreaPaint.setColorOrGradient( + betweenBarsData.color, + betweenBarsData.gradient, + aroundRect, + ); + + canvasWrapper + ..saveLayer( + Rect.fromLTWH(0, 0, viewSize.width, viewSize.height), + _clipPaint, + ) + ..drawPath(barPath, _barAreaPaint) + ..restore(); // clear the above area that get out of the bar line + } + + /// draw the main bar line's shadow by the [barPath] + @visibleForTesting + void drawBarShadow( + CanvasWrapper canvasWrapper, + Path barPath, + LineChartBarData barData, + ) { + if (!barData.show || barData.shadow.color.a == 0.0) { + return; + } + if (barPath.computeMetrics().isEmpty) { + return; + } + + _barPaint + ..strokeCap = barData.isStrokeCapRound ? StrokeCap.round : StrokeCap.butt + ..strokeJoin = + barData.isStrokeJoinRound ? StrokeJoin.round : StrokeJoin.miter + ..color = barData.shadow.color + ..shader = null + ..strokeWidth = barData.barWidth + ..color = barData.shadow.color + ..maskFilter = MaskFilter.blur( + BlurStyle.normal, + Utils().convertRadiusToSigma(barData.shadow.blurRadius), + ); + + barPath = barPath.toDashedPath(barData.dashArray); + + barPath = barPath.shift(barData.shadow.offset); + + canvasWrapper.drawPath( + barPath, + _barPaint, + ); + } + + /// draw the main bar line by the [barPath] + @visibleForTesting + void drawBar( + CanvasWrapper canvasWrapper, + Path barPath, + LineChartBarData barData, + PaintHolder holder, + ) { + if (!barData.show) { + return; + } + final viewSize = canvasWrapper.size; + + _barPaint + ..strokeCap = barData.isStrokeCapRound ? StrokeCap.round : StrokeCap.butt + ..strokeJoin = + barData.isStrokeJoinRound ? StrokeJoin.round : StrokeJoin.miter; + + final rectAroundTheLine = Rect.fromLTRB( + getPixelX(barData.mostLeftSpot.x, viewSize, holder), + getPixelY(barData.mostTopSpot.y, viewSize, holder), + getPixelX(barData.mostRightSpot.x, viewSize, holder), + getPixelY(barData.mostBottomSpot.y, viewSize, holder), + ); + _barPaint + ..setColorOrGradient( + barData.color, + barData.gradient, + barData.gradientArea == LineChartGradientArea.wholeChart + ? Offset.zero & viewSize + : rectAroundTheLine, + ) + ..maskFilter = null + ..strokeWidth = barData.barWidth + ..transparentIfWidthIsZero(); + + barPath = barPath.toDashedPath(barData.dashArray); + canvasWrapper.drawPath(barPath, _barPaint); + } + + @visibleForTesting + void drawTouchTooltip( + BuildContext context, + CanvasWrapper canvasWrapper, + LineTouchTooltipData tooltipData, + FlSpot showOnSpot, + ShowingTooltipIndicators showingTooltipSpots, + PaintHolder holder, + ) { + final viewSize = canvasWrapper.size; + + const textsBelowMargin = 4; + + // Get the dot height if available + final dotHeight = _getDotHeight( + viewSize: viewSize, + holder: holder, + showingTooltipSpots: showingTooltipSpots.showingSpots, + ); + + /// creating TextPainters to calculate the width and height of the tooltip + final drawingTextPainters = []; + + final tooltipItems = + tooltipData.getTooltipItems(showingTooltipSpots.showingSpots); + if (tooltipItems.length != showingTooltipSpots.showingSpots.length) { + throw Exception('tooltipItems and touchedSpots size should be same'); + } + + for (var i = 0; i < showingTooltipSpots.showingSpots.length; i++) { + var tooltipItem = tooltipItems[i]; + if (holder.data.rotationQuarterTurns % 4 == 2) { + tooltipItem = tooltipItems[tooltipItems.length - 1 - i]; + } + if (tooltipItem == null) { + continue; + } + + final span = TextSpan( + style: Utils().getThemeAwareTextStyle(context, tooltipItem.textStyle), + text: tooltipItem.text, + children: tooltipItem.children, + ); + + final tp = TextPainter( + text: span, + textAlign: tooltipItem.textAlign, + textDirection: tooltipItem.textDirection, + textScaler: holder.textScaler, + )..layout(maxWidth: tooltipData.maxContentWidth); + drawingTextPainters.add(tp); + } + if (drawingTextPainters.isEmpty) { + return; + } + + /// biggerWidth + /// some texts maybe larger, then we should + /// draw the tooltip' width as wide as biggerWidth + /// + /// sumTextsHeight + /// sum up all Texts height, then we should + /// draw the tooltip's height as tall as sumTextsHeight + var biggerWidth = 0.0; + var sumTextsHeight = 0.0; + for (final tp in drawingTextPainters) { + if (tp.width > biggerWidth) { + biggerWidth = tp.width; + } + sumTextsHeight += tp.height; + } + sumTextsHeight += (drawingTextPainters.length - 1) * textsBelowMargin; + + /// if we have multiple bar lines, + /// there are more than one FlCandidate on touch area, + /// we should get the most top FlSpot Offset to draw the tooltip on top of it + final mostTopOffset = Offset( + getPixelX(showOnSpot.x, viewSize, holder), + getPixelY(showOnSpot.y, viewSize, holder), + ); + + // Create an extended boundary that includes the center of the dot + final extendedBoundary = (Offset.zero & viewSize).inflate(dotHeight / 2); + + final isZoomed = holder.chartVirtualRect != null; + if (isZoomed && !extendedBoundary.contains(mostTopOffset)) { + return; + } + + final tooltipWidth = biggerWidth + tooltipData.tooltipPadding.horizontal; + final tooltipHeight = sumTextsHeight + tooltipData.tooltipPadding.vertical; + + double tooltipTopPosition; + if (tooltipData.showOnTopOfTheChartBoxArea) { + tooltipTopPosition = 0 - tooltipHeight - tooltipData.tooltipMargin; + } else { + tooltipTopPosition = + mostTopOffset.dy - tooltipHeight - tooltipData.tooltipMargin; + } + + final tooltipLeftPosition = getTooltipLeft( + mostTopOffset.dx, + tooltipWidth, + tooltipData.tooltipHorizontalAlignment, + tooltipData.tooltipHorizontalOffset, + ); + + /// draw the background rect with rounded radius + var rect = Rect.fromLTWH( + tooltipLeftPosition, + tooltipTopPosition, + tooltipWidth, + tooltipHeight, + ); + + if (tooltipData.fitInsideHorizontally) { + if (rect.left < 0) { + final shiftAmount = 0 - rect.left; + rect = Rect.fromLTRB( + rect.left + shiftAmount, + rect.top, + rect.right + shiftAmount, + rect.bottom, + ); + } + + if (rect.right > viewSize.width) { + final shiftAmount = rect.right - viewSize.width; + rect = Rect.fromLTRB( + rect.left - shiftAmount, + rect.top, + rect.right - shiftAmount, + rect.bottom, + ); + } + } + + if (tooltipData.fitInsideVertically) { + if (rect.top < 0) { + final shiftAmount = 0 - rect.top; + rect = Rect.fromLTRB( + rect.left, + rect.top + shiftAmount, + rect.right, + rect.bottom + shiftAmount, + ); + } + + if (rect.bottom > viewSize.height) { + final shiftAmount = rect.bottom - viewSize.height; + rect = Rect.fromLTRB( + rect.left, + rect.top - shiftAmount, + rect.right, + rect.bottom - shiftAmount, + ); + } + } + + final roundedRect = RRect.fromRectAndCorners( + rect, + topLeft: tooltipData.tooltipBorderRadius.topLeft, + topRight: tooltipData.tooltipBorderRadius.topRight, + bottomLeft: tooltipData.tooltipBorderRadius.bottomLeft, + bottomRight: tooltipData.tooltipBorderRadius.bottomRight, + ); + + var topSpot = showingTooltipSpots.showingSpots[0]; + for (final barSpot in showingTooltipSpots.showingSpots) { + if (barSpot.y > topSpot.y) { + topSpot = barSpot; + } + } + + _bgTouchTooltipPaint.color = tooltipData.getTooltipColor(topSpot); + + final rotateAngle = tooltipData.rotateAngle; + final rectRotationOffset = + Offset(0, Utils().calculateRotationOffset(rect.size, rotateAngle).dy); + final rectDrawOffset = Offset(roundedRect.left, roundedRect.top); + + final textRotationOffset = + Utils().calculateRotationOffset(rect.size, rotateAngle); + + if (tooltipData.tooltipBorder != BorderSide.none) { + _borderTouchTooltipPaint + ..color = tooltipData.tooltipBorder.color + ..strokeWidth = tooltipData.tooltipBorder.width; + } + final reverseQuarterTurnsAngle = -holder.data.rotationQuarterTurns * 90; + canvasWrapper.drawRotated( + size: rect.size, + rotationOffset: rectRotationOffset, + drawOffset: rectDrawOffset, + angle: reverseQuarterTurnsAngle + rotateAngle, + drawCallback: () { + canvasWrapper + ..drawRRect(roundedRect, _bgTouchTooltipPaint) + ..drawRRect(roundedRect, _borderTouchTooltipPaint); + }, + ); + + /// draw the texts one by one in below of each other + var topPosSeek = tooltipData.tooltipPadding.top; + for (final tp in drawingTextPainters) { + final yOffset = rect.topCenter.dy + + topPosSeek - + textRotationOffset.dy + + rectRotationOffset.dy; + + final align = tp.textAlign.getFinalHorizontalAlignment(tp.textDirection); + final xOffset = switch (align) { + HorizontalAlignment.left => rect.left + tooltipData.tooltipPadding.left, + HorizontalAlignment.right => + rect.right - tooltipData.tooltipPadding.right - tp.width, + _ => rect.center.dx - (tp.width / 2), + }; + + final drawOffset = Offset( + xOffset, + yOffset, + ); + + final reverseQuarterTurnsAngle = -holder.data.rotationQuarterTurns * 90; + canvasWrapper.drawRotated( + size: rect.size, + rotationOffset: rectRotationOffset, + drawOffset: rectDrawOffset, + angle: reverseQuarterTurnsAngle + rotateAngle, + drawCallback: () { + canvasWrapper.drawText(tp, drawOffset); + }, + ); + topPosSeek += tp.height; + topPosSeek += textsBelowMargin; + } + } + + @visibleForTesting + double getBarLineXLength( + LineChartBarData barData, + Size chartUsableSize, + PaintHolder holder, + ) { + if (barData.spots.isEmpty) { + return 0; + } + + final firstSpot = barData.spots[0]; + final firstSpotX = getPixelX(firstSpot.x, chartUsableSize, holder); + + final lastSpot = barData.spots[barData.spots.length - 1]; + final lastSpotX = getPixelX(lastSpot.x, chartUsableSize, holder); + + return lastSpotX - firstSpotX; + } + + /// Makes a [LineTouchResponse] based on the provided [localPosition] + /// + /// Processes [localPosition] and checks + /// the elements of the chart that are near the offset, + /// then makes a [LineTouchResponse] from the elements that has been touched. + List? handleTouch( + Offset localPosition, + Size size, + PaintHolder holder, + ) { + final data = holder.data; + final viewSize = holder.getChartUsableSize(size); + + final isZoomed = holder.chartVirtualRect != null; + if (isZoomed && !size.contains(localPosition)) { + return null; + } + + /// it holds list of nearest touched spots of each line + /// and we use it to draw touch stuff on them + final touchedSpots = []; + + /// draw each line independently on the chart + for (var i = 0; i < data.lineBarsData.length; i++) { + final barData = data.lineBarsData[i]; + + // find the nearest spot on touch area in this bar line + final foundTouchedSpot = getNearestTouchedSpot( + viewSize, + localPosition, + barData, + i, + holder, + ); + + if (foundTouchedSpot != null) { + touchedSpots.add(foundTouchedSpot); + } + } + + touchedSpots.sort((a, b) => a.distance.compareTo(b.distance)); + + return touchedSpots.isEmpty ? null : touchedSpots; + } + + /// find the nearest spot base on the touched offset + @visibleForTesting + TouchLineBarSpot? getNearestTouchedSpot( + Size viewSize, + Offset touchedPoint, + LineChartBarData barData, + int barDataPosition, + PaintHolder holder, + ) { + final data = holder.data; + if (!barData.show) { + return null; + } + + /// Find the nearest spot (based on distanceCalculator) + final sortedSpots = []; + double? smallestDistance; + for (final spot in barData.spots) { + if (spot.isNull()) continue; + final distance = data.lineTouchData.distanceCalculator( + touchedPoint, + Offset( + getPixelX(spot.x, viewSize, holder), + getPixelY(spot.y, viewSize, holder), + ), + ); + + if (distance <= data.lineTouchData.touchSpotThreshold) { + smallestDistance ??= distance; + + if (distance < smallestDistance) { + sortedSpots.insert(0, spot); + smallestDistance = distance; + } else { + sortedSpots.add(spot); + } + } + } + + if (sortedSpots.isNotEmpty) { + return TouchLineBarSpot( + barData, + barDataPosition, + sortedSpots.first, + smallestDistance!, + ); + } else { + return null; + } + } + + // Get the height of the dot for the given showingTooltipSpots + double _getDotHeight({ + required Size viewSize, + required PaintHolder holder, + required List showingTooltipSpots, + }) { + double? dotHeight; + for (final info in showingTooltipSpots) { + // Find the corresponding indicator data for this spot + final lineData = holder.data.lineBarsData.elementAtOrNull(info.barIndex); + if (lineData == null) continue; + + final indicators = holder.data.lineTouchData + .getTouchedSpotIndicator(lineData, [info.spotIndex]); + + final indicatorData = indicators.elementAtOrNull(0); + if (indicatorData != null && indicatorData.touchedSpotDotData.show) { + final xPercentInLine = (getPixelX(info.x, viewSize, holder) / + getBarLineXLength(lineData, viewSize, holder)) * + 100; + final dotPainter = indicatorData.touchedSpotDotData + .getDotPainter(info, xPercentInLine, lineData, info.spotIndex); + final currentDotHeight = dotPainter.getSize(info).height; + + // Keep the largest dot height + if (dotHeight == null || currentDotHeight > dotHeight) { + dotHeight = currentDotHeight; + } + } + } + return dotHeight ?? 0; + } +} + +@visibleForTesting +class LineIndexDrawingInfo { + LineIndexDrawingInfo( + this.line, + this.lineIndex, + this.spot, + this.spotIndex, + this.indicatorData, + ); + + final LineChartBarData line; + final int lineIndex; + final FlSpot spot; + final int spotIndex; + final TouchedSpotIndicatorData indicatorData; +} diff --git a/lib/src/chart/line_chart/line_chart_renderer.dart b/lib/src/chart/line_chart/line_chart_renderer.dart new file mode 100644 index 0000000..83e0a96 --- /dev/null +++ b/lib/src/chart/line_chart/line_chart_renderer.dart @@ -0,0 +1,141 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart'; +import 'package:fl_chart/src/chart/base/base_chart/render_base_chart.dart'; +import 'package:fl_chart/src/chart/line_chart/line_chart_painter.dart'; +import 'package:fl_chart/src/utils/canvas_wrapper.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +// coverage:ignore-start + +/// Low level LineChart Widget. +class LineChartLeaf extends LeafRenderObjectWidget { + const LineChartLeaf({ + super.key, + required this.data, + required this.targetData, + required this.canBeScaled, + required this.chartVirtualRect, + }); + + final LineChartData data; + final LineChartData targetData; + final Rect? chartVirtualRect; + final bool canBeScaled; + + @override + RenderLineChart createRenderObject(BuildContext context) => RenderLineChart( + context, + data, + targetData, + MediaQuery.of(context).textScaler, + chartVirtualRect, + canBeScaled: canBeScaled, + ); + + @override + void updateRenderObject(BuildContext context, RenderLineChart renderObject) { + renderObject + ..data = data + ..targetData = targetData + ..textScaler = MediaQuery.of(context).textScaler + ..buildContext = context + ..chartVirtualRect = chartVirtualRect + ..canBeScaled = canBeScaled; + } +} +// coverage:ignore-end + +/// Renders our LineChart, also handles hitTest. +class RenderLineChart extends RenderBaseChart { + RenderLineChart( + BuildContext context, + LineChartData data, + LineChartData targetData, + TextScaler textScaler, + Rect? chartVirtualRect, { + required bool canBeScaled, + }) : _data = data, + _targetData = targetData, + _textScaler = textScaler, + _chartVirtualRect = chartVirtualRect, + super( + targetData.lineTouchData, + context, + canBeScaled: canBeScaled, + ); + + LineChartData get data => _data; + LineChartData _data; + set data(LineChartData value) { + if (_data == value) return; + _data = value; + markNeedsPaint(); + } + + LineChartData get targetData => _targetData; + LineChartData _targetData; + set targetData(LineChartData value) { + if (_targetData == value) return; + _targetData = value; + super.updateBaseTouchData(_targetData.lineTouchData); + markNeedsPaint(); + } + + TextScaler get textScaler => _textScaler; + TextScaler _textScaler; + set textScaler(TextScaler value) { + if (_textScaler == value) return; + _textScaler = value; + markNeedsPaint(); + } + + Rect? get chartVirtualRect => _chartVirtualRect; + Rect? _chartVirtualRect; + set chartVirtualRect(Rect? value) { + if (_chartVirtualRect == value) return; + _chartVirtualRect = value; + markNeedsPaint(); + } + + // We couldn't mock [size] property of this class, that's why we have this + @visibleForTesting + Size? mockTestSize; + + @visibleForTesting + LineChartPainter painter = LineChartPainter(); + + PaintHolder get paintHolder => + PaintHolder(data, targetData, textScaler, chartVirtualRect); + + @override + void paint(PaintingContext context, Offset offset) { + final canvas = context.canvas + ..save() + ..translate(offset.dx, offset.dy); + painter.paint( + buildContext, + CanvasWrapper(canvas, mockTestSize ?? size), + paintHolder, + ); + canvas.restore(); + } + + @override + LineTouchResponse getResponseAtLocation(Offset localPosition) { + final chartSize = mockTestSize ?? size; + return LineTouchResponse( + touchLocation: localPosition, + touchChartCoordinate: painter.getChartCoordinateFromPixel( + localPosition, + chartSize, + paintHolder, + ), + lineBarSpots: painter.handleTouch( + localPosition, + chartSize, + paintHolder, + ), + ); + } +} diff --git a/lib/src/chart/pie_chart/pie_chart.dart b/lib/src/chart/pie_chart/pie_chart.dart new file mode 100644 index 0000000..98789be --- /dev/null +++ b/lib/src/chart/pie_chart/pie_chart.dart @@ -0,0 +1,117 @@ +import 'package:fl_chart/src/chart/pie_chart/pie_chart_data.dart'; +import 'package:fl_chart/src/chart/pie_chart/pie_chart_renderer.dart'; +import 'package:flutter/material.dart'; + +/// Renders a pie chart as a widget, using provided [PieChartData]. +class PieChart extends ImplicitlyAnimatedWidget { + /// [data] determines how the [PieChart] should be look like, + /// when you make any change in the [PieChartData], it updates + /// new values with animation, and duration is [duration]. + /// also you can change the [curve] + /// which default is [Curves.linear]. + const PieChart( + this.data, { + super.key, + @Deprecated('Please use [duration] instead') + Duration? swapAnimationDuration, + Duration duration = const Duration(milliseconds: 150), + @Deprecated('Please use [curve] instead') Curve? swapAnimationCurve, + Curve curve = Curves.linear, + }) : super( + duration: swapAnimationDuration ?? duration, + curve: swapAnimationCurve ?? curve, + ); + + /// Default duration to reuse externally. + static const defaultDuration = Duration(milliseconds: 150); + + /// Determines how the [PieChart] should be look like. + final PieChartData data; + + /// Creates a [_PieChartState] + @override + _PieChartState createState() => _PieChartState(); +} + +class _PieChartState extends AnimatedWidgetBaseState { + /// We handle under the hood animations (implicit animations) via this tween, + /// it lerps between the old [PieChartData] to the new one. + PieChartDataTween? _pieChartDataTween; + + @override + void initState() { + /// Make sure that [_widgetsPositionHandler] is updated. + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + if (mounted) { + setState(() {}); + } + }); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + final showingData = _getData(); + + return PieChartLeaf( + data: _pieChartDataTween!.evaluate(animation), + targetData: showingData, + ); + } + + /// if builtIn touches are enabled, we should recreate our [pieChartData] + /// to handle built in touches + PieChartData _getData() { + return widget.data; + } + + @override + void forEachTween(TweenVisitor visitor) { + _pieChartDataTween = visitor( + _pieChartDataTween, + widget.data, + (dynamic value) => + PieChartDataTween(begin: value as PieChartData, end: widget.data), + ) as PieChartDataTween?; + } +} + +/// Positions the badge widgets on their respective sections. +class BadgeWidgetsDelegate extends MultiChildLayoutDelegate { + BadgeWidgetsDelegate({ + required this.badgeWidgetsCount, + required this.badgeWidgetsOffsets, + }); + + final int badgeWidgetsCount; + final Map badgeWidgetsOffsets; + + @override + void performLayout(Size size) { + for (var index = 0; index < badgeWidgetsCount; index++) { + final key = badgeWidgetsOffsets.keys.elementAt(index); + + final finalSize = layoutChild( + key, + BoxConstraints( + maxWidth: size.width, + maxHeight: size.height, + ), + ); + + positionChild( + key, + Offset( + badgeWidgetsOffsets[key]!.dx - (finalSize.width / 2), + badgeWidgetsOffsets[key]!.dy - (finalSize.height / 2), + ), + ); + } + } + + @override + bool shouldRelayout(BadgeWidgetsDelegate oldDelegate) { + return oldDelegate.badgeWidgetsOffsets != badgeWidgetsOffsets; + } +} diff --git a/lib/src/chart/pie_chart/pie_chart_data.dart b/lib/src/chart/pie_chart/pie_chart_data.dart new file mode 100644 index 0000000..816e36c --- /dev/null +++ b/lib/src/chart/pie_chart/pie_chart_data.dart @@ -0,0 +1,407 @@ +// coverage:ignore-file +import 'dart:ui'; + +import 'package:equatable/equatable.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/utils/lerp.dart'; +import 'package:flutter/material.dart'; + +/// [PieChart] needs this class to render itself. +/// +/// It holds data needed to draw a pie chart, +/// including pie sections, colors, ... +class PieChartData extends BaseChartData with EquatableMixin { + /// [PieChart] draws some [sections] in a circle, + /// and applies free space with radius [centerSpaceRadius], + /// and color [centerSpaceColor] in the center of the circle, + /// if you don't want it, set [centerSpaceRadius] to zero. + /// + /// It draws [sections] from zero degree (right side of the circle) clockwise, + /// you can change the starting point, by changing [startDegreeOffset] (in degrees). + /// + /// You can define a gap between [sections] by setting [sectionsSpace]. + /// + /// You can modify [pieTouchData] to customize touch behaviors and responses. + PieChartData({ + List? sections, + double? centerSpaceRadius, + Color? centerSpaceColor, + double? sectionsSpace, + double? startDegreeOffset, + PieTouchData? pieTouchData, + FlBorderData? borderData, + bool? titleSunbeamLayout, + }) : sections = sections ?? const [], + centerSpaceRadius = centerSpaceRadius ?? double.infinity, + centerSpaceColor = centerSpaceColor ?? Colors.transparent, + sectionsSpace = sectionsSpace ?? 2, + startDegreeOffset = startDegreeOffset ?? 0, + pieTouchData = pieTouchData ?? PieTouchData(), + titleSunbeamLayout = titleSunbeamLayout ?? false, + super( + borderData: borderData ?? FlBorderData(show: false), + ); + + /// Defines showing sections of the [PieChart]. + final List sections; + + /// Radius of free space in center of the circle. + final double centerSpaceRadius; + + /// Color of free space in center of the circle. + final Color centerSpaceColor; + + /// Defines gap between sections. + /// + /// Does not work on html-renderer, + /// https://github.com/imaNNeo/fl_chart/issues/955 + final double sectionsSpace; + + /// [PieChart] draws [sections] from zero degree (right side of the circle) clockwise. + final double startDegreeOffset; + + /// Handles touch behaviors and responses. + final PieTouchData pieTouchData; + + /// Whether to rotate the titles on each section of the chart + final bool titleSunbeamLayout; + + /// We hold this value to determine weight of each [PieChartSectionData.value]. + double get sumValue => sections + .map((data) => data.value) + .reduce((first, second) => first + second); + + /// Copies current [PieChartData] to a new [PieChartData], + /// and replaces provided values. + PieChartData copyWith({ + List? sections, + double? centerSpaceRadius, + Color? centerSpaceColor, + double? sectionsSpace, + double? startDegreeOffset, + PieTouchData? pieTouchData, + FlBorderData? borderData, + bool? titleSunbeamLayout, + }) => + PieChartData( + sections: sections ?? this.sections, + centerSpaceRadius: centerSpaceRadius ?? this.centerSpaceRadius, + centerSpaceColor: centerSpaceColor ?? this.centerSpaceColor, + sectionsSpace: sectionsSpace ?? this.sectionsSpace, + startDegreeOffset: startDegreeOffset ?? this.startDegreeOffset, + pieTouchData: pieTouchData ?? this.pieTouchData, + borderData: borderData ?? this.borderData, + titleSunbeamLayout: titleSunbeamLayout ?? this.titleSunbeamLayout, + ); + + /// Lerps a [BaseChartData] based on [t] value, check [Tween.lerp]. + @override + PieChartData lerp(BaseChartData a, BaseChartData b, double t) { + if (a is PieChartData && b is PieChartData) { + return PieChartData( + borderData: FlBorderData.lerp(a.borderData, b.borderData, t), + centerSpaceColor: Color.lerp(a.centerSpaceColor, b.centerSpaceColor, t), + centerSpaceRadius: lerpDoubleAllowInfinity( + a.centerSpaceRadius, + b.centerSpaceRadius, + t, + ), + pieTouchData: b.pieTouchData, + sectionsSpace: lerpDouble(a.sectionsSpace, b.sectionsSpace, t), + startDegreeOffset: + lerpDouble(a.startDegreeOffset, b.startDegreeOffset, t), + sections: lerpPieChartSectionDataList(a.sections, b.sections, t), + titleSunbeamLayout: b.titleSunbeamLayout, + ); + } else { + throw Exception('Illegal State'); + } + } + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + sections, + centerSpaceRadius, + centerSpaceColor, + pieTouchData, + sectionsSpace, + startDegreeOffset, + borderData, + titleSunbeamLayout, + ]; +} + +/// Holds data related to drawing each [PieChart] section. +class PieChartSectionData with EquatableMixin { + /// [PieChart] draws section from right side of the circle (0 degrees), + /// each section have a [value] that determines how much it should occupy, + /// this is depends on sum of all sections, each section should + /// occupy ([value] / sumValues) * 360 degrees. + /// + /// It draws this section with filled [color], and [radius]. + /// + /// If [showTitle] is true, it draws a title at the middle of section, + /// you can set the text using [title], and set the style using [titleStyle], + /// by default it draws texts at the middle of section, but you can change the + /// [titlePositionPercentageOffset] to have your desire design, + /// it should be between 0.0 to 1.0, + /// 0.0 means near the center, + /// 1.0 means near the outside of the [PieChart]. + /// + /// If [badgeWidget] is not null, it draws a widget at the middle of section, + /// by default it draws the widget at the middle of section, but you can change the + /// [badgePositionPercentageOffset] to have your desire design, + /// the value works the same way as [titlePositionPercentageOffset]. + PieChartSectionData({ + double? value, + Color? color, + this.gradient, + double? radius, + bool? showTitle, + this.titleStyle, + String? title, + BorderSide? borderSide, + this.badgeWidget, + double? titlePositionPercentageOffset, + double? badgePositionPercentageOffset, + }) : value = value ?? 10, + color = color ?? Colors.cyan, + radius = radius ?? 40, + showTitle = showTitle ?? true, + title = title ?? (value == null ? '' : value.toString()), + borderSide = borderSide ?? const BorderSide(width: 0), + titlePositionPercentageOffset = titlePositionPercentageOffset ?? 0.5, + badgePositionPercentageOffset = badgePositionPercentageOffset ?? 0.5; + + /// It determines how much space it should occupy around the circle. + /// + /// This is depends on sum of all sections, each section should + /// occupy ([value] / sumValues) * 360 degrees. + /// + /// value can not be null. + final double value; + + /// Defines the color of section. + final Color color; + + /// Defines the gradient of section. If specified, overrides the color setting. + final Gradient? gradient; + + /// Defines the radius of section. + final double radius; + + /// Defines show or hide the title of section. + final bool showTitle; + + /// Defines style of showing title of section. + final TextStyle? titleStyle; + + /// Defines text of showing title at the middle of section. + final String title; + + /// Defines border stroke around the section + final BorderSide borderSide; + + /// Defines a widget that represents the section. + /// + /// This can be anything from a text, an image, an animation, and even a combination of widgets. + /// Use AnimatedWidgets to animate this widget. + final Widget? badgeWidget; + + /// Defines position of showing title in the section. + /// + /// It should be between 0.0 to 1.0, + /// 0.0 means near the center, + /// 1.0 means near the outside of the [PieChart]. + final double titlePositionPercentageOffset; + + /// Defines position of badge widget in the section. + /// + /// It should be between 0.0 to 1.0, + /// 0.0 means near the center, + /// 1.0 means near the outside of the [PieChart]. + final double badgePositionPercentageOffset; + + /// Copies current [PieChartSectionData] to a new [PieChartSectionData], + /// and replaces provided values. + PieChartSectionData copyWith({ + double? value, + Color? color, + Gradient? gradient, + double? radius, + bool? showTitle, + TextStyle? titleStyle, + String? title, + BorderSide? borderSide, + Widget? badgeWidget, + double? titlePositionPercentageOffset, + double? badgePositionPercentageOffset, + }) => + PieChartSectionData( + value: value ?? this.value, + color: color ?? this.color, + gradient: gradient ?? this.gradient, + radius: radius ?? this.radius, + showTitle: showTitle ?? this.showTitle, + titleStyle: titleStyle ?? this.titleStyle, + title: title ?? this.title, + borderSide: borderSide ?? this.borderSide, + badgeWidget: badgeWidget ?? this.badgeWidget, + titlePositionPercentageOffset: + titlePositionPercentageOffset ?? this.titlePositionPercentageOffset, + badgePositionPercentageOffset: + badgePositionPercentageOffset ?? this.badgePositionPercentageOffset, + ); + + /// Lerps a [PieChartSectionData] based on [t] value, check [Tween.lerp]. + static PieChartSectionData lerp( + PieChartSectionData a, + PieChartSectionData b, + double t, + ) => + PieChartSectionData( + value: lerpDouble(a.value, b.value, t), + color: Color.lerp(a.color, b.color, t), + gradient: Gradient.lerp(a.gradient, b.gradient, t), + radius: lerpDouble(a.radius, b.radius, t), + showTitle: b.showTitle, + titleStyle: TextStyle.lerp(a.titleStyle, b.titleStyle, t), + title: b.title, + borderSide: BorderSide.lerp(a.borderSide, b.borderSide, t), + badgeWidget: b.badgeWidget, + titlePositionPercentageOffset: lerpDouble( + a.titlePositionPercentageOffset, + b.titlePositionPercentageOffset, + t, + ), + badgePositionPercentageOffset: lerpDouble( + a.badgePositionPercentageOffset, + b.badgePositionPercentageOffset, + t, + ), + ); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + value, + color, + gradient, + radius, + showTitle, + titleStyle, + title, + borderSide, + badgeWidget, + titlePositionPercentageOffset, + badgePositionPercentageOffset, + ]; +} + +/// Holds data to handle touch events, and touch responses in the [PieChart]. +/// +/// There is a touch flow, explained [here](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/handle_touches.md) +/// in a simple way, each chart's renderer captures the touch events, and passes the pointerEvent +/// to the painter, and gets touched spot, and wraps it into a concrete [PieTouchResponse]. +class PieTouchData extends FlTouchData with EquatableMixin { + /// You can disable or enable the touch system using [enabled] flag, + /// + /// [touchCallback] notifies you about the happened touch/pointer events. + /// It gives you a [FlTouchEvent] which is the happened event such as [FlPointerHoverEvent], [FlTapUpEvent], ... + /// It also gives you a [PieTouchResponse] which contains information + /// about the elements that has touched. + /// + /// Using [mouseCursorResolver] you can change the mouse cursor + /// based on the provided [FlTouchEvent] and [PieTouchResponse] + PieTouchData({ + bool? enabled, + BaseTouchCallback? touchCallback, + MouseCursorResolver? mouseCursorResolver, + Duration? longPressDuration, + }) : super( + enabled ?? true, + touchCallback, + mouseCursorResolver, + longPressDuration, + ); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + enabled, + touchCallback, + mouseCursorResolver, + longPressDuration, + ]; +} + +class PieTouchedSection with EquatableMixin { + /// This class Contains [touchedSection], [touchedSectionIndex] that tells + /// you touch happened on which section, + /// [touchAngle] gives you angle of touch, + /// and [touchRadius] gives you radius of the touch. + PieTouchedSection( + this.touchedSection, + this.touchedSectionIndex, + this.touchAngle, + this.touchRadius, + ); + + /// touch happened on this section + final PieChartSectionData? touchedSection; + + /// touch happened on this position + final int touchedSectionIndex; + + /// touch happened with this angle on the [PieChart] + final double touchAngle; + + /// touch happened with this radius on the [PieChart] + final double touchRadius; + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + touchedSection, + touchedSectionIndex, + touchAngle, + touchRadius, + ]; +} + +/// Holds information about touch response in the [PieChart]. +/// +/// You can override [PieTouchData.touchCallback] to handle touch events, +/// it gives you a [PieTouchResponse] and you can do whatever you want. +class PieTouchResponse extends BaseTouchResponse { + /// If touch happens, [PieChart] processes it internally and passes out a [PieTouchResponse] + PieTouchResponse({ + required super.touchLocation, + required this.touchedSection, + }); + + /// Contains information about touched section, like index, angle, radius, ... + final PieTouchedSection? touchedSection; + + /// Copies current [PieTouchResponse] to a new [PieTouchResponse], + /// and replaces provided values. + PieTouchResponse copyWith({ + Offset? touchLocation, + PieTouchedSection? touchedSection, + }) => + PieTouchResponse( + touchLocation: touchLocation ?? this.touchLocation, + touchedSection: touchedSection ?? this.touchedSection, + ); +} + +/// It lerps a [PieChartData] to another [PieChartData] (handles animation for updating values) +class PieChartDataTween extends Tween { + PieChartDataTween({required PieChartData begin, required PieChartData end}) + : super(begin: begin, end: end); + + /// Lerps a [PieChartData] based on [t] value, check [Tween.lerp]. + @override + PieChartData lerp(double t) => begin!.lerp(begin!, end!, t); +} diff --git a/lib/src/chart/pie_chart/pie_chart_helper.dart b/lib/src/chart/pie_chart/pie_chart_helper.dart new file mode 100644 index 0000000..d746963 --- /dev/null +++ b/lib/src/chart/pie_chart/pie_chart_helper.dart @@ -0,0 +1,21 @@ +import 'package:fl_chart/src/chart/pie_chart/pie_chart_data.dart'; +import 'package:flutter/widgets.dart'; + +extension PieChartSectionDataListExtension on List { + List toWidgets() { + final widgets = List.filled(length, Container()); + var allWidgetsAreNull = true; + asMap().entries.forEach((e) { + final index = e.key; + final section = e.value; + if (section.badgeWidget != null) { + widgets[index] = section.badgeWidget!; + allWidgetsAreNull = false; + } + }); + if (allWidgetsAreNull) { + return List.empty(); + } + return widgets; + } +} diff --git a/lib/src/chart/pie_chart/pie_chart_painter.dart b/lib/src/chart/pie_chart/pie_chart_painter.dart new file mode 100644 index 0000000..86d8857 --- /dev/null +++ b/lib/src/chart/pie_chart/pie_chart_painter.dart @@ -0,0 +1,550 @@ +import 'dart:math' as math; + +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart'; +import 'package:fl_chart/src/chart/base/line.dart'; +import 'package:fl_chart/src/chart/pie_chart/pie_chart_data.dart'; +import 'package:fl_chart/src/extensions/paint_extension.dart'; +import 'package:fl_chart/src/utils/canvas_wrapper.dart'; +import 'package:fl_chart/src/utils/utils.dart'; +import 'package:flutter/material.dart'; + +/// Paints [PieChartData] in the canvas, it can be used in a [CustomPainter] +class PieChartPainter extends BaseChartPainter { + /// Paints [dataList] into canvas, it is the animating [PieChartData], + /// [targetData] is the animation's target and remains the same + /// during animation, then we should use it when we need to show + /// tooltips or something like that, because [dataList] is changing constantly. + /// + /// [textScale] used for scaling texts inside the chart, + /// parent can use [MediaQuery.textScaleFactor] to respect + /// the system's font size. + PieChartPainter() : super() { + _sectionPaint = Paint()..style = PaintingStyle.stroke; + + _sectionSaveLayerPaint = Paint(); + + _sectionStrokePaint = Paint()..style = PaintingStyle.stroke; + + _centerSpacePaint = Paint()..style = PaintingStyle.fill; + + _clipPaint = Paint(); + } + + late Paint _sectionPaint; + late Paint _sectionSaveLayerPaint; + late Paint _sectionStrokePaint; + late Paint _centerSpacePaint; + late Paint _clipPaint; + + /// Paints [PieChartData] into the provided canvas. + @override + void paint( + BuildContext context, + CanvasWrapper canvasWrapper, + PaintHolder holder, + ) { + super.paint(context, canvasWrapper, holder); + final data = holder.data; + if (data.sections.isEmpty) { + return; + } + + final sectionsAngle = calculateSectionsAngle(data.sections, data.sumValue); + final centerRadius = calculateCenterRadius(canvasWrapper.size, holder); + + drawCenterSpace(canvasWrapper, centerRadius, holder); + drawSections(canvasWrapper, sectionsAngle, centerRadius, holder); + drawTexts(context, canvasWrapper, holder, centerRadius); + } + + @visibleForTesting + List calculateSectionsAngle( + List sections, + double sumValue, + ) { + if (sumValue == 0) { + return List.filled(sections.length, 0); + } + + return sections.map((section) { + return 360 * (section.value / sumValue); + }).toList(); + } + + @visibleForTesting + void drawCenterSpace( + CanvasWrapper canvasWrapper, + double centerRadius, + PaintHolder holder, + ) { + final data = holder.data; + final viewSize = canvasWrapper.size; + final centerX = viewSize.width / 2; + final centerY = viewSize.height / 2; + + _centerSpacePaint.color = data.centerSpaceColor; + canvasWrapper.drawCircle( + Offset(centerX, centerY), + centerRadius, + _centerSpacePaint, + ); + } + + @visibleForTesting + void drawSections( + CanvasWrapper canvasWrapper, + List sectionsAngle, + double centerRadius, + PaintHolder holder, + ) { + final data = holder.data; + final viewSize = canvasWrapper.size; + + final center = Offset(viewSize.width / 2, viewSize.height / 2); + + var tempAngle = data.startDegreeOffset; + + for (var i = 0; i < data.sections.length; i++) { + final section = data.sections[i]; + if (section.value == 0) { + continue; + } + final sectionDegree = sectionsAngle[i]; + + if (sectionDegree == 360) { + final radius = centerRadius + section.radius / 2; + final rect = Rect.fromCircle(center: center, radius: radius); + _sectionPaint + ..setColorOrGradient( + section.color, + section.gradient, + rect, + ) + ..strokeWidth = section.radius + ..style = PaintingStyle.fill; + + final bounds = Rect.fromCircle( + center: center, + radius: centerRadius + section.radius, + ); + canvasWrapper + ..saveLayer(bounds, _sectionSaveLayerPaint) + ..drawCircle( + center, + centerRadius + section.radius, + _sectionPaint..blendMode = BlendMode.srcOver, + ) + ..drawCircle( + center, + centerRadius, + _sectionPaint..blendMode = BlendMode.srcOut, + ) + ..restore(); + _sectionPaint.blendMode = BlendMode.srcOver; + if (section.borderSide.width != 0.0 && + section.borderSide.color.a != 0.0) { + _sectionStrokePaint + ..strokeWidth = section.borderSide.width + ..color = section.borderSide.color; + // Outer + canvasWrapper + ..drawCircle( + center, + centerRadius + section.radius - (section.borderSide.width / 2), + _sectionStrokePaint, + ) + + // Inner + ..drawCircle( + center, + centerRadius + (section.borderSide.width / 2), + _sectionStrokePaint, + ); + } + return; + } + + final sectionPath = generateSectionPath( + section, + data.sectionsSpace, + tempAngle, + sectionDegree, + center, + centerRadius, + ); + + drawSection(section, sectionPath, canvasWrapper); + drawSectionStroke(section, sectionPath, canvasWrapper, viewSize); + tempAngle += sectionDegree; + } + } + + /// Generates a path around a section + @visibleForTesting + Path generateSectionPath( + PieChartSectionData section, + double sectionSpace, + double tempAngle, + double sectionDegree, + Offset center, + double centerRadius, + ) { + final sectionRadiusRect = Rect.fromCircle( + center: center, + radius: centerRadius + section.radius, + ); + + final centerRadiusRect = Rect.fromCircle( + center: center, + radius: centerRadius, + ); + + final startRadians = Utils().radians(tempAngle); + final sweepRadians = Utils().radians(sectionDegree); + final endRadians = startRadians + sweepRadians; + + final startLineDirection = + Offset(math.cos(startRadians), math.sin(startRadians)); + + final startLineFrom = center + startLineDirection * centerRadius; + final startLineTo = startLineFrom + startLineDirection * section.radius; + final startLine = Line(startLineFrom, startLineTo); + + final endLineDirection = Offset(math.cos(endRadians), math.sin(endRadians)); + + final endLineFrom = center + endLineDirection * centerRadius; + final endLineTo = endLineFrom + endLineDirection * section.radius; + final endLine = Line(endLineFrom, endLineTo); + + var sectionPath = Path() + ..moveTo(startLine.from.dx, startLine.from.dy) + ..lineTo(startLine.to.dx, startLine.to.dy) + ..arcTo(sectionRadiusRect, startRadians, sweepRadians, false) + ..lineTo(endLine.from.dx, endLine.from.dy) + ..arcTo(centerRadiusRect, endRadians, -sweepRadians, false) + ..moveTo(startLine.from.dx, startLine.from.dy) + ..close(); + + /// Subtract section space from the sectionPath + if (sectionSpace != 0) { + final startLineSeparatorPath = createRectPathAroundLine( + Line(startLineFrom, startLineTo), + sectionSpace, + ); + try { + sectionPath = Path.combine( + PathOperation.difference, + sectionPath, + startLineSeparatorPath, + ); + } catch (_) { + /// It's a flutter engine issue with [Path.combine] in web-html renderer + /// https://github.com/imaNNeo/fl_chart/issues/955 + } + + final endLineSeparatorPath = + createRectPathAroundLine(Line(endLineFrom, endLineTo), sectionSpace); + try { + sectionPath = Path.combine( + PathOperation.difference, + sectionPath, + endLineSeparatorPath, + ); + } catch (_) { + /// It's a flutter engine issue with [Path.combine] in web-html renderer + /// https://github.com/imaNNeo/fl_chart/issues/955 + } + } + + return sectionPath; + } + + /// Creates a rect around a narrow line + @visibleForTesting + Path createRectPathAroundLine(Line line, double width) { + width = width / 2; + final normalized = line.normalize(); + + final verticalAngle = line.direction() + (math.pi / 2); + final verticalDirection = + Offset(math.cos(verticalAngle), math.sin(verticalAngle)); + + final startPoint1 = Offset( + line.from.dx - + (normalized * (width / 2)).dx - + (verticalDirection * width).dx, + line.from.dy - + (normalized * (width / 2)).dy - + (verticalDirection * width).dy, + ); + + final startPoint2 = Offset( + line.to.dx + + (normalized * (width / 2)).dx - + (verticalDirection * width).dx, + line.to.dy + + (normalized * (width / 2)).dy - + (verticalDirection * width).dy, + ); + + final startPoint3 = Offset( + startPoint2.dx + (verticalDirection * (width * 2)).dx, + startPoint2.dy + (verticalDirection * (width * 2)).dy, + ); + + final startPoint4 = Offset( + startPoint1.dx + (verticalDirection * (width * 2)).dx, + startPoint1.dy + (verticalDirection * (width * 2)).dy, + ); + + return Path() + ..moveTo(startPoint1.dx, startPoint1.dy) + ..lineTo(startPoint2.dx, startPoint2.dy) + ..lineTo(startPoint3.dx, startPoint3.dy) + ..lineTo(startPoint4.dx, startPoint4.dy) + ..lineTo(startPoint1.dx, startPoint1.dy); + } + + @visibleForTesting + void drawSection( + PieChartSectionData section, + Path sectionPath, + CanvasWrapper canvasWrapper, + ) { + _sectionPaint + ..setColorOrGradient( + section.color, + section.gradient, + sectionPath.getBounds(), + ) + ..style = PaintingStyle.fill; + canvasWrapper.drawPath(sectionPath, _sectionPaint); + } + + @visibleForTesting + void drawSectionStroke( + PieChartSectionData section, + Path sectionPath, + CanvasWrapper canvasWrapper, + Size viewSize, + ) { + if (section.borderSide.width != 0.0 && section.borderSide.color.a != 0.0) { + canvasWrapper + ..saveLayer( + Rect.fromLTWH(0, 0, viewSize.width, viewSize.height), + _clipPaint, + ) + ..clipPath(sectionPath); + + _sectionStrokePaint + ..strokeWidth = section.borderSide.width * 2 + ..color = section.borderSide.color; + canvasWrapper + ..drawPath( + sectionPath, + _sectionStrokePaint, + ) + ..restore(); + } + } + + /// Calculates layout of overlaying elements, includes: + /// - title text + /// - badge widget positions + @visibleForTesting + void drawTexts( + BuildContext context, + CanvasWrapper canvasWrapper, + PaintHolder holder, + double centerRadius, + ) { + final data = holder.data; + final viewSize = canvasWrapper.size; + final center = Offset(viewSize.width / 2, viewSize.height / 2); + + var tempAngle = data.startDegreeOffset; + + for (var i = 0; i < data.sections.length; i++) { + final section = data.sections[i]; + if (section.value == 0) { + continue; + } + final startAngle = tempAngle; + final sweepAngle = 360 * (section.value / data.sumValue); + final sectionCenterAngle = startAngle + (sweepAngle / 2); + + double? rotateAngle; + if (data.titleSunbeamLayout) { + if (sectionCenterAngle >= 90 && sectionCenterAngle <= 270) { + rotateAngle = sectionCenterAngle - 180; + } else { + rotateAngle = sectionCenterAngle; + } + } + + Offset sectionCenter(double percentageOffset) => + center + + Offset( + math.cos(Utils().radians(sectionCenterAngle)) * + (centerRadius + (section.radius * percentageOffset)), + math.sin(Utils().radians(sectionCenterAngle)) * + (centerRadius + (section.radius * percentageOffset)), + ); + + final sectionCenterOffsetTitle = + sectionCenter(section.titlePositionPercentageOffset); + + if (section.showTitle) { + final span = TextSpan( + style: Utils().getThemeAwareTextStyle(context, section.titleStyle), + text: section.title, + ); + final tp = TextPainter( + text: span, + textAlign: TextAlign.center, + textDirection: TextDirection.ltr, + textScaler: holder.textScaler, + )..layout(); + + canvasWrapper.drawText( + tp, + sectionCenterOffsetTitle - Offset(tp.width / 2, tp.height / 2), + rotateAngle, + ); + } + + tempAngle += sweepAngle; + } + } + + /// Calculates center radius based on the provided sections radius + @visibleForTesting + double calculateCenterRadius( + Size viewSize, + PaintHolder holder, + ) { + final data = holder.data; + if (data.centerSpaceRadius.isFinite) { + return data.centerSpaceRadius; + } + final maxRadius = + data.sections.reduce((a, b) => a.radius > b.radius ? a : b).radius; + return (viewSize.shortestSide - (maxRadius * 2)) / 2; + } + + /// Makes a [PieTouchedSection] based on the provided [localPosition] + /// + /// Processes [localPosition] and checks + /// the elements of the chart that are near the offset, + /// then makes a [PieTouchedSection] from the elements that has been touched. + PieTouchedSection handleTouch( + Offset localPosition, + Size viewSize, + PaintHolder holder, + ) { + final data = holder.data; + final sectionsAngle = calculateSectionsAngle(data.sections, data.sumValue); + final centerRadius = calculateCenterRadius(viewSize, holder); + + final center = Offset(viewSize.width / 2, viewSize.height / 2); + + final touchedPoint2 = localPosition - center; + + final touchX = touchedPoint2.dx; + final touchY = touchedPoint2.dy; + + final touchR = math.sqrt(math.pow(touchX, 2) + math.pow(touchY, 2)); + var touchAngle = Utils().degrees(math.atan2(touchY, touchX)); + touchAngle = touchAngle < 0 ? (180 - touchAngle.abs()) + 180 : touchAngle; + + PieChartSectionData? foundSectionData; + var foundSectionDataPosition = -1; + + var tempAngle = data.startDegreeOffset; + for (var i = 0; i < data.sections.length; i++) { + final section = data.sections[i]; + final sectionAngle = sectionsAngle[i]; + + if (sectionAngle == 360) { + final distance = math.sqrt( + math.pow(localPosition.dx - center.dx, 2) + + math.pow(localPosition.dy - center.dy, 2), + ); + if (distance >= centerRadius && + distance <= section.radius + centerRadius) { + foundSectionData = section; + foundSectionDataPosition = i; + } + break; + } + + final sectionPath = generateSectionPath( + section, + data.sectionsSpace, + tempAngle, + sectionAngle, + center, + centerRadius, + ); + + if (sectionPath.contains(localPosition)) { + foundSectionData = section; + foundSectionDataPosition = i; + break; + } + + tempAngle += sectionAngle; + } + + return PieTouchedSection( + foundSectionData, + foundSectionDataPosition, + touchAngle, + touchR, + ); + } + + /// Exposes offset for laying out the badge widgets upon the chart. + Map getBadgeOffsets( + Size viewSize, + PaintHolder holder, + ) { + final data = holder.data; + final center = viewSize.center(Offset.zero); + final badgeWidgetsOffsets = {}; + + if (data.sections.isEmpty) { + return badgeWidgetsOffsets; + } + + var tempAngle = data.startDegreeOffset; + + final sectionsAngle = calculateSectionsAngle(data.sections, data.sumValue); + for (var i = 0; i < data.sections.length; i++) { + final section = data.sections[i]; + final startAngle = tempAngle; + final sweepAngle = sectionsAngle[i]; + final sectionCenterAngle = startAngle + (sweepAngle / 2); + final centerRadius = calculateCenterRadius(viewSize, holder); + + Offset sectionCenter(double percentageOffset) => + center + + Offset( + math.cos(Utils().radians(sectionCenterAngle)) * + (centerRadius + (section.radius * percentageOffset)), + math.sin(Utils().radians(sectionCenterAngle)) * + (centerRadius + (section.radius * percentageOffset)), + ); + + final sectionCenterOffsetBadgeWidget = + sectionCenter(section.badgePositionPercentageOffset); + + badgeWidgetsOffsets[i] = sectionCenterOffsetBadgeWidget; + + tempAngle += sweepAngle; + } + + return badgeWidgetsOffsets; + } +} diff --git a/lib/src/chart/pie_chart/pie_chart_renderer.dart b/lib/src/chart/pie_chart/pie_chart_renderer.dart new file mode 100644 index 0000000..3d4e5b1 --- /dev/null +++ b/lib/src/chart/pie_chart/pie_chart_renderer.dart @@ -0,0 +1,184 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart'; +import 'package:fl_chart/src/chart/base/base_chart/render_base_chart.dart'; +import 'package:fl_chart/src/chart/pie_chart/pie_chart_helper.dart'; +import 'package:fl_chart/src/chart/pie_chart/pie_chart_painter.dart'; +import 'package:fl_chart/src/utils/canvas_wrapper.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; + +// coverage:ignore-start + +/// Low level PieChart Widget. +class PieChartLeaf extends MultiChildRenderObjectWidget { + PieChartLeaf({ + super.key, + required this.data, + required this.targetData, + }) : super(children: targetData.sections.toWidgets()); + + final PieChartData data; + final PieChartData targetData; + + @override + RenderPieChart createRenderObject(BuildContext context) => RenderPieChart( + context, + data, + targetData, + MediaQuery.of(context).textScaler, + ); + + @override + void updateRenderObject(BuildContext context, RenderPieChart renderObject) { + renderObject + ..data = data + ..targetData = targetData + ..textScaler = MediaQuery.of(context).textScaler + ..buildContext = context; + } +} +// coverage:ignore-end + +/// Renders our PieChart, also handles hitTest. +class RenderPieChart extends RenderBaseChart + with + ContainerRenderObjectMixin, + RenderBoxContainerDefaultsMixin + implements MouseTrackerAnnotation { + RenderPieChart( + BuildContext context, + PieChartData data, + PieChartData targetData, + TextScaler textScaler, + ) : _data = data, + _targetData = targetData, + _textScaler = textScaler, + super(targetData.pieTouchData, context, canBeScaled: false); + + PieChartData get data => _data; + PieChartData _data; + + set data(PieChartData value) { + if (_data == value) return; + _data = value; + // We must update layout to draw badges correctly! + markNeedsLayout(); + } + + PieChartData get targetData => _targetData; + PieChartData _targetData; + + set targetData(PieChartData value) { + if (_targetData == value) return; + _targetData = value; + super.updateBaseTouchData(_targetData.pieTouchData); + // We must update layout to draw badges correctly! + markNeedsLayout(); + } + + TextScaler get textScaler => _textScaler; + TextScaler _textScaler; + + set textScaler(TextScaler value) { + if (_textScaler == value) return; + _textScaler = value; + markNeedsPaint(); + } + + // We couldn't mock [size] property of this class, that's why we have this + @visibleForTesting + Size? mockTestSize; + + @visibleForTesting + PieChartPainter painter = PieChartPainter(); + + PaintHolder get paintHolder => + PaintHolder(data, targetData, textScaler); + + @override + void setupParentData(RenderBox child) { + if (child.parentData is! MultiChildLayoutParentData) { + child.parentData = MultiChildLayoutParentData(); + } + } + + @override + void performLayout() { + var child = firstChild; + size = computeDryLayout(constraints); + + final childConstraints = constraints.loosen(); + + var counter = 0; + final badgeOffsets = painter.getBadgeOffsets( + mockTestSize ?? size, + paintHolder, + ); + while (child != null) { + if (counter >= badgeOffsets.length) { + break; + } + child.layout(childConstraints, parentUsesSize: true); + final childParentData = child.parentData! as MultiChildLayoutParentData; + final sizeOffset = Offset(child.size.width / 2, child.size.height / 2); + childParentData.offset = badgeOffsets[counter]! - sizeOffset; + child = childParentData.nextSibling; + counter++; + } + } + + @override + bool hitTestChildren(BoxHitTestResult result, {required Offset position}) => + defaultHitTestChildren(result, position: position); + + @override + void paint(PaintingContext context, Offset offset) { + final canvas = context.canvas + ..save() + ..translate(offset.dx, offset.dy); + painter.paint( + buildContext, + CanvasWrapper(canvas, mockTestSize ?? size), + paintHolder, + ); + canvas.restore(); + badgeWidgetPaint(context, offset); + } + + void badgeWidgetPaint(PaintingContext context, Offset offset) { + RenderObject? child = firstChild; + var counter = 0; + while (child != null) { + final childParentData = child.parentData! as MultiChildLayoutParentData; + if (data.sections[counter].value > 0) { + context.paintChild(child, childParentData.offset + offset); + } + child = childParentData.nextSibling; + counter++; + } + } + + @override + PieTouchResponse getResponseAtLocation(Offset localPosition) { + return PieTouchResponse( + touchLocation: localPosition, + touchedSection: painter.handleTouch( + localPosition, + mockTestSize ?? size, + paintHolder, + ), + ); + } + + @override + void visitChildrenForSemantics(RenderObjectVisitor visitor) { + /// It produces an error when we change the sections list, Check this issue: + /// https://github.com/imaNNeo/fl_chart/issues/861 + /// + /// Below is the error message: + /// Updated layout information required for RenderSemanticsAnnotations#f3b96 NEEDS-LAYOUT NEEDS-PAINT to calculate semantics. + /// + /// I don't know how to solve this error. That's why we disabled semantics for now. + } +} diff --git a/lib/src/chart/radar_chart/radar_chart.dart b/lib/src/chart/radar_chart/radar_chart.dart new file mode 100644 index 0000000..ac7aecf --- /dev/null +++ b/lib/src/chart/radar_chart/radar_chart.dart @@ -0,0 +1,58 @@ +import 'package:fl_chart/src/chart/radar_chart/radar_chart_data.dart'; +import 'package:fl_chart/src/chart/radar_chart/radar_chart_renderer.dart'; +import 'package:flutter/material.dart'; + +/// Renders a radar chart as a widget, using provided [RadarChartData]. +class RadarChart extends ImplicitlyAnimatedWidget { + /// [data] determines how the [RadarChart] should be look like, + /// when you make any change in the [RadarChart], it updates + /// new values with animation, and duration is [duration]. + /// also you can change the [curve] + /// which default is [Curves.linear]. + const RadarChart( + this.data, { + super.key, + @Deprecated('Please use [duration] instead') + Duration? swapAnimationDuration, + Duration duration = const Duration(milliseconds: 150), + @Deprecated('Please use [curve] instead') Curve? swapAnimationCurve, + Curve curve = Curves.linear, + }) : super( + duration: swapAnimationDuration ?? duration, + curve: swapAnimationCurve ?? curve, + ); + + /// Determines how the [RadarChart] should be look like. + final RadarChartData data; + + @override + _RadarChartState createState() => _RadarChartState(); +} + +class _RadarChartState extends AnimatedWidgetBaseState { + /// we handle under the hood animations (implicit animations) via this tween, + /// it lerps between the old [RadarChartData] to the new one. + RadarChartDataTween? _radarChartDataTween; + + @override + Widget build(BuildContext context) { + final showingData = _getDate(); + + return RadarChartLeaf( + data: _radarChartDataTween!.evaluate(animation), + targetData: showingData, + ); + } + + RadarChartData _getDate() => widget.data; + + @override + void forEachTween(TweenVisitor visitor) { + _radarChartDataTween = visitor( + _radarChartDataTween, + widget.data, + (dynamic value) => + RadarChartDataTween(begin: value as RadarChartData, end: widget.data), + ) as RadarChartDataTween?; + } +} diff --git a/lib/src/chart/radar_chart/radar_chart_data.dart b/lib/src/chart/radar_chart/radar_chart_data.dart new file mode 100644 index 0000000..0cb376c --- /dev/null +++ b/lib/src/chart/radar_chart/radar_chart_data.dart @@ -0,0 +1,506 @@ +// coverage:ignore-file +import 'dart:ui'; + +import 'package:equatable/equatable.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/radar_chart/radar_extension.dart'; +import 'package:fl_chart/src/utils/lerp.dart'; +import 'package:flutter/material.dart'; + +typedef GetTitleByIndexFunction = RadarChartTitle Function( + int index, + double angle, +); + +enum RadarShape { + circle, + polygon, +} + +class RadarChartTitle { + const RadarChartTitle({ + required this.text, + this.children, + this.angle = 0, + this.positionPercentageOffset, + }); + + /// [text] is used to draw titles outside the [RadarChart] + final String text; + + /// [children] is used to draw additional titles outside the [RadarChart] + final List? children; + + /// [angle] is used to rotate the title + final double angle; + + /// [positionPercentageOffset] is the place of showing title on the [RadarChart] + /// The higher the value of this field, the more titles move away from the chart. + /// The value of [positionPercentageOffset] takes precedence over the value of + /// [RadarChartData.titlePositionPercentageOffset], even if it is set. + final double? positionPercentageOffset; +} + +/// [RadarChart] needs this class to render itself. +/// +/// It holds data needed to draw a radar chart, +/// including radar dataSets, colors, ... +class RadarChartData extends BaseChartData with EquatableMixin { + /// [RadarChart] draws some [dataSets] in a radar-shaped chart. + /// it fills the radar area with [radarBackgroundColor] + /// and draws radar border with [radarBorderData] + /// then draws a grid over it, you can customize it using [gridBorderData]. + /// + /// it draws some titles based on the number of [dataSets] values. + /// the titles are shown near each radar grid or line. + /// for changing the titles you can modify the [getTitle] field. + /// and for styling the titles you can use [titleTextStyle]. + /// + /// it draws some ticks. and you can customize the number of ticks by modifying the [titleCount] + /// and style the ticks titles with [ticksTextStyle]. + /// for changing the ticks color and border width you can use [tickBorderData]. + /// + /// You can modify [radarTouchData] to customize touch behaviors and responses. + RadarChartData({ + @required List? dataSets, + Color? radarBackgroundColor, + BorderSide? radarBorderData, + RadarShape? radarShape, + this.getTitle, + this.titleTextStyle, + double? titlePositionPercentageOffset, + int? tickCount, + this.ticksTextStyle, + BorderSide? tickBorderData, + BorderSide? gridBorderData, + RadarTouchData? radarTouchData, + this.isMinValueAtCenter = false, + super.borderData, + }) : assert(dataSets != null && dataSets.hasEqualDataEntriesLength), + assert( + tickCount == null || tickCount >= 1, + "RadarChart need's at least 1 tick", + ), + assert( + titlePositionPercentageOffset == null || + titlePositionPercentageOffset >= 0 && + titlePositionPercentageOffset <= 1, + 'titlePositionPercentageOffset must be something between 0 and 1 ', + ), + dataSets = dataSets ?? const [], + radarBackgroundColor = radarBackgroundColor ?? Colors.transparent, + radarBorderData = radarBorderData ?? const BorderSide(width: 2), + radarShape = radarShape ?? RadarShape.circle, + radarTouchData = radarTouchData ?? RadarTouchData(), + titlePositionPercentageOffset = titlePositionPercentageOffset ?? 0.2, + tickCount = tickCount ?? 1, + tickBorderData = tickBorderData ?? const BorderSide(width: 2), + gridBorderData = gridBorderData ?? const BorderSide(width: 2), + super(); + + /// [RadarChart] draw [dataSets] that each of them showing a list of [RadarEntry] + final List dataSets; + + /// [radarBackgroundColor] draw the background color of the [RadarChart] + final Color radarBackgroundColor; + + /// [radarBorderData] is used to draw [RadarChart] border + final BorderSide radarBorderData; + + /// [radarShape] is used to draw [RadarChart] border and background + final RadarShape radarShape; + + /// [getTitle] is used to draw titles outside the [RadarChart] + /// [getTitle] is type of [GetTitleByIndexFunction] so you should return a valid [RadarChartTitle] + /// for each [index] (we provide a default [angle] = index * 360 / titleCount) + /// + /// ```dart + /// getTitle: (index, angle) { + /// switch (index) { + /// case 0: + /// return RadarChartTitle(text: 'Mobile or Tablet', angle: angle); + /// case 2: + /// return RadarChartTitle(text: 'Desktop', angle: angle); + /// case 1: + /// return RadarChartTitle(text: 'TV', angle: angle); + /// default: + /// return const RadarChartTitle(text: ''); + /// } + /// } + /// ``` + final GetTitleByIndexFunction? getTitle; + + /// Defines style of showing [RadarChart] titles. + final TextStyle? titleTextStyle; + + /// the [titlePositionPercentageOffset] is the place of showing title on the [RadarChart] + /// The higher the value of this field, the more titles move away from the chart. + /// this field should be between 0 and 1, + /// if it is 0 the title will be drawn near the inside section, + /// if it is 1 the title will be drawn near the outside of section, + /// the default value is 0.2. + final double titlePositionPercentageOffset; + + /// Defines the number of ticks that should be paint in [RadarChart] + /// the default & minimum value of this field is 1. + final int tickCount; + + /// Defines style of showing [RadarChart] tick titles. + final TextStyle? ticksTextStyle; + + /// Defines style of showing [RadarChart] tick borders. + final BorderSide tickBorderData; + + /// Defines style of showing [RadarChart] grid borders. + final BorderSide gridBorderData; + + /// Handles touch behaviors and responses. + final RadarTouchData radarTouchData; + + /// If [isMinValueAtCenter] is true, the minimum value of the [RadarChart] will be at the center of the chart. + final bool isMinValueAtCenter; + + /// [titleCount] we use this value to determine number of [RadarChart] grid or lines. + int get titleCount => dataSets[0].dataEntries.length; + + /// defines the maximum [RadarEntry] value in all [dataSets] + /// we use this value to calculate the maximum value of ticks. + RadarEntry get maxEntry { + var maximum = dataSets.first.dataEntries.first; + + for (final dataSet in dataSets) { + for (final entry in dataSet.dataEntries) { + if (entry.value > maximum.value) maximum = entry; + } + } + return maximum; + } + + /// defines the minimum [RadarEntry] value in all [dataSets] + /// we use this value to calculate the minimum value of ticks. + RadarEntry get minEntry { + var minimum = dataSets.first.dataEntries.first; + + for (final dataSet in dataSets) { + for (final entry in dataSet.dataEntries) { + if (entry.value < minimum.value) minimum = entry; + } + } + + return minimum; + } + + /// Copies current [RadarChartData] to a new [RadarChartData], + /// and replaces provided values. + RadarChartData copyWith({ + List? dataSets, + Color? radarBackgroundColor, + BorderSide? radarBorderData, + RadarShape? radarShape, + GetTitleByIndexFunction? getTitle, + TextStyle? titleTextStyle, + double? titlePositionPercentageOffset, + int? tickCount, + TextStyle? ticksTextStyle, + BorderSide? tickBorderData, + BorderSide? gridBorderData, + RadarTouchData? radarTouchData, + bool? isMinValueAtCenter, + FlBorderData? borderData, + }) => + RadarChartData( + dataSets: dataSets ?? this.dataSets, + radarBackgroundColor: radarBackgroundColor ?? this.radarBackgroundColor, + radarBorderData: radarBorderData ?? this.radarBorderData, + radarShape: radarShape ?? this.radarShape, + getTitle: getTitle ?? this.getTitle, + titleTextStyle: titleTextStyle ?? this.titleTextStyle, + titlePositionPercentageOffset: + titlePositionPercentageOffset ?? this.titlePositionPercentageOffset, + tickCount: tickCount ?? this.tickCount, + ticksTextStyle: ticksTextStyle ?? this.ticksTextStyle, + tickBorderData: tickBorderData ?? this.tickBorderData, + gridBorderData: gridBorderData ?? this.gridBorderData, + radarTouchData: radarTouchData ?? this.radarTouchData, + isMinValueAtCenter: isMinValueAtCenter ?? this.isMinValueAtCenter, + borderData: borderData ?? this.borderData, + ); + + /// Lerps a [BaseChartData] based on [t] value, check [Tween.lerp]. + @override + RadarChartData lerp(BaseChartData a, BaseChartData b, double t) { + if (a is RadarChartData && b is RadarChartData) { + return RadarChartData( + dataSets: lerpRadarDataSetList(a.dataSets, b.dataSets, t), + radarBackgroundColor: + Color.lerp(a.radarBackgroundColor, b.radarBackgroundColor, t), + getTitle: b.getTitle, + titleTextStyle: TextStyle.lerp(a.titleTextStyle, b.titleTextStyle, t), + titlePositionPercentageOffset: lerpDouble( + a.titlePositionPercentageOffset, + b.titlePositionPercentageOffset, + t, + ), + tickCount: lerpInt(a.tickCount, b.tickCount, t), + ticksTextStyle: TextStyle.lerp(a.ticksTextStyle, b.ticksTextStyle, t), + gridBorderData: BorderSide.lerp(a.gridBorderData, b.gridBorderData, t), + radarBorderData: + BorderSide.lerp(a.radarBorderData, b.radarBorderData, t), + radarShape: b.radarShape, + tickBorderData: BorderSide.lerp(a.tickBorderData, b.tickBorderData, t), + borderData: FlBorderData.lerp(a.borderData, b.borderData, t), + isMinValueAtCenter: b.isMinValueAtCenter, + radarTouchData: b.radarTouchData, + ); + } else { + throw Exception('Illegal State'); + } + } + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + borderData, + dataSets, + radarBackgroundColor, + radarBorderData, + radarShape, + getTitle, + titleTextStyle, + titlePositionPercentageOffset, + tickCount, + ticksTextStyle, + tickBorderData, + gridBorderData, + radarTouchData, + isMinValueAtCenter, + ]; +} + +/// the data values for drawing [RadarChart] sections +class RadarDataSet with EquatableMixin { + /// [RadarChart] can contain multiple [RadarDataSet] And it shows them on top of each other. + /// each [RadarDataSet] has a set of [dataEntries] + /// and the [RadarChart] uses this [dataEntries] to draw the chart. + /// + /// it fill dataSets with [fillColor]. + /// + /// the [RadarDataSet] can have custom border. for changing border of [RadarDataSet] + /// you can modify the [borderColor] and [borderWidth]. + RadarDataSet({ + List? dataEntries, + Color? fillColor, + this.fillGradient, + Color? borderColor, + double? borderWidth, + double? entryRadius, + }) : assert( + dataEntries == null || dataEntries.isEmpty || dataEntries.length >= 3, + 'Radar needs at least 3 RadarEntry', + ), + dataEntries = dataEntries ?? const [], + fillColor = fillColor ?? Colors.cyan, + borderColor = borderColor ?? Colors.cyan, + borderWidth = borderWidth ?? 2.0, + entryRadius = entryRadius ?? 5.0; + + /// each section or dataSets consists of a set of [dataEntries]. + final List dataEntries; + + /// defines the color that fills the [RadarDataSet]. + final Color fillColor; + + // defines the gradient color that fills the [RadarDataSet]. + final Gradient? fillGradient; + + /// defines the border color of the [RadarDataSet]. + /// if [borderColor] is not defined it will replaced with [fillColor]. + final Color borderColor; + + /// defines the width of [RadarDataSet] border. + /// the default value of this field is 2.0 + final double borderWidth; + + /// defines the radius of each entry + /// the default value of this field is 5.0 + final double entryRadius; + + /// Copies current [RadarDataSet] to a new [RadarDataSet], + /// and replaces provided values. + RadarDataSet copyWith({ + List? dataEntries, + Color? fillColor, + Gradient? fillGradient, + Color? borderColor, + double? borderWidth, + double? entryRadius, + }) => + RadarDataSet( + dataEntries: dataEntries ?? this.dataEntries, + fillColor: fillColor ?? this.fillColor, + fillGradient: fillGradient, + borderColor: borderColor ?? this.borderColor, + borderWidth: borderWidth ?? this.borderWidth, + entryRadius: entryRadius ?? this.entryRadius, + ); + + /// Lerps a [RadarDataSet] based on [t] value, check [Tween.lerp]. + static RadarDataSet lerp(RadarDataSet a, RadarDataSet b, double t) => + RadarDataSet( + dataEntries: lerpRadarEntryList(a.dataEntries, b.dataEntries, t), + fillColor: Color.lerp(a.fillColor, b.fillColor, t), + fillGradient: Gradient.lerp(a.fillGradient, b.fillGradient, t), + borderColor: Color.lerp(a.borderColor, b.borderColor, t), + borderWidth: lerpDouble(a.borderWidth, b.borderWidth, t), + entryRadius: lerpDouble(a.entryRadius, b.entryRadius, t), + ); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + dataEntries, + fillColor, + fillGradient, + borderColor, + borderWidth, + entryRadius, + ]; +} + +/// holds the data about each entry or point in [RadarChart] +class RadarEntry with EquatableMixin { + /// [RadarChart] draws every point or entry with [RadarEntry] + const RadarEntry({required this.value}); + + /// [RadarChart] uses this field to render every point in chart. + final double value; + + /// Lerps a [RadarEntry] based on [t] value, check [Tween.lerp]. + RadarEntry copyWith({double? value}) => + RadarEntry(value: value ?? this.value); + + /// Lerps a [RadarDataSet] based on [t] value, check [Tween.lerp]. + static RadarEntry lerp(RadarEntry a, RadarEntry b, double t) => + RadarEntry(value: lerpDouble(a.value, b.value, t)!); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [value]; +} + +/// Holds data to handle touch events, and touch responses in the [RadarChart]. +/// +/// There is a touch flow, explained [here](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/handle_touches.md) +/// in a simple way, each chart's renderer captures the touch events, and passes the pointerEvent +/// to the painter, and gets touched spot, and wraps it into a concrete [RadarTouchResponse]. +class RadarTouchData extends FlTouchData + with EquatableMixin { + /// You can disable or enable the touch system using [enabled] flag, + /// + /// [touchCallback] notifies you about the happened touch/pointer events. + /// It gives you a [FlTouchEvent] which is the happened event such as [FlPointerHoverEvent], [FlTapUpEvent], ... + /// It also gives you a [RadarTouchResponse] which contains information + /// about the elements that has touched. + /// + /// Using [mouseCursorResolver] you can change the mouse cursor + /// based on the provided [FlTouchEvent] and [RadarTouchResponse] + RadarTouchData({ + bool? enabled, + BaseTouchCallback? touchCallback, + MouseCursorResolver? mouseCursorResolver, + Duration? longPressDuration, + double? touchSpotThreshold, + }) : touchSpotThreshold = touchSpotThreshold ?? 10, + super( + enabled ?? true, + touchCallback, + mouseCursorResolver, + longPressDuration, + ); + + /// we find the nearest spots on touched position based on this threshold + final double touchSpotThreshold; + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + enabled, + touchCallback, + mouseCursorResolver, + longPressDuration, + touchSpotThreshold, + ]; +} + +/// Holds information about touch response in the [RadarTouchResponse]. +/// +/// You can override [RadarTouchData.touchCallback] to handle touch events, +/// it gives you a [RadarTouchResponse] and you can do whatever you want. +class RadarTouchResponse extends BaseTouchResponse { + /// If touch happens, [RadarChart] processes it internally and passes out a [RadarTouchResponse] + /// that contains a [touchedSpot], it gives you information about the touched spot. + RadarTouchResponse({ + required super.touchLocation, + required this.touchedSpot, + }); + + /// touch happened on this spot. this spot has useful information about spot or entry + final RadarTouchedSpot? touchedSpot; + + /// Copies current [RadarTouchResponse] to a new [RadarTouchResponse], + /// and replaces provided values. + RadarTouchResponse copyWith({ + Offset? touchLocation, + RadarTouchedSpot? touchedSpot, + }) => + RadarTouchResponse( + touchLocation: touchLocation ?? this.touchLocation, + touchedSpot: touchedSpot ?? this.touchedSpot, + ); +} + +/// It gives you information about the touched spot. +class RadarTouchedSpot extends TouchedSpot with EquatableMixin { + /// When touch happens, a [RadarTouchedSpot] returns as a output, + /// it tells you where the touch happened. + /// [touchedDataSet], and [touchedDataSetIndex] tell you in which dataSet touch happened, + /// [touchedRadarEntry], and [touchedRadarEntryIndex] tell you in which entry touch happened, + /// You can also have the touched x and y in the chart as a [FlSpot] using [spot] value, + /// and you can have the local touch coordinates on the screen as a [Offset] using [offset] value. + RadarTouchedSpot( + this.touchedDataSet, + this.touchedDataSetIndex, + this.touchedRadarEntry, + this.touchedRadarEntryIndex, + FlSpot spot, + Offset offset, + ) : super(spot, offset); + final RadarDataSet touchedDataSet; + final int touchedDataSetIndex; + + final RadarEntry touchedRadarEntry; + final int touchedRadarEntryIndex; + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + spot, + offset, + touchedDataSet, + touchedDataSetIndex, + touchedRadarEntry, + touchedRadarEntryIndex, + ]; +} + +/// It lerps a [RadarChartData] to another [RadarChartData] (handles animation for updating values) +class RadarChartDataTween extends Tween { + RadarChartDataTween({ + required RadarChartData begin, + required RadarChartData end, + }) : super(begin: begin, end: end); + + /// Lerps a [RadarChartData] based on [t] value, check [Tween.lerp]. + @override + RadarChartData lerp(double t) => begin!.lerp(begin!, end!, t); +} diff --git a/lib/src/chart/radar_chart/radar_chart_painter.dart b/lib/src/chart/radar_chart/radar_chart_painter.dart new file mode 100644 index 0000000..e572712 --- /dev/null +++ b/lib/src/chart/radar_chart/radar_chart_painter.dart @@ -0,0 +1,499 @@ +import 'dart:math' show cos, min, pi, sin; + +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart'; +import 'package:fl_chart/src/utils/canvas_wrapper.dart'; +import 'package:fl_chart/src/utils/utils.dart'; +import 'package:flutter/material.dart'; + +/// Paints [RadarChartData] in the canvas, it can be used in a [CustomPainter] +class RadarChartPainter extends BaseChartPainter { + /// Paints [dataList] into canvas, it is the animating [RadarChartData], + /// [targetData] is the animation's target and remains the same + /// during animation, then we should use it when we need to show + /// tooltips or something like that, because [dataList] is changing constantly. + /// + /// [textScale] used for scaling texts inside the chart, + /// parent can use [MediaQuery.textScaleFactor] to respect + /// the system's font size. + RadarChartPainter() : super() { + _backgroundPaint = Paint() + ..style = PaintingStyle.fill + ..isAntiAlias = true; + + _borderPaint = Paint()..style = PaintingStyle.stroke; + + _gridPaint = Paint()..style = PaintingStyle.stroke; + + _tickPaint = Paint()..style = PaintingStyle.stroke; + + _graphPaint = Paint(); + _graphBorderPaint = Paint(); + _graphPointPaint = Paint(); + _ticksTextPaint = TextPainter(); + _titleTextPaint = TextPainter(); + } + late Paint _borderPaint; + late Paint _backgroundPaint; + late Paint _gridPaint; + late Paint _tickPaint; + late Paint _graphPaint; + late Paint _graphBorderPaint; + late Paint _graphPointPaint; + + late TextPainter _ticksTextPaint; + late TextPainter _titleTextPaint; + + List? dataSetsPosition; + + /// Paints [RadarChartData] into the provided canvas. + @override + void paint( + BuildContext context, + CanvasWrapper canvasWrapper, + PaintHolder holder, + ) { + super.paint(context, canvasWrapper, holder); + final data = holder.data; + + if (data.dataSets.isEmpty) { + return; + } + + dataSetsPosition = calculateDataSetsPosition(canvasWrapper.size, holder); + + drawGrids(canvasWrapper, holder); + drawTicks(context, canvasWrapper, holder); + drawTitles(context, canvasWrapper, holder); + drawDataSets(canvasWrapper, holder); + } + + @visibleForTesting + double getDefaultChartCenterValue() => 0; + + double getChartCenterValue(RadarChartData data) { + final dataSetMaxValue = data.maxEntry.value; + final dataSetMinValue = data.minEntry.value; + + if (data.isMinValueAtCenter) { + return dataSetMinValue; + } + + final tickSpace = getSpaceBetweenTicks(data); + final centerValue = dataSetMinValue - tickSpace; + + return dataSetMaxValue == dataSetMinValue + ? getDefaultChartCenterValue() + : centerValue; + } + + @visibleForTesting + double getScaledPoint(RadarEntry point, double radius, RadarChartData data) { + final centerValue = getChartCenterValue(data); + final distanceFromPointToCenter = point.value - centerValue; + final distanceFromMaxToCenter = data.maxEntry.value - centerValue; + + if (distanceFromMaxToCenter == 0) { + return radius * distanceFromPointToCenter / 0.001; + } + + return radius * distanceFromPointToCenter / distanceFromMaxToCenter; + } + + @visibleForTesting + double getFirstTickValue(RadarChartData data) { + final defaultCenterValue = getDefaultChartCenterValue(); + if (data.isMinValueAtCenter) { + return defaultCenterValue; + } + + final dataSetMaxValue = data.maxEntry.value; + final dataSetMinValue = data.minEntry.value; + + return dataSetMaxValue == dataSetMinValue + ? (dataSetMaxValue - defaultCenterValue) / (data.tickCount + 1) + + defaultCenterValue + : dataSetMinValue; + } + + @visibleForTesting + double getSpaceBetweenTicks(RadarChartData data) { + final dataSetMaxValue = data.maxEntry.value; + final dataSetMinValue = data.minEntry.value; + + if (data.isMinValueAtCenter) { + return (dataSetMaxValue - dataSetMinValue) / (data.tickCount); + } + + final defaultCenterValue = getDefaultChartCenterValue(); + final tickSpace = (dataSetMaxValue - dataSetMinValue) / data.tickCount; + final defaultTickSpace = + (dataSetMaxValue - defaultCenterValue) / (data.tickCount + 1); + + return dataSetMaxValue == dataSetMinValue ? defaultTickSpace : tickSpace; + } + + @visibleForTesting + void drawTicks( + BuildContext context, + CanvasWrapper canvasWrapper, + PaintHolder holder, + ) { + final data = holder.data; + final size = canvasWrapper.size; + + final centerX = radarCenterX(size); + final centerY = radarCenterY(size); + final centerOffset = Offset(centerX, centerY); + + /// controls Radar chart size + final radius = radarRadius(size); + + _backgroundPaint.color = data.radarBackgroundColor; + + _borderPaint + ..color = data.radarBorderData.color + ..strokeWidth = data.radarBorderData.width; + + if (data.radarShape == RadarShape.circle) { + /// draw radar background + canvasWrapper + ..drawCircle(centerOffset, radius, _backgroundPaint) + + /// draw radar border + ..drawCircle(centerOffset, radius, _borderPaint); + } else { + final path = + _generatePolygonPath(centerX, centerY, radius, data.titleCount); + + /// draw radar background + canvasWrapper + ..drawPath(path, _backgroundPaint) + + /// draw radar border + ..drawPath(path, _borderPaint); + } + + final tickSpace = getSpaceBetweenTicks(data); + final ticks = []; + var tickValue = getFirstTickValue(data); + + for (var i = 0; i <= data.tickCount; i++) { + ticks.add(tickValue); + tickValue += tickSpace; + } + + final tickDistance = data.isMinValueAtCenter + ? radius / (ticks.length - 1) + : radius / ticks.length; + + _tickPaint + ..color = data.tickBorderData.color + ..strokeWidth = data.tickBorderData.width; + + /// draw radar ticks + ticks.sublist(0, ticks.length - 1).asMap().forEach( + (index, tick) { + final tickRadius = + tickDistance * (index + (data.isMinValueAtCenter ? 0 : 1)); + if (data.radarShape == RadarShape.circle) { + canvasWrapper.drawCircle(centerOffset, tickRadius, _tickPaint); + } else { + canvasWrapper.drawPath( + _generatePolygonPath(centerX, centerY, tickRadius, data.titleCount), + _tickPaint, + ); + } + + _ticksTextPaint + ..text = TextSpan( + text: tick.toStringAsFixed(1), + style: Utils().getThemeAwareTextStyle(context, data.ticksTextStyle), + ) + ..textDirection = TextDirection.ltr + ..layout(maxWidth: size.width); + canvasWrapper.drawText( + _ticksTextPaint, + Offset(centerX + 5, centerY - tickRadius - _ticksTextPaint.height), + ); + }, + ); + } + + Path _generatePolygonPath( + double centerX, + double centerY, + double radius, + int count, + ) { + final path = Path()..moveTo(centerX, centerY - radius); + final angle = (2 * pi) / count; + for (var index = 0; index < count; index++) { + final xAngle = cos(angle * index - pi / 2); + final yAngle = sin(angle * index - pi / 2); + path.lineTo(centerX + radius * xAngle, centerY + radius * yAngle); + } + path.lineTo(centerX, centerY - radius); + return path; + } + + void drawGrids( + CanvasWrapper canvasWrapper, + PaintHolder holder, + ) { + final data = holder.data; + final size = canvasWrapper.size; + + final centerX = radarCenterX(size); + final centerY = radarCenterY(size); + final centerOffset = Offset(centerX, centerY); + + /// controls Radar chart size + final radius = radarRadius(size); + + final angle = (2 * pi) / data.titleCount; + + /// drawing grids + for (var index = 0; index < data.titleCount; index++) { + final endX = centerX + radius * cos(angle * index - pi / 2); + final endY = centerY + radius * sin(angle * index - pi / 2); + + final gridOffset = Offset(endX, endY); + + _gridPaint + ..color = data.gridBorderData.color + ..strokeWidth = data.gridBorderData.width; + canvasWrapper.drawLine(centerOffset, gridOffset, _gridPaint); + } + } + + @visibleForTesting + void drawTitles( + BuildContext context, + CanvasWrapper canvasWrapper, + PaintHolder holder, + ) { + final data = holder.data; + if (data.getTitle == null) return; + + final size = canvasWrapper.size; + + final centerX = radarCenterX(size); + final centerY = radarCenterY(size); + + /// controls Radar chart size + final radius = radarRadius(size); + + final diffAngle = (2 * pi) / data.titleCount; + + final style = Utils().getThemeAwareTextStyle(context, data.titleTextStyle); + + _titleTextPaint + ..textAlign = TextAlign.center + ..textDirection = TextDirection.ltr + ..textScaler = holder.textScaler; + + for (var index = 0; index < data.titleCount; index++) { + final baseTitleAngle = Utils().degrees(diffAngle * index); + final title = data.getTitle!(index, baseTitleAngle); + final span = + TextSpan(text: title.text, children: title.children, style: style); + _titleTextPaint + ..text = span + ..layout(); + final angle = diffAngle * index - pi / 2; + final threshold = 1.0 + + (title.positionPercentageOffset ?? + data.titlePositionPercentageOffset); + final titleX = centerX + + cos(angle) * (radius * threshold + (_titleTextPaint.height / 2)); + final titleY = centerY + + sin(angle) * (radius * threshold + (_titleTextPaint.height / 2)); + + final rect = Rect.fromLTWH( + titleX, + titleY, + _titleTextPaint.width, + _titleTextPaint.height, + ); + final rectDrawOffset = Offset(rect.left, rect.top); + + final drawTitleDegrees = (angle * 180 / pi) + 90; + canvasWrapper.drawRotated( + size: rect.size, + rotationOffset: Offset( + -rect.width / 2, + -rect.height / 2, + ), + drawOffset: rectDrawOffset, + angle: drawTitleDegrees, + drawCallback: () { + canvasWrapper.drawText( + _titleTextPaint, + rect.topLeft, + title.angle - baseTitleAngle, + ); + }, + ); + } + } + + @visibleForTesting + void drawDataSets( + CanvasWrapper canvasWrapper, + PaintHolder holder, + ) { + final data = holder.data; + // we will use dataSetsPosition to draw the graphs + dataSetsPosition ??= calculateDataSetsPosition(canvasWrapper.size, holder); + + final size = canvasWrapper.size; + final centerX = radarCenterX(size); + final centerY = radarCenterY(size); + final centerOffset = Offset(centerX, centerY); + final radius = radarRadius(size); + + dataSetsPosition!.asMap().forEach((index, dataSetOffset) { + final graph = data.dataSets[index]; + // if fillGradient exists + if (graph.fillGradient != null) { + // Create the shader + final rect = Rect.fromCircle(center: centerOffset, radius: radius); + _graphPaint + ..shader = graph.fillGradient!.createShader(rect) + ..style = PaintingStyle.fill; + } else { + // else solid fill color + _graphPaint + ..color = graph.fillColor + ..style = PaintingStyle.fill; + } + _graphBorderPaint + ..color = graph.borderColor + ..style = PaintingStyle.stroke + ..strokeWidth = graph.borderWidth; + + _graphPointPaint + ..color = _graphBorderPaint.color + ..style = PaintingStyle.fill; + + final path = Path(); + + final firstOffset = Offset( + dataSetOffset.entriesOffset.first.dx, + dataSetOffset.entriesOffset.first.dy, + ); + + path.moveTo(firstOffset.dx, firstOffset.dy); + + canvasWrapper.drawCircle( + firstOffset, + graph.entryRadius, + _graphPointPaint, + ); + dataSetOffset.entriesOffset.asMap().forEach((index, pointOffset) { + if (index == 0) return; + + path.lineTo(pointOffset.dx, pointOffset.dy); + + canvasWrapper.drawCircle( + pointOffset, + graph.entryRadius, + _graphPointPaint, + ); + }); + + path.close(); + canvasWrapper + ..drawPath(path, _graphPaint) + ..drawPath(path, _graphBorderPaint); + }); + } + + RadarTouchedSpot? handleTouch( + Offset touchedPoint, + Size viewSize, + PaintHolder holder, + ) { + final targetData = holder.targetData; + dataSetsPosition ??= calculateDataSetsPosition(viewSize, holder); + + for (var i = 0; i < dataSetsPosition!.length; i++) { + final dataSetPosition = dataSetsPosition![i]; + for (var j = 0; j < dataSetPosition.entriesOffset.length; j++) { + final entryOffset = dataSetPosition.entriesOffset[j]; + if ((touchedPoint.dx - entryOffset.dx).abs() <= + targetData.radarTouchData.touchSpotThreshold && + (touchedPoint.dy - entryOffset.dy).abs() <= + targetData.radarTouchData.touchSpotThreshold) { + return RadarTouchedSpot( + targetData.dataSets[i], + i, + targetData.dataSets[i].dataEntries[j], + j, + FlSpot(entryOffset.dx, entryOffset.dy), + entryOffset, + ); + } + } + } + return null; + } + + @visibleForTesting + double radarCenterY(Size size) => size.height / 2.0; + + @visibleForTesting + double radarCenterX(Size size) => size.width / 2.0; + + @visibleForTesting + double radarRadius(Size size) => + min(radarCenterX(size), radarCenterY(size)) * 0.8; + + @visibleForTesting + List calculateDataSetsPosition( + Size viewSize, + PaintHolder holder, + ) { + final data = holder.data; + final centerX = radarCenterX(viewSize); + final centerY = radarCenterY(viewSize); + final radius = radarRadius(viewSize); + + final angle = (2 * pi) / data.titleCount; + + final dataSetsPosition = List.filled( + data.dataSets.length, + const RadarDataSetsPosition([]), + ); + for (var i = 0; i < data.dataSets.length; i++) { + final dataSet = data.dataSets[i]; + final entriesOffset = + List.filled(dataSet.dataEntries.length, Offset.zero); + + for (var j = 0; j < dataSet.dataEntries.length; j++) { + final point = dataSet.dataEntries[j]; + + final xAngle = cos(angle * j - pi / 2); + final yAngle = sin(angle * j - pi / 2); + final scaledPoint = getScaledPoint(point, radius, data); + + final entryOffset = Offset( + centerX + scaledPoint * xAngle, + centerY + scaledPoint * yAngle, + ); + + entriesOffset[j] = entryOffset; + } + dataSetsPosition[i] = RadarDataSetsPosition(entriesOffset); + } + + return dataSetsPosition; + } +} + +class RadarDataSetsPosition { + const RadarDataSetsPosition(this.entriesOffset); + + final List entriesOffset; +} diff --git a/lib/src/chart/radar_chart/radar_chart_renderer.dart b/lib/src/chart/radar_chart/radar_chart_renderer.dart new file mode 100644 index 0000000..ec8f9a3 --- /dev/null +++ b/lib/src/chart/radar_chart/radar_chart_renderer.dart @@ -0,0 +1,114 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart'; +import 'package:fl_chart/src/chart/base/base_chart/render_base_chart.dart'; +import 'package:fl_chart/src/chart/radar_chart/radar_chart_painter.dart'; +import 'package:fl_chart/src/utils/canvas_wrapper.dart'; +import 'package:flutter/cupertino.dart'; + +// coverage:ignore-start + +/// Low level RadarChart Widget. +class RadarChartLeaf extends LeafRenderObjectWidget { + const RadarChartLeaf({ + super.key, + required this.data, + required this.targetData, + }); + + final RadarChartData data; + final RadarChartData targetData; + + @override + RenderRadarChart createRenderObject(BuildContext context) => RenderRadarChart( + context, + data, + targetData, + MediaQuery.of(context).textScaler, + ); + + @override + void updateRenderObject(BuildContext context, RenderRadarChart renderObject) { + renderObject + ..data = data + ..targetData = targetData + ..textScaler = MediaQuery.of(context).textScaler + ..buildContext = context; + } +} +// coverage:ignore-end + +/// Renders our RadarChart, also handles hitTest. +class RenderRadarChart extends RenderBaseChart { + RenderRadarChart( + BuildContext context, + RadarChartData data, + RadarChartData targetData, + TextScaler textScaler, + ) : _data = data, + _targetData = targetData, + _textScaler = textScaler, + super(targetData.radarTouchData, context, canBeScaled: false); + + RadarChartData get data => _data; + RadarChartData _data; + + set data(RadarChartData value) { + if (_data == value) return; + _data = value; + markNeedsPaint(); + } + + RadarChartData get targetData => _targetData; + RadarChartData _targetData; + + set targetData(RadarChartData value) { + if (_targetData == value) return; + _targetData = value; + super.updateBaseTouchData(_targetData.radarTouchData); + markNeedsPaint(); + } + + TextScaler get textScaler => _textScaler; + TextScaler _textScaler; + + set textScaler(TextScaler value) { + if (_textScaler == value) return; + _textScaler = value; + markNeedsPaint(); + } + + // We couldn't mock [size] property of this class, that's why we have this + @visibleForTesting + Size? mockTestSize; + + @visibleForTesting + RadarChartPainter painter = RadarChartPainter(); + + PaintHolder get paintHolder => + PaintHolder(data, targetData, textScaler); + + @override + void paint(PaintingContext context, Offset offset) { + final canvas = context.canvas + ..save() + ..translate(offset.dx, offset.dy); + painter.paint( + buildContext, + CanvasWrapper(canvas, mockTestSize ?? size), + paintHolder, + ); + canvas.restore(); + } + + @override + RadarTouchResponse getResponseAtLocation(Offset localPosition) { + return RadarTouchResponse( + touchLocation: localPosition, + touchedSpot: painter.handleTouch( + localPosition, + mockTestSize ?? size, + paintHolder, + ), + ); + } +} diff --git a/lib/src/chart/radar_chart/radar_extension.dart b/lib/src/chart/radar_chart/radar_extension.dart new file mode 100644 index 0000000..f0eb734 --- /dev/null +++ b/lib/src/chart/radar_chart/radar_extension.dart @@ -0,0 +1,15 @@ +import 'package:fl_chart/src/chart/radar_chart/radar_chart_data.dart'; + +/// Defines extensions on the [List] +extension DashedPath on List { + /// check all the [RadarDataSet] has a same [dataEntries] length + bool get hasEqualDataEntriesLength { + if (length == 0) return false; + + final firstDataEntriesLength = this[0].dataEntries.length; + + return every( + (element) => element.dataEntries.length == firstDataEntriesLength, + ); + } +} diff --git a/lib/src/chart/scatter_chart/scatter_chart.dart b/lib/src/chart/scatter_chart/scatter_chart.dart new file mode 100644 index 0000000..29819f9 --- /dev/null +++ b/lib/src/chart/scatter_chart/scatter_chart.dart @@ -0,0 +1,130 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/base/axis_chart/axis_chart_scaffold_widget.dart'; +import 'package:fl_chart/src/chart/scatter_chart/scatter_chart_renderer.dart'; +import 'package:flutter/cupertino.dart'; + +/// Renders a pie chart as a widget, using provided [ScatterChartData]. +class ScatterChart extends ImplicitlyAnimatedWidget { + /// [data] determines how the [ScatterChart] should be look like, + /// when you make any change in the [ScatterChartData], it updates + /// new values with animation, and duration is [duration]. + /// also you can change the [curve] + /// which default is [Curves.linear]. + const ScatterChart( + this.data, { + this.chartRendererKey, + super.key, + @Deprecated('Please use [duration] instead') + Duration? swapAnimationDuration, + Duration duration = const Duration(milliseconds: 150), + @Deprecated('Please use [curve] instead') Curve? swapAnimationCurve, + Curve curve = Curves.linear, + this.transformationConfig = const FlTransformationConfig(), + }) : super( + duration: swapAnimationDuration ?? duration, + curve: swapAnimationCurve ?? curve, + ); + + /// Determines how the [ScatterChart] should be look like. + final ScatterChartData data; + + /// {@macro fl_chart.AxisChartScaffoldWidget.transformationConfig} + final FlTransformationConfig transformationConfig; + + /// We pass this key to our renderers which are responsible to + /// render the chart itself (without anything around the chart). + final Key? chartRendererKey; + + /// Creates a [_ScatterChartState] + @override + _ScatterChartState createState() => _ScatterChartState(); +} + +class _ScatterChartState extends AnimatedWidgetBaseState { + /// we handle under the hood animations (implicit animations) via this tween, + /// it lerps between the old [ScatterChartData] to the new one. + ScatterChartDataTween? _scatterChartDataTween; + + /// If [ScatterTouchData.handleBuiltInTouches] is true, we override the callback to handle touches internally, + /// but we need to keep the provided callback to notify it too. + BaseTouchCallback? _providedTouchCallback; + + List touchedSpots = []; + + @override + Widget build(BuildContext context) { + final showingData = _getData(); + + return AxisChartScaffoldWidget( + data: showingData, + transformationConfig: widget.transformationConfig, + chartBuilder: (context, chartVirtualRect) => ScatterChartLeaf( + data: + _withTouchedIndicators(_scatterChartDataTween!.evaluate(animation)), + targetData: _withTouchedIndicators(showingData), + key: widget.chartRendererKey, + chartVirtualRect: chartVirtualRect, + canBeScaled: widget.transformationConfig.scaleAxis != FlScaleAxis.none, + ), + ); + } + + ScatterChartData _withTouchedIndicators(ScatterChartData scatterChartData) { + if (!scatterChartData.scatterTouchData.enabled || + !scatterChartData.scatterTouchData.handleBuiltInTouches) { + return scatterChartData; + } + + return scatterChartData.copyWith( + showingTooltipIndicators: touchedSpots, + ); + } + + ScatterChartData _getData() { + final scatterTouchData = widget.data.scatterTouchData; + if (scatterTouchData.enabled && scatterTouchData.handleBuiltInTouches) { + _providedTouchCallback = scatterTouchData.touchCallback; + return widget.data.copyWith( + scatterTouchData: widget.data.scatterTouchData + .copyWith(touchCallback: _handleBuiltInTouch), + ); + } + return widget.data; + } + + void _handleBuiltInTouch( + FlTouchEvent event, + ScatterTouchResponse? touchResponse, + ) { + if (!mounted) { + return; + } + _providedTouchCallback?.call(event, touchResponse); + + final desiredTouch = event.isInterestedForInteractions; + + if (!desiredTouch || + touchResponse == null || + touchResponse.touchedSpot == null) { + setState(() { + touchedSpots = []; + }); + return; + } + setState(() { + touchedSpots = [touchResponse.touchedSpot!.spotIndex]; + }); + } + + @override + void forEachTween(TweenVisitor visitor) { + _scatterChartDataTween = visitor( + _scatterChartDataTween, + _getData(), + (dynamic value) => ScatterChartDataTween( + begin: value as ScatterChartData, + end: widget.data, + ), + ) as ScatterChartDataTween?; + } +} diff --git a/lib/src/chart/scatter_chart/scatter_chart_data.dart b/lib/src/chart/scatter_chart/scatter_chart_data.dart new file mode 100644 index 0000000..69aea51 --- /dev/null +++ b/lib/src/chart/scatter_chart/scatter_chart_data.dart @@ -0,0 +1,796 @@ +// coverage:ignore-file +import 'dart:ui'; + +import 'package:equatable/equatable.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/scatter_chart/scatter_chart_helper.dart'; +import 'package:fl_chart/src/extensions/color_extension.dart'; +import 'package:fl_chart/src/utils/lerp.dart'; +import 'package:flutter/material.dart'; + +/// [ScatterChart] needs this class to render itself. +/// +/// It holds data needed to draw a scatter chart, +/// including background color, scatter spots, ... +class ScatterChartData extends AxisChartData with EquatableMixin { + /// [ScatterChart] draws some points in a square space, + /// points are defined by [scatterSpots], + /// + /// It draws some titles on left, top, right, bottom sides per each axis number, + /// you can modify [titlesData] to have your custom titles, + /// also you can define the axis title (one text per axis) for each side + /// using [axisTitleData], you can restrict the y axis using [minY] and [maxY] value, + /// and restrict x axis using [minX] and [maxX]. + /// + /// It draws a color as a background behind everything you can set it using [backgroundColor], + /// then a grid over it, you can customize it using [gridData], + /// and it draws 4 borders around your chart, you can customize it using [borderData]. + /// + /// You can modify [scatterTouchData] to customize touch behaviors and responses. + /// + /// You can show some tooltipIndicators (a popup with an information) + /// on top of each [ScatterChartData.scatterSpots] using [showingTooltipIndicators], + /// just put spot indices you want to show it on top of them. + /// + /// [clipData] forces the [LineChart] to draw lines inside the chart bounding box. + ScatterChartData({ + List? scatterSpots, + FlTitlesData? titlesData, + ScatterTouchData? scatterTouchData, + List? showingTooltipIndicators, + FlGridData? gridData, + super.borderData, + double? minX, + double? maxX, + super.baselineX, + double? minY, + double? maxY, + super.baselineY, + FlClipData? clipData, + super.backgroundColor, + ScatterLabelSettings? scatterLabelSettings, + super.rotationQuarterTurns, + this.errorIndicatorData = + const FlErrorIndicatorData(), + }) : scatterSpots = scatterSpots ?? const [], + scatterTouchData = scatterTouchData ?? ScatterTouchData(), + showingTooltipIndicators = showingTooltipIndicators ?? const [], + scatterLabelSettings = scatterLabelSettings ?? ScatterLabelSettings(), + super( + gridData: gridData ?? const FlGridData(), + titlesData: titlesData ?? const FlTitlesData(), + clipData: clipData ?? const FlClipData.none(), + minX: minX ?? + ScatterChartHelper.calculateMaxAxisValues( + scatterSpots ?? const [], + ).$1, + maxX: maxX ?? + ScatterChartHelper.calculateMaxAxisValues( + scatterSpots ?? const [], + ).$2, + minY: minY ?? + ScatterChartHelper.calculateMaxAxisValues( + scatterSpots ?? const [], + ).$3, + maxY: maxY ?? + ScatterChartHelper.calculateMaxAxisValues( + scatterSpots ?? const [], + ).$4, + ); + final List scatterSpots; + final ScatterTouchData scatterTouchData; + + /// you can show some tooltipIndicators (a popup with an information) + /// on top of each [ScatterSpot] using [showingTooltipIndicators], + /// just put indices you want to show it on top of them. + /// + /// An important point is that you have to disable the default touch behaviour + /// to show the tooltip manually, see [ScatterTouchData.handleBuiltInTouches]. + final List showingTooltipIndicators; + + final ScatterLabelSettings scatterLabelSettings; + + /// Holds data for showing error indicators on the [scatterSpots] + final FlErrorIndicatorData + errorIndicatorData; + + /// Lerps a [ScatterChartData] based on [t] value, check [Tween.lerp]. + @override + ScatterChartData lerp(BaseChartData a, BaseChartData b, double t) { + if (a is ScatterChartData && b is ScatterChartData) { + return ScatterChartData( + scatterSpots: lerpScatterSpotList(a.scatterSpots, b.scatterSpots, t), + titlesData: FlTitlesData.lerp(a.titlesData, b.titlesData, t), + scatterTouchData: b.scatterTouchData, + showingTooltipIndicators: lerpIntList( + a.showingTooltipIndicators, + b.showingTooltipIndicators, + t, + ), + gridData: FlGridData.lerp(a.gridData, b.gridData, t), + borderData: FlBorderData.lerp(a.borderData, b.borderData, t), + minX: lerpDouble(a.minX, b.minX, t), + maxX: lerpDouble(a.maxX, b.maxX, t), + baselineX: lerpDouble(a.baselineX, b.baselineX, t), + minY: lerpDouble(a.minY, b.minY, t), + maxY: lerpDouble(a.maxY, b.maxY, t), + baselineY: lerpDouble(a.baselineY, b.baselineY, t), + clipData: b.clipData, + backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), + scatterLabelSettings: ScatterLabelSettings.lerp( + a.scatterLabelSettings, + b.scatterLabelSettings, + t, + ), + rotationQuarterTurns: b.rotationQuarterTurns, + errorIndicatorData: FlErrorIndicatorData.lerp( + a.errorIndicatorData, + b.errorIndicatorData, + t, + ), + ); + } else { + throw Exception('Illegal State'); + } + } + + /// Copies current [ScatterChartData] to a new [ScatterChartData], + /// and replaces provided values. + ScatterChartData copyWith({ + List? scatterSpots, + FlTitlesData? titlesData, + ScatterTouchData? scatterTouchData, + List? showingTooltipIndicators, + FlGridData? gridData, + FlBorderData? borderData, + double? minX, + double? maxX, + double? baselineX, + double? minY, + double? maxY, + double? baselineY, + FlClipData? clipData, + Color? backgroundColor, + ScatterLabelSettings? scatterLabelSettings, + int? rotationQuarterTurns, + FlErrorIndicatorData? + errorIndicatorData, + }) => + ScatterChartData( + scatterSpots: scatterSpots ?? this.scatterSpots, + titlesData: titlesData ?? this.titlesData, + scatterTouchData: scatterTouchData ?? this.scatterTouchData, + showingTooltipIndicators: + showingTooltipIndicators ?? this.showingTooltipIndicators, + gridData: gridData ?? this.gridData, + borderData: borderData ?? this.borderData, + minX: minX ?? this.minX, + maxX: maxX ?? this.maxX, + baselineX: baselineX ?? this.baselineX, + minY: minY ?? this.minY, + maxY: maxY ?? this.maxY, + baselineY: baselineY ?? this.baselineY, + clipData: clipData ?? this.clipData, + backgroundColor: backgroundColor ?? this.backgroundColor, + scatterLabelSettings: scatterLabelSettings ?? this.scatterLabelSettings, + rotationQuarterTurns: rotationQuarterTurns ?? this.rotationQuarterTurns, + errorIndicatorData: errorIndicatorData ?? this.errorIndicatorData, + ); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + scatterSpots, + scatterTouchData, + showingTooltipIndicators, + gridData, + titlesData, + rangeAnnotations, + minX, + maxX, + baselineX, + minY, + maxY, + baselineY, + rangeAnnotations, + scatterLabelSettings, + clipData, + backgroundColor, + borderData, + rotationQuarterTurns, + errorIndicatorData, + ]; +} + +/// Defines information about a spot in the [ScatterChart] +class ScatterSpot extends FlSpot with EquatableMixin { + /// You can change [show] value to show or hide the spot, + /// [x], and [y] defines the location of spot in the [ScatterChart], + /// [radius] defines the size of spot, and [color] defines the color of it. + ScatterSpot( + super.x, + super.y, { + bool? show, + int? renderPriority, + FlDotPainter? dotPainter, + super.xError, + super.yError, + }) : show = show ?? true, + renderPriority = renderPriority ?? 0, + dotPainter = dotPainter ?? + FlDotCirclePainter( + radius: 6, + color: + Colors.primaries[((x * y) % Colors.primaries.length).toInt()], + ); + + /// Determines show or hide the spot. + final bool show; + + // Determines Z-Index of the spot + final int renderPriority; + + /// Determines shape of the spot + final FlDotPainter dotPainter; + + Size get size => dotPainter.getSize(this); + + String get defaultLabel { + if (dotPainter is FlDotCirclePainter) { + return '${(dotPainter as FlDotCirclePainter).radius.toInt()}'; + } else { + return '${x.toInt()}, ${y.toInt()}'; + } + } + + @override + ScatterSpot copyWith({ + double? x, + double? y, + bool? show, + int? renderPriority, + FlDotPainter? dotPainter, + FlErrorRange? xError, + FlErrorRange? yError, + }) => + ScatterSpot( + x ?? this.x, + y ?? this.y, + show: show ?? this.show, + renderPriority: renderPriority ?? this.renderPriority, + dotPainter: dotPainter ?? this.dotPainter, + xError: xError ?? this.xError, + yError: yError ?? this.yError, + ); + + /// Lerps a [ScatterSpot] based on [t] value, check [Tween.lerp]. + static ScatterSpot lerp(ScatterSpot a, ScatterSpot b, double t) => + ScatterSpot( + lerpDouble(a.x, b.x, t)!, + lerpDouble(a.y, b.y, t)!, + show: b.show, + renderPriority: a.renderPriority + + (t * (b.renderPriority - a.renderPriority)).round(), + dotPainter: a.dotPainter.lerp(a.dotPainter, b.dotPainter, t), + xError: FlErrorRange.lerp(a.xError, b.xError, t), + yError: FlErrorRange.lerp(a.yError, b.yError, t), + ); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + x, + y, + show, + renderPriority, + dotPainter, + xError, + yError, + ]; +} + +/// Holds data to handle touch events, and touch responses in the [ScatterChart]. +/// +/// There is a touch flow, explained [here](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/handle_touches.md) +/// in a simple way, each chart's renderer captures the touch events, and passes the pointerEvent +/// to the painter, and gets touched spot, and wraps it into a concrete [ScatterTouchResponse]. +class ScatterTouchData extends FlTouchData + with EquatableMixin { + /// You can disable or enable the touch system using [enabled] flag, + /// + /// [touchCallback] notifies you about the happened touch/pointer events. + /// It gives you a [FlTouchEvent] which is the happened event such as [FlPointerHoverEvent], [FlTapUpEvent], ... + /// It also gives you a [ScatterTouchResponse] which contains information + /// about the elements that has touched. + /// + /// Using [mouseCursorResolver] you can change the mouse cursor + /// based on the provided [FlTouchEvent] and [ScatterTouchResponse] + /// + /// if [handleBuiltInTouches] is true, [ScatterChart] shows a tooltip popup on top of the spots if + /// touch occurs (or you can show it manually using, [ScatterChartData.showingTooltipIndicators]) + /// You can customize this tooltip using [touchTooltipData], + /// + /// If you need to have a distance threshold for handling touches, use [touchSpotThreshold]. + ScatterTouchData({ + bool? enabled, + BaseTouchCallback? touchCallback, + MouseCursorResolver? mouseCursorResolver, + Duration? longPressDuration, + ScatterTouchTooltipData? touchTooltipData, + double? touchSpotThreshold, + bool? handleBuiltInTouches, + }) : touchTooltipData = touchTooltipData ?? const ScatterTouchTooltipData(), + touchSpotThreshold = touchSpotThreshold ?? 0, + handleBuiltInTouches = handleBuiltInTouches ?? true, + super( + enabled ?? true, + touchCallback, + mouseCursorResolver, + longPressDuration, + ); + + /// show a tooltip on touched spots + final ScatterTouchTooltipData touchTooltipData; + + /// we find the nearest spots on touched position based on this threshold + final double touchSpotThreshold; + + /// set this true if you want the built in touch handling + /// (show a tooltip bubble and an indicator on touched spots) + final bool handleBuiltInTouches; + + /// Copies current [ScatterTouchData] to a new [ScatterTouchData], + /// and replaces provided values. + ScatterTouchData copyWith({ + bool? enabled, + BaseTouchCallback? touchCallback, + MouseCursorResolver? mouseCursorResolver, + Duration? longPressDuration, + ScatterTouchTooltipData? touchTooltipData, + double? touchSpotThreshold, + bool? handleBuiltInTouches, + }) => + ScatterTouchData( + enabled: enabled ?? this.enabled, + touchCallback: touchCallback ?? this.touchCallback, + mouseCursorResolver: mouseCursorResolver ?? this.mouseCursorResolver, + longPressDuration: longPressDuration ?? this.longPressDuration, + touchTooltipData: touchTooltipData ?? this.touchTooltipData, + handleBuiltInTouches: handleBuiltInTouches ?? this.handleBuiltInTouches, + touchSpotThreshold: touchSpotThreshold ?? this.touchSpotThreshold, + ); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + enabled, + touchCallback, + mouseCursorResolver, + longPressDuration, + touchTooltipData, + touchSpotThreshold, + handleBuiltInTouches, + ]; +} + +/// [ScatterChart]'s touch callback. +typedef ScatterTouchCallback = void Function(ScatterTouchResponse); + +/// Holds information about touch response in the [ScatterChart]. +/// +/// You can override [ScatterTouchData.touchCallback] to handle touch events, +/// it gives you a [ScatterTouchResponse] and you can do whatever you want. +class ScatterTouchResponse extends AxisBaseTouchResponse { + /// If touch happens, [ScatterChart] processes it internally and + /// passes out a [ScatterTouchResponse], it gives you information about the touched spot. + /// + /// [touchedSpot] tells you + /// in which spot (of [ScatterChartData.scatterSpots]) touch happened. + ScatterTouchResponse({ + required super.touchLocation, + required super.touchChartCoordinate, + required this.touchedSpot, + }); + + final ScatterTouchedSpot? touchedSpot; + + /// Copies current [ScatterTouchResponse] to a new [ScatterTouchResponse], + /// and replaces provided values. + ScatterTouchResponse copyWith({ + Offset? touchLocation, + Offset? touchChartCoordinate, + ScatterTouchedSpot? touchedSpot, + }) => + ScatterTouchResponse( + touchLocation: touchLocation ?? this.touchLocation, + touchChartCoordinate: touchChartCoordinate ?? this.touchChartCoordinate, + touchedSpot: touchedSpot ?? this.touchedSpot, + ); +} + +/// Holds the touched spot data +class ScatterTouchedSpot with EquatableMixin { + /// [spot], and [spotIndex] tells you + /// in which spot (of [ScatterChartData.scatterSpots]) touch happened. + const ScatterTouchedSpot(this.spot, this.spotIndex); + + /// Touch happened on this spot + final ScatterSpot spot; + + /// Touch happened on this spot index + final int spotIndex; + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + spot, + spotIndex, + ]; + + /// Copies current [ScatterTouchedSpot] to a new [ScatterTouchedSpot], + /// and replaces provided values. + ScatterTouchedSpot copyWith({ + ScatterSpot? spot, + int? spotIndex, + }) => + ScatterTouchedSpot(spot ?? this.spot, spotIndex ?? this.spotIndex); +} + +/// Holds representation data for showing tooltip popup on top of spots. +class ScatterTouchTooltipData with EquatableMixin { + /// if [ScatterTouchData.handleBuiltInTouches] is true, + /// [ScatterChart] shows a tooltip popup on top of spots automatically when touch happens, + /// otherwise you can show it manually using [ScatterChartData.showingTooltipIndicators]. + /// Tooltip shows on top of rods, with [getTooltipColor] as a background color. + /// You can set the corner radius using [tooltipBorderRadius], + /// If you want to have a padding inside the tooltip, fill [tooltipPadding]. + /// Content of the tooltip will provide using [getTooltipItems] callback, you can override it + /// and pass your custom data to show in the tooltip. + /// You can restrict the tooltip's width using [maxContentWidth]. + /// Sometimes, [ScatterChart] shows the tooltip outside of the chart, + /// you can set [fitInsideHorizontally] true to force it to shift inside the chart horizontally, + /// also you can set [fitInsideVertically] true to force it to shift inside the chart vertically. + const ScatterTouchTooltipData({ + BorderRadius? tooltipBorderRadius, + EdgeInsets? tooltipPadding, + FLHorizontalAlignment? tooltipHorizontalAlignment, + double? tooltipHorizontalOffset, + double? maxContentWidth, + GetScatterTooltipItems? getTooltipItems, + bool? fitInsideHorizontally, + bool? fitInsideVertically, + double? rotateAngle, + BorderSide? tooltipBorder, + GetScatterTooltipColor? getTooltipColor, + }) : _tooltipBorderRadius = tooltipBorderRadius, + tooltipPadding = tooltipPadding ?? + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + tooltipHorizontalAlignment = + tooltipHorizontalAlignment ?? FLHorizontalAlignment.center, + tooltipHorizontalOffset = tooltipHorizontalOffset ?? 0, + maxContentWidth = maxContentWidth ?? 120, + getTooltipItems = getTooltipItems ?? defaultScatterTooltipItem, + fitInsideHorizontally = fitInsideHorizontally ?? false, + fitInsideVertically = fitInsideVertically ?? false, + rotateAngle = rotateAngle ?? 0.0, + tooltipBorder = tooltipBorder ?? BorderSide.none, + getTooltipColor = getTooltipColor ?? defaultScatterTooltipColor, + super(); + + /// Sets a rounded radius for the tooltip. + final BorderRadius? _tooltipBorderRadius; + + /// Sets a rounded radius for the tooltip. + BorderRadius get tooltipBorderRadius => + _tooltipBorderRadius ?? BorderRadius.circular(4); + + /// Applies a padding for showing contents inside the tooltip. + final EdgeInsets tooltipPadding; + + /// Controls showing tooltip on left side, right side or center aligned with spot, default is center + final FLHorizontalAlignment tooltipHorizontalAlignment; + + /// Applies horizontal offset for showing tooltip, default is zero. + final double tooltipHorizontalOffset; + + /// Restricts the tooltip's width. + final double maxContentWidth; + + /// Retrieves data for showing content inside the tooltip. + final GetScatterTooltipItems getTooltipItems; + + /// Forces the tooltip to shift horizontally inside the chart, if overflow happens. + final bool fitInsideHorizontally; + + /// Forces the tooltip to shift vertically inside the chart, if overflow happens. + final bool fitInsideVertically; + + /// Controls the rotation of the tooltip. + final double rotateAngle; + + /// The tooltip border color. + final BorderSide tooltipBorder; + + /// Retrieves data for showing content inside the tooltip. + final GetScatterTooltipColor getTooltipColor; + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + _tooltipBorderRadius, + tooltipPadding, + tooltipHorizontalAlignment, + tooltipHorizontalOffset, + maxContentWidth, + getTooltipItems, + fitInsideHorizontally, + fitInsideVertically, + rotateAngle, + tooltipBorder, + getTooltipColor, + ]; + + /// Copies current [ScatterTouchTooltipData] to a new [ScatterTouchTooltipData], + /// and replaces provided values. + ScatterTouchTooltipData copyWith({ + EdgeInsets? tooltipPadding, + FLHorizontalAlignment? tooltipHorizontalAlignment, + double? tooltipHorizontalOffset, + double? maxContentWidth, + GetScatterTooltipItems? getTooltipItems, + bool? fitInsideHorizontally, + bool? fitInsideVertically, + double? rotateAngle, + BorderSide? tooltipBorder, + GetScatterTooltipColor? getTooltipColor, + }) => + ScatterTouchTooltipData( + tooltipPadding: tooltipPadding ?? this.tooltipPadding, + tooltipHorizontalAlignment: + tooltipHorizontalAlignment ?? this.tooltipHorizontalAlignment, + tooltipHorizontalOffset: + tooltipHorizontalOffset ?? this.tooltipHorizontalOffset, + maxContentWidth: maxContentWidth ?? this.maxContentWidth, + getTooltipItems: getTooltipItems ?? this.getTooltipItems, + fitInsideHorizontally: + fitInsideHorizontally ?? this.fitInsideHorizontally, + fitInsideVertically: fitInsideVertically ?? this.fitInsideVertically, + rotateAngle: rotateAngle ?? this.rotateAngle, + tooltipBorder: tooltipBorder ?? this.tooltipBorder, + getTooltipColor: getTooltipColor ?? this.getTooltipColor, + ); +} + +/// Provides a [ScatterTooltipItem] for showing content inside the [ScatterTouchTooltipData]. +/// +/// You can override [ScatterTouchTooltipData.getTooltipItems], it gives you +/// [touchedSpot] that touch happened on, +/// then you should and pass your custom [ScatterTooltipItem] +/// to show it inside the tooltip popup. +typedef GetScatterTooltipItems = ScatterTooltipItem? Function( + ScatterSpot touchedSpot, +); + +/// Default implementation for [ScatterTouchTooltipData.getTooltipItems]. +ScatterTooltipItem? defaultScatterTooltipItem(ScatterSpot touchedSpot) { + final textStyle = TextStyle( + color: touchedSpot.dotPainter.mainColor, + fontWeight: FontWeight.bold, + fontSize: 14, + ); + String text; + if (touchedSpot.dotPainter is FlDotCirclePainter) { + text = '${(touchedSpot.dotPainter as FlDotCirclePainter).radius.toInt()}'; + } else { + text = '${touchedSpot.x.toInt()}, ${touchedSpot.y.toInt()}'; + } + return ScatterTooltipItem( + text, + textStyle: textStyle, + ); +} + +/// Provides a [Color] to show different background color inside the [ScatterTouchTooltipData]. +/// +/// You can override [ScatterTouchTooltipData.getTooltipColor], it gives you +/// [touchedSpot] that touch happened on, +/// then you should and pass your custom [Color] +/// to show it inside the tooltip popup. +typedef GetScatterTooltipColor = Color Function( + ScatterSpot touchedSpot, +); + +/// Default implementation for [ScatterTouchTooltipData.getTooltipItems]. +Color defaultScatterTooltipColor(ScatterSpot touchedSpot) => + Colors.blueGrey.darken(15); + +/// Holds data of showing each item in the tooltip popup. +class ScatterTooltipItem with EquatableMixin { + /// Shows a [text] with [textStyle], [textDirection], and optional [children] in the tooltip popup, + /// [bottomMargin] is the bottom space from spot. + ScatterTooltipItem( + this.text, { + this.textStyle, + double? bottomMargin, + TextAlign? textAlign, + TextDirection? textDirection, + this.children, + }) : bottomMargin = bottomMargin ?? 8, + textAlign = textAlign ?? TextAlign.center, + textDirection = textDirection ?? TextDirection.ltr; + + /// Showing text. + final String text; + + /// Style of showing text. + final TextStyle? textStyle; + + /// Defines bottom space from spot. + final double bottomMargin; + + /// TextAlign of the showing content. + final TextAlign textAlign; + + /// Direction of showing text. + final TextDirection textDirection; + + /// Add further style and format to the text of the tooltip + final List? children; + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + text, + textStyle, + bottomMargin, + textAlign, + textDirection, + children, + ]; + + /// Copies current [ScatterTooltipItem] to a new [ScatterTooltipItem], + /// and replaces provided values. + ScatterTooltipItem copyWith({ + String? text, + TextStyle? textStyle, + double? bottomMargin, + TextAlign? textAlign, + TextDirection? textDirection, + List? children, + }) => + ScatterTooltipItem( + text ?? this.text, + textStyle: textStyle ?? this.textStyle, + bottomMargin: bottomMargin ?? this.bottomMargin, + textAlign: textAlign ?? this.textAlign, + textDirection: textDirection ?? this.textDirection, + children: children ?? this.children, + ); +} + +/// It lerps a [ScatterChartData] to another [ScatterChartData] (handles animation for updating values) +class ScatterChartDataTween extends Tween { + ScatterChartDataTween({ + required ScatterChartData begin, + required ScatterChartData end, + }) : super(begin: begin, end: end); + + /// Lerps a [ScatterChartData] based on [t] value, check [Tween.lerp]. + @override + ScatterChartData lerp(double t) => begin!.lerp(begin!, end!, t); +} + +/// It gives you the index value as well as the spot and gets the text style of the label. +typedef GetLabelTextStyleFunction = TextStyle? Function( + int spotIndex, + ScatterSpot spot, +); + +/// It gives you the index value as well as the spot and returns the label of the spot. +typedef GetLabelFunction = String Function( + int spotIndex, + ScatterSpot spot, +); + +/// It gives you the default text style of the label for a spot. +TextStyle? getDefaultLabelTextStyleFunction( + int spotIndex, + ScatterSpot spot, +) { + return null; +} + +/// It gives you the default label of the spot. +String getDefaultLabelFunction( + int spotIndex, + ScatterSpot spot, +) => + spot.defaultLabel; + +/// Defines information about the labels in the [ScatterChart] +class ScatterLabelSettings with EquatableMixin { + /// You can change [showLabel] value to show or hide the label, + /// [textStyle] defines the style of label in the [ScatterChart]. + ScatterLabelSettings({ + bool? showLabel, + GetLabelTextStyleFunction? getLabelTextStyleFunction, + GetLabelFunction? getLabelFunction, + TextDirection? textDirection, + }) : showLabel = showLabel ?? false, + getLabelTextStyleFunction = + getLabelTextStyleFunction ?? getDefaultLabelTextStyleFunction, + getLabelFunction = getLabelFunction ?? getDefaultLabelFunction, + textDirection = textDirection ?? TextDirection.ltr; + + /// Determines whether to show or hide the labels. + final bool showLabel; + + /// This function gives you the index value of the spot in the list and returns the text style. + final GetLabelTextStyleFunction getLabelTextStyleFunction; + + /// This function gives you the index value of the spot in the list and returns the label. + final GetLabelFunction getLabelFunction; + + /// Determines the direction of the text for the labels. + final TextDirection textDirection; + + ScatterLabelSettings copyWith({ + bool? showLabel, + GetLabelTextStyleFunction? getLabelTextStyleFunction, + GetLabelFunction? getLabelFunction, + TextDirection? textDirection, + }) { + return ScatterLabelSettings( + showLabel: showLabel ?? this.showLabel, + getLabelTextStyleFunction: + getLabelTextStyleFunction ?? this.getLabelTextStyleFunction, + getLabelFunction: getLabelFunction ?? this.getLabelFunction, + textDirection: textDirection ?? this.textDirection, + ); + } + + /// Lerps a [ScatterLabelSettings] based on [t] value, check [Tween.lerp]. + static ScatterLabelSettings lerp( + ScatterLabelSettings a, + ScatterLabelSettings b, + double t, + ) => + ScatterLabelSettings( + showLabel: b.showLabel, + getLabelTextStyleFunction: b.getLabelTextStyleFunction, + getLabelFunction: b.getLabelFunction, + textDirection: b.textDirection, + ); + + /// Used for equality check, see [EquatableMixin]. + @override + List get props => [ + showLabel, + getLabelTextStyleFunction, + getLabelFunction, + textDirection, + ]; +} + +/// It is the input of the [GetSpotRangeErrorPainter] callback in +/// the [ScatterChartData.errorIndicatorData] +/// +/// It contains the [spot] and [spotIndex] that the error range +/// should be drawn for. +/// It works based on the [ScatterSpot.xError] and [ScatterSpot.yError] values. +class ScatterChartSpotErrorRangeCallbackInput + extends FlSpotErrorRangeCallbackInput { + ScatterChartSpotErrorRangeCallbackInput({ + required this.spot, + required this.spotIndex, + }); + + final ScatterSpot spot; + final int spotIndex; + + @override + List get props => [ + spot, + spotIndex, + ]; +} diff --git a/lib/src/chart/scatter_chart/scatter_chart_helper.dart b/lib/src/chart/scatter_chart/scatter_chart_helper.dart new file mode 100644 index 0000000..547df66 --- /dev/null +++ b/lib/src/chart/scatter_chart/scatter_chart_helper.dart @@ -0,0 +1,43 @@ +import 'package:fl_chart/src/chart/scatter_chart/scatter_chart_data.dart'; + +/// Contains anything that helps ScatterChart works +class ScatterChartHelper { + /// Calculates minX, maxX, minY, and maxY based on [scatterSpots], + /// returns cached values, to prevent redundant calculations. + static ( + double minX, + double maxX, + double minY, + double maxY, + ) calculateMaxAxisValues( + List scatterSpots, + ) { + if (scatterSpots.isEmpty) { + return (0, 0, 0, 0); + } + + var minX = scatterSpots[0].x; + var maxX = scatterSpots[0].x; + var minY = scatterSpots[0].y; + var maxY = scatterSpots[0].y; + for (var j = 0; j < scatterSpots.length; j++) { + final spot = scatterSpots[j]; + if (spot.x > maxX) { + maxX = spot.x; + } + + if (spot.x < minX) { + minX = spot.x; + } + + if (spot.y > maxY) { + maxY = spot.y; + } + + if (spot.y < minY) { + minY = spot.y; + } + } + return (minX, maxX, minY, maxY); + } +} diff --git a/lib/src/chart/scatter_chart/scatter_chart_painter.dart b/lib/src/chart/scatter_chart/scatter_chart_painter.dart new file mode 100644 index 0000000..2110ba3 --- /dev/null +++ b/lib/src/chart/scatter_chart/scatter_chart_painter.dart @@ -0,0 +1,472 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/base/axis_chart/axis_chart_painter.dart'; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart'; +import 'package:fl_chart/src/utils/canvas_wrapper.dart'; +import 'package:fl_chart/src/utils/utils.dart'; +import 'package:flutter/material.dart'; + +/// Paints [ScatterChartData] in the canvas, it can be used in a [CustomPainter] +class ScatterChartPainter extends AxisChartPainter { + /// Paints [dataList] into canvas, it is the animating [ScatterChartData], + /// [targetData] is the animation's target and remains the same + /// during animation, then we should use it when we need to show + /// tooltips or something like that, because [dataList] is changing constantly. + /// + /// [textScale] used for scaling texts inside the chart, + /// parent can use [MediaQuery.textScaleFactor] to respect + /// the system's font size. + ScatterChartPainter() : super() { + _bgTouchTooltipPaint = Paint() + ..style = PaintingStyle.fill + ..color = Colors.white; + + _borderTouchTooltipPaint = Paint() + ..style = PaintingStyle.stroke + ..color = Colors.transparent + ..strokeWidth = 1.0; + + _clipPaint = Paint(); + } + + late Paint _bgTouchTooltipPaint; + late Paint _borderTouchTooltipPaint; + late Paint _clipPaint; + + /// Paints [ScatterChartData] into the provided canvas. + @override + void paint( + BuildContext context, + CanvasWrapper canvasWrapper, + PaintHolder holder, + ) { + if (holder.chartVirtualRect != null) { + canvasWrapper + ..saveLayer( + Offset.zero & canvasWrapper.size, + _clipPaint, + ) + ..clipRect(Offset.zero & canvasWrapper.size); + } + super.paint(context, canvasWrapper, holder); + drawSpots(context, canvasWrapper, holder); + + if (holder.chartVirtualRect != null) { + canvasWrapper.restore(); + } + + drawTouchTooltips(context, canvasWrapper, holder); + } + + @visibleForTesting + void drawSpots( + BuildContext context, + CanvasWrapper canvasWrapper, + PaintHolder holder, + ) { + final data = holder.data; + final viewSize = canvasWrapper.size; + final clip = data.clipData; + final border = data.borderData.show ? data.borderData.border : null; + + if (data.clipData.any) { + canvasWrapper.saveLayer( + Rect.fromLTRB( + 0, + 0, + canvasWrapper.size.width, + canvasWrapper.size.height, + ), + _clipPaint, + ); + + var left = 0.0; + var top = 0.0; + var right = viewSize.width; + var bottom = viewSize.height; + + if (clip.left) { + final borderWidth = border?.left.width ?? 0; + left = borderWidth / 2; + } + if (clip.top) { + final borderWidth = border?.top.width ?? 0; + top = borderWidth / 2; + } + if (clip.right) { + final borderWidth = border?.right.width ?? 0; + right = viewSize.width - (borderWidth / 2); + } + if (clip.bottom) { + final borderWidth = border?.bottom.width ?? 0; + bottom = viewSize.height - (borderWidth / 2); + } + + canvasWrapper.clipRect(Rect.fromLTRB(left, top, right, bottom)); + } + + for (final scatterSpot in data.scatterSpots) { + if (!scatterSpot.show) { + continue; + } + final pixelX = getPixelX(scatterSpot.x, viewSize, holder); + final pixelY = getPixelY(scatterSpot.y, viewSize, holder); + + canvasWrapper.drawDot( + scatterSpot.dotPainter, + scatterSpot, + Offset(pixelX, pixelY), + ); + } + + drawScatterErrorBars(canvasWrapper, holder); + + if (data.scatterLabelSettings.showLabel) { + for (var i = 0; i < data.scatterSpots.length; i++) { + final scatterSpot = data.scatterSpots[i]; + final spotIndex = i; + + final label = + data.scatterLabelSettings.getLabelFunction(spotIndex, scatterSpot); + + if (label.isEmpty || !scatterSpot.show) { + continue; + } + + final span = TextSpan( + text: label, + style: Utils().getThemeAwareTextStyle( + context, + data.scatterLabelSettings.getLabelTextStyleFunction( + spotIndex, + scatterSpot, + ), + ), + ); + + final tp = TextPainter( + text: span, + textAlign: TextAlign.center, + textDirection: holder.data.scatterLabelSettings.textDirection, + textScaler: holder.textScaler, + )..layout(maxWidth: viewSize.width); + + final pixelX = getPixelX(scatterSpot.x, viewSize, holder); + final pixelY = getPixelY(scatterSpot.y, viewSize, holder); + + double newPixelY; + + /// To ensure the label is centered horizontally with respect to the spot. + final newPixelX = pixelX - tp.width / 2; + + final centerChartY = viewSize.height / 2; + + final radius = scatterSpot.dotPainter.getSize(scatterSpot).width / 2; + + /// if the spot is in the lower half of the chart, then draw the label either in the center or above the spot, + /// if the spot is in upper half of the chart, then draw the label either in the center or below the spot. + if (pixelY > centerChartY) { + /// if either the height or the width of the spot is greater than the radius of the spot, then draw the label above the bubble, + /// else draw the label inside the bubble. + final off = (radius * 1.5 < tp.height || radius * 1.5 < tp.width) + ? radius + tp.height + : tp.height / 2; + + newPixelY = pixelY - off; + } else { + /// if either the height or the width of the spot is greater than the radius of the spot, then draw the label below the bubble, + /// else draw the label inside the bubble. + final off = (radius * 1.5 < tp.height || radius * 1.5 < tp.width) + ? radius + : -tp.height / 2; + newPixelY = pixelY + off; + } + + canvasWrapper.drawText( + tp, + Offset(newPixelX, newPixelY), + ); + } + } + + if (data.clipData.any) { + canvasWrapper.restore(); + } + } + + @visibleForTesting + void drawScatterErrorBars( + CanvasWrapper canvasWrapper, + PaintHolder holder, + ) { + final data = holder.data; + final viewSize = canvasWrapper.size; + + final errorIndicatorData = data.errorIndicatorData; + if (!errorIndicatorData.show) { + return; + } + for (var i = 0; i < data.scatterSpots.length; i++) { + final spot = data.scatterSpots[i]; + if (!spot.show || spot.isNull()) { + continue; + } + final x = getPixelX(spot.x, viewSize, holder); + final y = getPixelY(spot.y, viewSize, holder); + if (spot.xError == null && spot.yError == null) { + continue; + } + + var left = 0.0; + var right = 0.0; + if (spot.xError != null) { + left = getPixelX(spot.x - spot.xError!.lowerBy, viewSize, holder) - x; + right = getPixelX(spot.x + spot.xError!.upperBy, viewSize, holder) - x; + } + + var top = 0.0; + var bottom = 0.0; + if (spot.yError != null) { + top = getPixelY(spot.y + spot.yError!.lowerBy, viewSize, holder) - y; + bottom = getPixelY(spot.y - spot.yError!.upperBy, viewSize, holder) - y; + } + final relativeErrorPixelsRect = Rect.fromLTRB( + left, + top, + right, + bottom, + ); + + final painter = errorIndicatorData.painter( + ScatterChartSpotErrorRangeCallbackInput( + spot: spot, + spotIndex: i, + ), + ); + canvasWrapper.drawErrorIndicator( + painter, + spot, + Offset(x, y), + relativeErrorPixelsRect, + holder.data, + ); + } + } + + @visibleForTesting + void drawTouchTooltips( + BuildContext context, + CanvasWrapper canvasWrapper, + PaintHolder holder, + ) { + final targetData = holder.targetData; + for (var i = 0; i < targetData.scatterSpots.length; i++) { + if (!targetData.showingTooltipIndicators.contains(i)) { + continue; + } + + final scatterSpot = targetData.scatterSpots[i]; + drawTouchTooltip( + context, + canvasWrapper, + targetData.scatterTouchData.touchTooltipData, + scatterSpot, + holder, + ); + } + } + + @visibleForTesting + void drawTouchTooltip( + BuildContext context, + CanvasWrapper canvasWrapper, + ScatterTouchTooltipData tooltipData, + ScatterSpot showOnSpot, + PaintHolder holder, + ) { + final viewSize = canvasWrapper.size; + + final tooltipItem = tooltipData.getTooltipItems(showOnSpot); + + if (tooltipItem == null) { + return; + } + + final span = TextSpan( + style: Utils().getThemeAwareTextStyle(context, tooltipItem.textStyle), + text: tooltipItem.text, + children: tooltipItem.children, + ); + + final drawingTextPainter = TextPainter( + text: span, + textAlign: tooltipItem.textAlign, + textDirection: tooltipItem.textDirection, + textScaler: holder.textScaler, + )..layout(maxWidth: tooltipData.maxContentWidth); + + final width = drawingTextPainter.width; + final height = drawingTextPainter.height; + + final tooltipOriginPoint = Offset( + getPixelX(showOnSpot.x, viewSize, holder), + getPixelY(showOnSpot.y, viewSize, holder), + ); + + // Get the dot size to create an extended boundary + final dotSize = showOnSpot.dotPainter.getSize(showOnSpot); + final dotRadius = dotSize.width / 2; + final viewRect = Offset.zero & viewSize; + final extendedBoundary = viewRect.inflate(dotRadius); + + // Check if any part of the dot is within the extended boundary + if (!extendedBoundary.contains(tooltipOriginPoint)) { + return; + } + + final tooltipWidth = width + tooltipData.tooltipPadding.horizontal; + final tooltipHeight = height + tooltipData.tooltipPadding.vertical; + + final tooltipLeftPosition = getTooltipLeft( + tooltipOriginPoint.dx, + tooltipWidth, + tooltipData.tooltipHorizontalAlignment, + tooltipData.tooltipHorizontalOffset, + ); + + /// draw the background rect with rounded radius + var rect = Rect.fromLTWH( + tooltipLeftPosition, + tooltipOriginPoint.dy - + tooltipHeight - + (showOnSpot.size.height / 2) - + tooltipItem.bottomMargin, + tooltipWidth, + tooltipHeight, + ); + + if (tooltipData.fitInsideHorizontally) { + if (rect.left < 0) { + final shiftAmount = 0 - rect.left; + rect = Rect.fromLTRB( + rect.left + shiftAmount, + rect.top, + rect.right + shiftAmount, + rect.bottom, + ); + } + + if (rect.right > viewSize.width) { + final shiftAmount = rect.right - viewSize.width; + rect = Rect.fromLTRB( + rect.left - shiftAmount, + rect.top, + rect.right - shiftAmount, + rect.bottom, + ); + } + } + + if (tooltipData.fitInsideVertically) { + if (rect.top < 0) { + final shiftAmount = 0 - rect.top; + rect = Rect.fromLTRB( + rect.left, + rect.top + shiftAmount, + rect.right, + rect.bottom + shiftAmount, + ); + } + + if (rect.bottom > viewSize.height) { + final shiftAmount = rect.bottom - viewSize.height; + rect = Rect.fromLTRB( + rect.left, + rect.top - shiftAmount, + rect.right, + rect.bottom - shiftAmount, + ); + } + } + + final roundedRect = RRect.fromRectAndCorners( + rect, + topLeft: tooltipData.tooltipBorderRadius.topLeft, + topRight: tooltipData.tooltipBorderRadius.topRight, + bottomLeft: tooltipData.tooltipBorderRadius.bottomLeft, + bottomRight: tooltipData.tooltipBorderRadius.bottomRight, + ); + + _bgTouchTooltipPaint.color = tooltipData.getTooltipColor(showOnSpot); + + final rotateAngle = tooltipData.rotateAngle; + final rectRotationOffset = + Offset(0, Utils().calculateRotationOffset(rect.size, rotateAngle).dy); + final rectDrawOffset = Offset(roundedRect.left, roundedRect.top); + + final textRotationOffset = + Utils().calculateRotationOffset(drawingTextPainter.size, rotateAngle); + + final drawOffset = Offset( + rect.center.dx - (drawingTextPainter.width / 2), + rect.topCenter.dy + + tooltipData.tooltipPadding.top - + textRotationOffset.dy + + rectRotationOffset.dy, + ); + + if (tooltipData.tooltipBorder != BorderSide.none) { + _borderTouchTooltipPaint + ..color = tooltipData.tooltipBorder.color + ..strokeWidth = tooltipData.tooltipBorder.width; + } + + final reverseQuarterTurnsAngle = -holder.data.rotationQuarterTurns * 90; + canvasWrapper.drawRotated( + size: rect.size, + rotationOffset: rectRotationOffset, + drawOffset: rectDrawOffset, + angle: reverseQuarterTurnsAngle + rotateAngle, + drawCallback: () { + canvasWrapper + ..drawRRect(roundedRect, _bgTouchTooltipPaint) + ..drawRRect(roundedRect, _borderTouchTooltipPaint) + ..drawText(drawingTextPainter, drawOffset); + }, + ); + } + + /// Makes a [ScatterTouchedSpot] based on the provided [localPosition] + /// + /// Processes [localPosition] and checks + /// the elements of the chart that are near the offset, + /// then makes a [ScatterTouchedSpot] from the elements that has been touched. + /// + /// Returns null if finds nothing! + ScatterTouchedSpot? handleTouch( + Offset localPosition, + Size viewSize, + PaintHolder holder, + ) { + final data = holder.data; + + for (var i = data.scatterSpots.length - 1; i >= 0; i--) { + // Reverse the loop to check the topmost spot first + final spot = data.scatterSpots[i]; + + final spotPixelX = getPixelX(spot.x, viewSize, holder); + final spotPixelY = getPixelY(spot.y, viewSize, holder); + final center = Offset(spotPixelX, spotPixelY); + + final touched = spot.dotPainter.hitTest( + spot, + localPosition, + center, + data.scatterTouchData.touchSpotThreshold, + ); + if (touched) { + return ScatterTouchedSpot(spot, i); + } + } + return null; + } +} diff --git a/lib/src/chart/scatter_chart/scatter_chart_renderer.dart b/lib/src/chart/scatter_chart/scatter_chart_renderer.dart new file mode 100644 index 0000000..3380610 --- /dev/null +++ b/lib/src/chart/scatter_chart/scatter_chart_renderer.dart @@ -0,0 +1,144 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart'; +import 'package:fl_chart/src/chart/base/base_chart/render_base_chart.dart'; +import 'package:fl_chart/src/chart/scatter_chart/scatter_chart_painter.dart'; +import 'package:fl_chart/src/utils/canvas_wrapper.dart'; +import 'package:flutter/cupertino.dart'; + +// coverage:ignore-start + +/// Low level ScatterChart Widget. +class ScatterChartLeaf extends LeafRenderObjectWidget { + const ScatterChartLeaf({ + super.key, + required this.data, + required this.targetData, + required this.chartVirtualRect, + required this.canBeScaled, + }); + + final ScatterChartData data; + final ScatterChartData targetData; + final Rect? chartVirtualRect; + final bool canBeScaled; + + @override + RenderScatterChart createRenderObject(BuildContext context) => + RenderScatterChart( + context, + data, + targetData, + MediaQuery.of(context).textScaler, + chartVirtualRect, + canBeScaled: canBeScaled, + ); + + @override + void updateRenderObject( + BuildContext context, + RenderScatterChart renderObject, + ) { + renderObject + ..data = data + ..targetData = targetData + ..textScaler = MediaQuery.of(context).textScaler + ..buildContext = context + ..chartVirtualRect = chartVirtualRect + ..canBeScaled = canBeScaled; + } +} +// coverage:ignore-end + +/// Renders our ScatterChart, also handles hitTest. +class RenderScatterChart extends RenderBaseChart { + RenderScatterChart( + BuildContext context, + ScatterChartData data, + ScatterChartData targetData, + TextScaler textScaler, + Rect? chartVirtualRect, { + required bool canBeScaled, + }) : _data = data, + _targetData = targetData, + _textScaler = textScaler, + _chartVirtualRect = chartVirtualRect, + super(targetData.scatterTouchData, context, canBeScaled: canBeScaled); + + ScatterChartData get data => _data; + ScatterChartData _data; + + set data(ScatterChartData value) { + if (_data == value) return; + _data = value; + markNeedsPaint(); + } + + ScatterChartData get targetData => _targetData; + ScatterChartData _targetData; + + set targetData(ScatterChartData value) { + if (_targetData == value) return; + _targetData = value; + super.updateBaseTouchData(_targetData.scatterTouchData); + markNeedsPaint(); + } + + TextScaler get textScaler => _textScaler; + TextScaler _textScaler; + + set textScaler(TextScaler value) { + if (_textScaler == value) return; + _textScaler = value; + markNeedsPaint(); + } + + Rect? get chartVirtualRect => _chartVirtualRect; + Rect? _chartVirtualRect; + + set chartVirtualRect(Rect? value) { + if (_chartVirtualRect == value) return; + _chartVirtualRect = value; + markNeedsPaint(); + } + + // We couldn't mock [size] property of this class, that's why we have this + @visibleForTesting + Size? mockTestSize; + + @visibleForTesting + ScatterChartPainter painter = ScatterChartPainter(); + + PaintHolder get paintHolder => + PaintHolder(data, targetData, textScaler, chartVirtualRect); + + @override + void paint(PaintingContext context, Offset offset) { + final canvas = context.canvas + ..save() + ..translate(offset.dx, offset.dy); + painter.paint( + buildContext, + CanvasWrapper(canvas, mockTestSize ?? size), + paintHolder, + ); + canvas.restore(); + } + + @override + ScatterTouchResponse getResponseAtLocation(Offset localPosition) { + final chartSize = mockTestSize ?? size; + return ScatterTouchResponse( + touchLocation: localPosition, + touchChartCoordinate: painter.getChartCoordinateFromPixel( + localPosition, + chartSize, + paintHolder, + ), + touchedSpot: painter.handleTouch( + localPosition, + chartSize, + paintHolder, + ), + ); + } +} diff --git a/lib/src/extensions/bar_chart_data_extension.dart b/lib/src/extensions/bar_chart_data_extension.dart new file mode 100644 index 0000000..e381c6d --- /dev/null +++ b/lib/src/extensions/bar_chart_data_extension.dart @@ -0,0 +1,100 @@ +import 'package:fl_chart/src/chart/bar_chart/bar_chart_data.dart'; + +extension BarChartDataExtension on BarChartData { + List calculateGroupsX(double viewWidth) { + assert(barGroups.isNotEmpty); + final groupsX = List.filled(barGroups.length, 0); + + var sumWidth = + barGroups.map((group) => group.width).reduce((a, b) => a + b); + final spaceAvailable = viewWidth - sumWidth; + + void spaceEvenly() { + final eachSpace = spaceAvailable / (barGroups.length + 1); + var tempX = 0.0; + barGroups.asMap().forEach((i, group) { + tempX += eachSpace; + tempX += group.width / 2; + groupsX[i] = tempX; + tempX += group.width / 2; + }); + } + + switch (alignment) { + case BarChartAlignment.start: + var tempX = 0.0; + for (var i = 0; i < barGroups.length; i++) { + final group = barGroups[i]; + groupsX[i] = tempX + group.width / 2; + + final groupSpace = i == barGroups.length - 1 ? 0 : groupsSpace; + tempX += group.width + groupSpace; + } + if (tempX > viewWidth) { + spaceEvenly(); + } + + case BarChartAlignment.end: + sumWidth += groupsSpace * (barGroups.length - 1); + final horizontalMargin = viewWidth - sumWidth; + + var tempX = 0.0; + for (var i = 0; i < barGroups.length; i++) { + final group = barGroups[i]; + groupsX[i] = horizontalMargin + tempX + group.width / 2; + + final groupSpace = i == barGroups.length - 1 ? 0 : groupsSpace; + tempX += group.width + groupSpace; + } + if (tempX > viewWidth) { + spaceEvenly(); + } + + case BarChartAlignment.center: + sumWidth += groupsSpace * (barGroups.length - 1); + final horizontalMargin = (viewWidth - sumWidth) / 2; + + var tempX = 0.0; + for (var i = 0; i < barGroups.length; i++) { + final group = barGroups[i]; + groupsX[i] = horizontalMargin + tempX + group.width / 2; + + final groupSpace = i == barGroups.length - 1 ? 0 : groupsSpace; + tempX += group.width + groupSpace; + } + if (tempX > viewWidth) { + spaceEvenly(); + } + + case BarChartAlignment.spaceBetween: + final eachSpace = spaceAvailable / (barGroups.length - 1); + + var tempX = 0.0; + barGroups.asMap().forEach((index, group) { + tempX += group.width / 2; + if (index != 0) { + tempX += eachSpace; + } + groupsX[index] = tempX; + tempX += group.width / 2; + }); + + case BarChartAlignment.spaceAround: + final eachSpace = spaceAvailable / (barGroups.length * 2); + + var tempX = 0.0; + barGroups.asMap().forEach((i, group) { + tempX += eachSpace; + tempX += group.width / 2; + groupsX[i] = tempX; + tempX += group.width / 2; + tempX += eachSpace; + }); + + case BarChartAlignment.spaceEvenly: + spaceEvenly(); + } + + return groupsX; + } +} diff --git a/lib/src/extensions/border_extension.dart b/lib/src/extensions/border_extension.dart new file mode 100644 index 0000000..7078000 --- /dev/null +++ b/lib/src/extensions/border_extension.dart @@ -0,0 +1,20 @@ +import 'package:flutter/widgets.dart'; + +extension BorderExtension on Border { + bool isVisible() { + if (left.width == 0 && + top.width == 0 && + right.width == 0 && + bottom.width == 0) { + return false; + } + + if (left.color.a == 0.0 && + top.color.a == 0.0 && + right.color.a == 0.0 && + bottom.color.a == 0.0) { + return false; + } + return true; + } +} diff --git a/lib/src/extensions/color_extension.dart b/lib/src/extensions/color_extension.dart new file mode 100644 index 0000000..3d4d813 --- /dev/null +++ b/lib/src/extensions/color_extension.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +extension ColorExtension on Color { + /// Convert the color to a darken color based on the [percent] + Color darken([int percent = 40]) { + assert(1 <= percent && percent <= 100); + final value = 1 - percent / 100; + return Color.fromARGB( + _floatToInt8(a), + (_floatToInt8(r) * value).round(), + (_floatToInt8(g) * value).round(), + (_floatToInt8(b) * value).round(), + ); + } + + // Int color components were deprecated in Flutter 3.27.0. + // This method is used to convert the new double color components to the + // old int color components. + // + // Taken from the Color class. + int _floatToInt8(double x) { + return (x * 255.0).round() & 0xff; + } +} diff --git a/lib/src/extensions/edge_insets_extension.dart b/lib/src/extensions/edge_insets_extension.dart new file mode 100644 index 0000000..fd565be --- /dev/null +++ b/lib/src/extensions/edge_insets_extension.dart @@ -0,0 +1,13 @@ +import 'package:flutter/widgets.dart'; + +extension EdgeInsetsExtension on EdgeInsets { + EdgeInsets get onlyTopBottom => EdgeInsets.only( + top: top, + bottom: bottom, + ); + + EdgeInsets get onlyLeftRight => EdgeInsets.only( + left: left, + right: right, + ); +} diff --git a/lib/src/extensions/fl_border_data_extension.dart b/lib/src/extensions/fl_border_data_extension.dart new file mode 100644 index 0000000..64a6469 --- /dev/null +++ b/lib/src/extensions/fl_border_data_extension.dart @@ -0,0 +1,11 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/widgets.dart'; + +extension FlBorderDataExtension on FlBorderData { + EdgeInsets get allSidesPadding => EdgeInsets.only( + left: show ? border.left.width : 0.0, + top: show ? border.top.width : 0.0, + right: show ? border.right.width : 0.0, + bottom: show ? border.bottom.width : 0.0, + ); +} diff --git a/lib/src/extensions/fl_titles_data_extension.dart b/lib/src/extensions/fl_titles_data_extension.dart new file mode 100644 index 0000000..28565d7 --- /dev/null +++ b/lib/src/extensions/fl_titles_data_extension.dart @@ -0,0 +1,12 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/extensions/side_titles_extension.dart'; +import 'package:flutter/widgets.dart'; + +extension FlTitlesDataExtension on FlTitlesData { + EdgeInsets get allSidesPadding => EdgeInsets.only( + left: show ? leftTitles.totalReservedSize : 0.0, + top: show ? topTitles.totalReservedSize : 0.0, + right: show ? rightTitles.totalReservedSize : 0.0, + bottom: show ? bottomTitles.totalReservedSize : 0.0, + ); +} diff --git a/lib/src/extensions/gradient_extension.dart b/lib/src/extensions/gradient_extension.dart new file mode 100644 index 0000000..3ddf151 --- /dev/null +++ b/lib/src/extensions/gradient_extension.dart @@ -0,0 +1,25 @@ +import 'package:flutter/painting.dart'; + +/// Extensions on [Gradient] +extension GradientExtension on Gradient { + /// Returns color stops. + /// + /// If [stops] has the same length as [colors], returns it directly. + /// Otherwise, calculates stops linearly between 0.0 and 1.0. + /// + /// Throws [ArgumentError] if [colors] has less than 2 colors. + List getSafeColorStops() { + if (stops?.length == colors.length) { + return stops!; + } + + if (colors.length <= 1) { + throw ArgumentError('"colors" must have length > 1.'); + } + + final stopsStep = 1.0 / (colors.length - 1); + return [ + for (var index = 0; index < colors.length; index++) index * stopsStep, + ]; + } +} diff --git a/lib/src/extensions/paint_extension.dart b/lib/src/extensions/paint_extension.dart new file mode 100644 index 0000000..228ed3a --- /dev/null +++ b/lib/src/extensions/paint_extension.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +extension PaintExtension on Paint { + /// Hides the paint's color, if strokeWidth is zero + void transparentIfWidthIsZero() { + if (strokeWidth == 0) { + shader = null; + color = color.withValues(alpha: 0); + } + } + + void setColorOrGradient(Color? color, Gradient? gradient, Rect rect) { + if (gradient != null) { + this.color = Colors.black; + shader = gradient.createShader(rect); + } else { + this.color = color ?? Colors.transparent; + shader = null; + } + } + + void setColorOrGradientForLine( + Color? color, + Gradient? gradient, { + required Offset from, + required Offset to, + }) { + final rect = Rect.fromPoints(from, to); + setColorOrGradient(color, gradient, rect); + } +} diff --git a/lib/src/extensions/path_extension.dart b/lib/src/extensions/path_extension.dart new file mode 100644 index 0000000..9a47e1c --- /dev/null +++ b/lib/src/extensions/path_extension.dart @@ -0,0 +1,23 @@ +import 'dart:ui'; + +import 'package:fl_chart/src/utils/path_drawing/dash_path.dart'; + +/// Defines extensions on the [Path] +extension DashedPath on Path { + /// Returns a dashed path based on [dashArray]. + /// + /// it is a circular array of dash offsets and lengths. + /// For example, the array `[5, 10]` would result in dashes 5 pixels long + /// followed by blank spaces 10 pixels long. + Path toDashedPath(List? dashArray) { + if (dashArray != null) { + final castedArray = dashArray.map((value) => value.toDouble()).toList(); + final dashedPath = + dashPath(this, dashArray: CircularIntervalList(castedArray)); + + return dashedPath; + } else { + return this; + } + } +} diff --git a/lib/src/extensions/rrect_extension.dart b/lib/src/extensions/rrect_extension.dart new file mode 100644 index 0000000..066cafd --- /dev/null +++ b/lib/src/extensions/rrect_extension.dart @@ -0,0 +1,12 @@ +import 'package:flutter/cupertino.dart'; + +/// Defines extensions on the [RRect] +extension RRectExtension on RRect { + /// Return [Rect] from [RRect] + Rect getRect() => Rect.fromLTRB( + left, + top, + right, + bottom, + ); +} diff --git a/lib/src/extensions/side_titles_extension.dart b/lib/src/extensions/side_titles_extension.dart new file mode 100644 index 0000000..d500f3e --- /dev/null +++ b/lib/src/extensions/side_titles_extension.dart @@ -0,0 +1,14 @@ +import 'package:fl_chart/src/chart/base/axis_chart/axis_chart_data.dart'; + +extension SideTitlesExtension on AxisTitles { + double get totalReservedSize { + var size = 0.0; + if (showAxisTitles) { + size += axisNameSize; + } + if (showSideTitles) { + size += sideTitles.reservedSize; + } + return size; + } +} diff --git a/lib/src/extensions/size_extension.dart b/lib/src/extensions/size_extension.dart new file mode 100644 index 0000000..f1ec276 --- /dev/null +++ b/lib/src/extensions/size_extension.dart @@ -0,0 +1,13 @@ +import 'dart:ui'; + +extension SizeExtension on Size { + Size rotateByQuarterTurns(int quarterTurns) { + if (quarterTurns < 0) { + throw ArgumentError('quarterTurns must be greater than or equal to 0.'); + } + return switch (quarterTurns % 4) { + 0 || 2 => this, + _ /*2 || 3*/ => Size(height, width), + }; + } +} diff --git a/lib/src/extensions/text_align_extension.dart b/lib/src/extensions/text_align_extension.dart new file mode 100644 index 0000000..f231aea --- /dev/null +++ b/lib/src/extensions/text_align_extension.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +enum HorizontalAlignment { left, center, right } + +extension TextAlignExtension on TextAlign { + HorizontalAlignment getFinalHorizontalAlignment(TextDirection? direction) { + if ((this == TextAlign.left) || + (this == TextAlign.start && direction == TextDirection.ltr) || + (this == TextAlign.end && direction == TextDirection.rtl)) { + return HorizontalAlignment.left; + } else if ((this == TextAlign.right) || + (this == TextAlign.end && direction == TextDirection.ltr) || + (this == TextAlign.start && direction == TextDirection.rtl)) { + return HorizontalAlignment.right; + } else { + return HorizontalAlignment.center; + } + } +} diff --git a/lib/src/utils/canvas_wrapper.dart b/lib/src/utils/canvas_wrapper.dart new file mode 100644 index 0000000..fb5ce71 --- /dev/null +++ b/lib/src/utils/canvas_wrapper.dart @@ -0,0 +1,168 @@ +import 'dart:ui'; + +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/extensions/path_extension.dart'; +import 'package:fl_chart/src/utils/utils.dart'; +import 'package:flutter/cupertino.dart' hide Image; + +typedef DrawCallback = void Function(); + +/// Proxies Canvas functions +/// +/// We wrapped the canvas here, because we needed to write tests for our drawing system. +/// Now in tests we can verify that these functions called with a specific value. +class CanvasWrapper { + CanvasWrapper( + this.canvas, + this.size, + ); + + final Canvas canvas; + final Size size; + + /// Directly calls [Canvas.drawRRect] + void drawRRect(RRect rrect, Paint paint) => canvas.drawRRect(rrect, paint); + + /// Directly calls [Canvas.save] + void save() => canvas.save(); + + /// Directly calls [Canvas.restore] + void restore() => canvas.restore(); + + /// Directly calls [Canvas.clipRect] + void clipRect( + Rect rect, { + ClipOp clipOp = ClipOp.intersect, + bool doAntiAlias = true, + }) => + canvas.clipRect(rect, clipOp: clipOp, doAntiAlias: doAntiAlias); + + /// Directly calls [Canvas.translate] + void translate(double dx, double dy) => canvas.translate(dx, dy); + + /// Directly calls [Canvas.rotate] + void rotate(double radius) => canvas.rotate(radius); + + /// Directly calls [Canvas.drawPath] + void drawPath(Path path, Paint paint) => canvas.drawPath(path, paint); + + /// Directly calls [Canvas.saveLayer] + void saveLayer(Rect bounds, Paint paint) => canvas.saveLayer(bounds, paint); + + /// Directly calls [Canvas.drawPicture] + void drawPicture(Picture picture) => canvas.drawPicture(picture); + + /// Directly calls [Canvas.drawImage] + void drawImage(Image image, Offset offset, Paint paint) => + canvas.drawImage(image, offset, paint); + + /// Directly calls [Canvas.clipPath] + void clipPath(Path path, {bool doAntiAlias = true}) => + canvas.clipPath(path, doAntiAlias: doAntiAlias); + + /// Directly calls [Canvas.drawRect] + void drawRect(Rect rect, Paint paint) => canvas.drawRect(rect, paint); + + /// Directly calls [Canvas.drawLine] + void drawLine(Offset p1, Offset p2, Paint paint) => + canvas.drawLine(p1, p2, paint); + + /// Directly calls [Canvas.drawCircle] + void drawCircle(Offset center, double radius, Paint paint) => + canvas.drawCircle(center, radius, paint); + + /// Directly calls [Canvas.drawCircle] + void drawArc( + Rect rect, + double startAngle, + double sweepAngle, + bool useCenter, + Paint paint, + ) => + canvas.drawArc(rect, startAngle, sweepAngle, useCenter, paint); + + /// Paints a text on the [Canvas] + /// + /// Gets a [TextPainter] and call its [TextPainter.paint] using our canvas + void drawText(TextPainter tp, Offset offset, [double? rotateAngle]) { + if (rotateAngle == null) { + tp.paint(canvas, offset); + } else { + drawRotated( + size: tp.size, + drawOffset: offset, + angle: rotateAngle, + drawCallback: () { + tp.paint(canvas, offset); + }, + ); + } + } + + /// Paints a vertical text on the [Canvas] + /// + /// Gets a [TextPainter] and call its [TextPainter.paint] using our canvas + void drawVerticalText(TextPainter tp, Offset offset) { + save(); + translate(offset.dx, offset.dy); + rotate(Utils().radians(90)); + translate(-offset.dx, -offset.dy); + tp.paint(canvas, offset); + restore(); + } + + /// Paints a dot using customized [FlDotPainter] + /// + /// Paints a customized dot using [FlDotPainter] at the [spot]'s position, + /// with the [offset] + void drawDot(FlDotPainter painter, FlSpot spot, Offset offset) { + painter.draw(canvas, spot, offset); + } + + /// Paints a error indicator using the [painter] + void drawErrorIndicator( + FlSpotErrorRangePainter painter, + FlSpot origin, + Offset offset, + Rect errorRelativeRect, + AxisChartData axisData, + ) { + painter.draw(canvas, offset, origin, errorRelativeRect, axisData); + } + + /// Handles performing multiple draw actions rotated. + void drawRotated({ + required Size size, + Offset rotationOffset = Offset.zero, + Offset drawOffset = Offset.zero, + required double angle, + required DrawCallback drawCallback, + }) { + save(); + translate( + rotationOffset.dx + drawOffset.dx + size.width / 2, + rotationOffset.dy + drawOffset.dy + size.height / 2, + ); + rotate(Utils().radians(angle)); + translate( + -drawOffset.dx - size.width / 2, + -drawOffset.dy - size.height / 2, + ); + drawCallback(); + restore(); + } + + /// Draws a dashed line from passed in offsets + void drawDashedLine( + Offset from, + Offset to, + Paint painter, + List? dashArray, + ) { + var path = Path() + ..moveTo(from.dx, from.dy) + ..lineTo(to.dx, to.dy); + path = path.toDashedPath(dashArray); + drawPath(path, painter); + } +} diff --git a/lib/src/utils/lerp.dart b/lib/src/utils/lerp.dart new file mode 100644 index 0000000..f6da790 --- /dev/null +++ b/lib/src/utils/lerp.dart @@ -0,0 +1,201 @@ +import 'dart:ui'; + +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; + +@visibleForTesting +List? lerpList( + List? a, + List? b, + double t, { + required T Function(T, T, double) lerp, +}) { + if (a != null && b != null && a.length == b.length) { + return List.generate(a.length, (i) { + return lerp(a[i], b[i], t); + }); + } else if (a != null && b != null) { + return List.generate(b.length, (i) { + return lerp(i >= a.length ? b[i] : a[i], b[i], t); + }); + } else { + return b; + } +} + +/// Lerps [Color] list based on [t] value, check [Tween.lerp]. +List? lerpColorList(List? a, List? b, double t) => + lerpList(a, b, t, lerp: lerpColor); + +/// Lerps [Color] based on [t] value, check [Color.lerp]. +Color lerpColor(Color a, Color b, double t) => Color.lerp(a, b, t)!; + +/// Lerps [double] list based on [t] value, allows [double.infinity]. +double? lerpDoubleAllowInfinity(double? a, double? b, double t) { + if (a == b || (a?.isNaN == true) && (b?.isNaN == true)) { + return a; + } + + if (a!.isInfinite || b!.isInfinite) { + return b; + } + assert(a.isFinite, 'Cannot interpolate between finite and non-finite values'); + assert(b.isFinite, 'Cannot interpolate between finite and non-finite values'); + assert(t.isFinite, 't must be finite when interpolating between values'); + return a * (1.0 - t) + b * t; +} + +/// Lerps [double] list based on [t] value, check [Tween.lerp]. +List? lerpDoubleList(List? a, List? b, double t) => + lerpList(a, b, t, lerp: lerpNonNullDouble); + +/// Lerps [int] list based on [t] value, check [Tween.lerp]. +List? lerpIntList(List? a, List? b, double t) => + lerpList(a, b, t, lerp: lerpInt); + +/// Lerps [int] list based on [t] value, check [Tween.lerp]. +int lerpInt(int a, int b, double t) => (a + (b - a) * t).round(); + +@visibleForTesting +double lerpNonNullDouble(double a, double b, double t) => lerpDouble(a, b, t)!; + +/// Lerps [FlSpot] list based on [t] value, check [Tween.lerp]. +List? lerpFlSpotList(List? a, List? b, double t) => + lerpList(a, b, t, lerp: FlSpot.lerp); + +/// Lerps [HorizontalLine] list based on [t] value, check [Tween.lerp]. +List? lerpHorizontalLineList( + List? a, + List? b, + double t, +) => + lerpList(a, b, t, lerp: HorizontalLine.lerp); + +/// Lerps [VerticalLine] list based on [t] value, check [Tween.lerp]. +List? lerpVerticalLineList( + List? a, + List? b, + double t, +) => + lerpList(a, b, t, lerp: VerticalLine.lerp); + +/// Lerps [HorizontalRangeAnnotation] list based on [t] value, check [Tween.lerp]. +List? lerpHorizontalRangeAnnotationList( + List? a, + List? b, + double t, +) => + lerpList(a, b, t, lerp: HorizontalRangeAnnotation.lerp); + +/// Lerps [VerticalRangeAnnotation] list based on [t] value, check [Tween.lerp]. +List? lerpVerticalRangeAnnotationList( + List? a, + List? b, + double t, +) => + lerpList(a, b, t, lerp: VerticalRangeAnnotation.lerp); + +/// Lerps [LineChartBarData] list based on [t] value, check [Tween.lerp]. +List? lerpLineChartBarDataList( + List? a, + List? b, + double t, +) => + lerpList(a, b, t, lerp: LineChartBarData.lerp); + +/// Lerps [BetweenBarsData] list based on [t] value, check [Tween.lerp]. +List? lerpBetweenBarsDataList( + List? a, + List? b, + double t, +) => + lerpList(a, b, t, lerp: BetweenBarsData.lerp); + +/// Lerps [BarChartGroupData] list based on [t] value, check [Tween.lerp]. +List? lerpBarChartGroupDataList( + List? a, + List? b, + double t, +) => + lerpList(a, b, t, lerp: BarChartGroupData.lerp); + +/// Lerps [BarChartRodData] list based on [t] value, check [Tween.lerp]. +List? lerpBarChartRodDataList( + List? a, + List? b, + double t, +) => + lerpList(a, b, t, lerp: BarChartRodData.lerp); + +/// Lerps [PieChartSectionData] list based on [t] value, check [Tween.lerp]. +List? lerpPieChartSectionDataList( + List? a, + List? b, + double t, +) => + lerpList(a, b, t, lerp: PieChartSectionData.lerp); + +/// Lerps [ScatterSpot] list based on [t] value, check [Tween.lerp]. +List? lerpScatterSpotList( + List? a, + List? b, + double t, +) => + lerpList(a, b, t, lerp: ScatterSpot.lerp); + +/// Lerps [CandlestickSpot] list based on [t] value, check [Tween.lerp]. +List? lerpCandleSpotList( + List? a, + List? b, + double t, +) => + lerpList(a, b, t, lerp: CandlestickSpot.lerp); + +/// Lerps [BarChartRodStackItem] list based on [t] value, check [Tween.lerp]. +List? lerpBarChartRodStackList( + List? a, + List? b, + double t, +) => + lerpList(a, b, t, lerp: BarChartRodStackItem.lerp); + +/// Lerps [RadarDataSet] list based on [t] value, check [Tween.lerp]. +List? lerpRadarDataSetList( + List? a, + List? b, + double t, +) => + lerpList(a, b, t, lerp: RadarDataSet.lerp); + +/// Lerps [RadarEntry] list based on [t] value, check [Tween.lerp]. +List? lerpRadarEntryList( + List? a, + List? b, + double t, +) => + lerpList(a, b, t, lerp: RadarEntry.lerp); + +/// Lerps between a [LinearGradient] colors, based on [t] +Color lerpGradient(List colors, List stops, double t) { + final length = colors.length; + if (stops.length != length) { + /// provided gradientColorStops is invalid and we calculate it here + stops = List.generate(length, (i) => (i + 1) / length); + } + + for (var s = 0; s < stops.length - 1; s++) { + final leftStop = stops[s]; + final rightStop = stops[s + 1]; + + final leftColor = colors[s]; + final rightColor = colors[s + 1]; + + if (t <= leftStop) { + return leftColor; + } else if (t < rightStop) { + final sectionT = (t - leftStop) / (rightStop - leftStop); + return Color.lerp(leftColor, rightColor, sectionT)!; + } + } + return colors.last; +} diff --git a/lib/src/utils/path_drawing/dash_path.dart b/lib/src/utils/path_drawing/dash_path.dart new file mode 100644 index 0000000..d7061af --- /dev/null +++ b/lib/src/utils/path_drawing/dash_path.dart @@ -0,0 +1,86 @@ +import 'dart:ui'; + +/// Came from [flutter_path_drawing](https://github.com/dnfield/flutter_path_drawing) library. +/// Creates a new path that is drawn from the segments of `source`. +/// +/// Dash intervals are controlled by the `dashArray` - see [CircularIntervalList] +/// for examples. +/// +/// `dashOffset` specifies an initial starting point for the dashing. +/// +/// Passing a `source` that is an empty path will return an empty path. +Path dashPath( + Path source, { + required CircularIntervalList dashArray, + DashOffset? dashOffset, +}) { + dashOffset = dashOffset ?? const DashOffset.absolute(0); + // TODO(imaNNeo): Is there some way to determine how much of a path would be visible today? + + final dest = Path(); + for (final metric in source.computeMetrics()) { + var distance = dashOffset._calculate(metric.length); + var draw = true; + while (distance < metric.length) { + final len = dashArray.next; + if (draw) { + dest.addPath(metric.extractPath(distance, distance + len), Offset.zero); + } + distance += len; + draw = !draw; + } + } + + return dest; +} + +enum _DashOffsetType { absolute, percentage } + +/// Specifies the starting position of a dash array on a path, either as a +/// percentage or absolute value. +/// +/// The internal value will be guaranteed to not be null. +class DashOffset { + /// Create a DashOffset that will be measured as a percentage of the length + /// of the segment being dashed. + /// + /// `percentage` will be clamped between 0.0 and 1.0. + DashOffset.percentage(double percentage) + : _rawVal = percentage.clamp(0.0, 1.0), + _dashOffsetType = _DashOffsetType.percentage; + + /// Create a DashOffset that will be measured in terms of absolute pixels + /// along the length of a [Path] segment. + const DashOffset.absolute(double start) + : _rawVal = start, + _dashOffsetType = _DashOffsetType.absolute; + + final double _rawVal; + final _DashOffsetType _dashOffsetType; + + double _calculate(double length) => + _dashOffsetType == _DashOffsetType.absolute ? _rawVal : length * _rawVal; +} + +/// A circular array of dash offsets and lengths. +/// +/// For example, the array `[5, 10]` would result in dashes 5 pixels long +/// followed by blank spaces 10 pixels long. The array `[5, 10, 5]` would +/// result in a 5 pixel dash, a 10 pixel gap, a 5 pixel dash, a 5 pixel gap, +/// a 10 pixel dash, etc. +/// +/// Note that this does not quite conform to an [Iterable], because it does +/// not have a moveNext. +class CircularIntervalList { + CircularIntervalList(this._values); + + final List _values; + int _idx = 0; + + T get next { + if (_idx >= _values.length) { + _idx = 0; + } + return _values[_idx++]; + } +} diff --git a/lib/src/utils/utils.dart b/lib/src/utils/utils.dart new file mode 100644 index 0000000..2417904 --- /dev/null +++ b/lib/src/utils/utils.dart @@ -0,0 +1,314 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; + +class Utils { + factory Utils() { + return _singleton; + } + + Utils._internal(); + static Utils _singleton = Utils._internal(); + + @visibleForTesting + static void changeInstance(Utils val) => _singleton = val; + + static const double _degrees2Radians = math.pi / 180.0; + + /// Converts degrees to radians + double radians(double degrees) => degrees * _degrees2Radians; + + static const double _radians2Degrees = 180.0 / math.pi; + + /// Converts radians to degrees + double degrees(double radians) => radians * _radians2Degrees; + + /// Forward the view base on its degree + double translateRotatedPosition(double size, double degree) { + return (size / 4) * math.sin(radians(degree.abs())); + } + + Offset calculateRotationOffset(Size size, double degree) { + final rotatedHeight = (size.width * math.sin(radians(degree))).abs() + + (size.height * math.cos(radians(degree))).abs(); + final rotatedWidth = (size.width * math.cos(radians(degree))).abs() + + (size.height * math.sin(radians(degree))).abs(); + return Offset( + (size.width - rotatedWidth) / 2, + (size.height - rotatedHeight) / 2, + ); + } + + /// Decreases [borderRadius] to <= width / 2 + BorderRadius? normalizeBorderRadius( + BorderRadius? borderRadius, + double width, + ) { + if (borderRadius == null) { + return null; + } + + Radius topLeft; + if (borderRadius.topLeft.x > width / 2 || + borderRadius.topLeft.y > width / 2) { + topLeft = Radius.circular(width / 2); + } else { + topLeft = borderRadius.topLeft; + } + + Radius topRight; + if (borderRadius.topRight.x > width / 2 || + borderRadius.topRight.y > width / 2) { + topRight = Radius.circular(width / 2); + } else { + topRight = borderRadius.topRight; + } + + Radius bottomLeft; + if (borderRadius.bottomLeft.x > width / 2 || + borderRadius.bottomLeft.y > width / 2) { + bottomLeft = Radius.circular(width / 2); + } else { + bottomLeft = borderRadius.bottomLeft; + } + + Radius bottomRight; + if (borderRadius.bottomRight.x > width / 2 || + borderRadius.bottomRight.y > width / 2) { + bottomRight = Radius.circular(width / 2); + } else { + bottomRight = borderRadius.bottomRight; + } + + return BorderRadius.only( + topLeft: topLeft, + topRight: topRight, + bottomLeft: bottomLeft, + bottomRight: bottomRight, + ); + } + + /// Default value for BorderSide where borderSide value is not exists + static const BorderSide defaultBorderSide = BorderSide(width: 0); + + /// Decreases [borderSide] to <= width / 2 + BorderSide normalizeBorderSide(BorderSide? borderSide, double width) { + if (borderSide == null) { + return defaultBorderSide; + } + + double borderWidth; + if (borderSide.width > width / 2) { + borderWidth = width / 2.toDouble(); + } else { + borderWidth = borderSide.width; + } + + return borderSide.copyWith(width: borderWidth); + } + + /// Returns an efficient interval for showing axis titles, or grid lines or ... + /// + /// If there isn't any provided interval, we use this function to calculate an interval to apply, + /// using [axisViewSize] / [pixelPerInterval], we calculate the allowedCount lines in the axis, + /// then using [diffInAxis] / allowedCount, we can find out how much interval we need, + /// then we round that number by finding nearest number in this pattern: + /// 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 5000, 10000,... + double getEfficientInterval( + double axisViewSize, + double diffInAxis, { + double pixelPerInterval = 40, + }) { + final allowedCount = math.max(axisViewSize ~/ pixelPerInterval, 1); + if (diffInAxis == 0) { + return 1; + } + final accurateInterval = + diffInAxis == 0 ? axisViewSize : diffInAxis / allowedCount; + if (allowedCount <= 2) { + return accurateInterval; + } + return roundInterval(accurateInterval); + } + + @visibleForTesting + double roundInterval(double input) { + if (input < 1) { + return _roundIntervalBelowOne(input); + } + return _roundIntervalAboveOne(input); + } + + double _roundIntervalBelowOne(double input) { + assert(input < 1.0); + + if (input < 0.000001) { + return input; + } + + final inputString = input.toString(); + var precisionCount = inputString.length - 2; + + var zeroCount = 0; + for (var i = 2; i <= inputString.length; i++) { + if (inputString[i] != '0') { + break; + } + zeroCount++; + } + + final afterZerosNumberLength = precisionCount - zeroCount; + if (afterZerosNumberLength > 2) { + final numbersToRemove = afterZerosNumberLength - 2; + precisionCount -= numbersToRemove; + } + + final pow10onPrecision = math.pow(10, precisionCount); + input *= pow10onPrecision; + return _roundIntervalAboveOne(input) / pow10onPrecision; + } + + double _roundIntervalAboveOne(double input) { + assert(input >= 1.0); + final decimalCount = input.toInt().toString().length - 1; + input /= math.pow(10, decimalCount); + + final scaled = input >= 10 ? input.round() / 10 : input; + + if (scaled >= 7.6) { + return 10 * math.pow(10, decimalCount).toInt().toDouble(); + } else if (scaled >= 2.6) { + return 5 * math.pow(10, decimalCount).toInt().toDouble(); + } else if (scaled >= 1.6) { + return 2 * math.pow(10, decimalCount).toInt().toDouble(); + } else { + return 1 * math.pow(10, decimalCount).toInt().toDouble(); + } + } + + /// billion number + /// in short scale (https://en.wikipedia.org/wiki/Billion) + static const double billion = 1000000000; + + /// million number + static const double million = 1000000; + + /// kilo (thousands) number + static const double kilo = 1000; + + /// Returns count of fraction digits of a value + int getFractionDigits(double value) { + if (value >= 1) { + return 1; + } else if (value >= 0.1) { + return 2; + } else if (value >= 0.01) { + return 3; + } else if (value >= 0.001) { + return 4; + } else if (value >= 0.0001) { + return 5; + } else if (value >= 0.00001) { + return 6; + } else if (value >= 0.000001) { + return 7; + } else if (value >= 0.0000001) { + return 8; + } else if (value >= 0.00000001) { + return 9; + } else if (value >= 0.000000001) { + return 10; + } + return 1; + } + + /// Formats and add symbols (K, M, B) at the end of number. + /// + /// if number is larger than [billion], it returns a short number like 13.3B, + /// if number is larger than [million], it returns a short number line 43M, + /// if number is larger than [kilo], it returns a short number like 4K, + /// otherwise it returns number itself. + /// also it removes .0, at the end of number for simplicity. + String formatNumber(double axisMin, double axisMax, double axisValue) { + final isNegative = axisValue < 0; + + if (isNegative) { + axisValue = axisValue.abs(); + } + + String resultNumber; + String symbol; + if (axisValue >= billion) { + resultNumber = (axisValue / billion).toStringAsFixed(1); + symbol = 'B'; + } else if (axisValue >= million) { + resultNumber = (axisValue / million).toStringAsFixed(1); + symbol = 'M'; + } else if (axisValue >= kilo) { + resultNumber = (axisValue / kilo).toStringAsFixed(1); + symbol = 'K'; + } else { + final diff = (axisMin - axisMax).abs(); + resultNumber = axisValue.toStringAsFixed( + getFractionDigits(diff), + ); + symbol = ''; + } + + if (resultNumber.endsWith('.0')) { + resultNumber = resultNumber.substring(0, resultNumber.length - 2); + } + + if (isNegative) { + resultNumber = '-$resultNumber'; + } + + if (resultNumber == '-0') { + resultNumber = '0'; + } + + return resultNumber + symbol; + } + + /// Returns a TextStyle based on provided [context], if [providedStyle] provided we try to merge it. + TextStyle getThemeAwareTextStyle( + BuildContext context, + TextStyle? providedStyle, + ) { + final defaultTextStyle = DefaultTextStyle.of(context); + var effectiveTextStyle = providedStyle; + if (providedStyle == null || providedStyle.inherit) { + effectiveTextStyle = defaultTextStyle.style.merge(providedStyle); + } + if (MediaQuery.boldTextOf(context)) { + effectiveTextStyle = effectiveTextStyle! + .merge(const TextStyle(fontWeight: FontWeight.bold)); + } + return effectiveTextStyle!; + } + + /// Finds the best initial interval value + /// + /// If there is a zero point in the axis, we want to have a value that passes through it. + /// For example if we have -3 to +3, with interval 2. if we start from -3, we get something like this: -3, -1, +1, +3 + /// But the most important point is zero in most cases. with this logic we get this: -2, 0, 2 + double getBestInitialIntervalValue( + double min, + double max, + double interval, { + double baseline = 0.0, + }) { + final diff = baseline - min; + final mod = diff % interval; + if ((max - min).abs() <= mod) { + return min; + } + if (mod == 0) { + return min; + } + return min + mod; + } + + /// Converts radius number to sigma for drawing shadows + double convertRadiusToSigma(double radius) => radius * 0.57735 + 0.5; +} diff --git a/pub_screenshots/bar_chart.jpg b/pub_screenshots/bar_chart.jpg new file mode 100644 index 0000000..06b348a Binary files /dev/null and b/pub_screenshots/bar_chart.jpg differ diff --git a/pub_screenshots/bar_chart_sample_7.gif b/pub_screenshots/bar_chart_sample_7.gif new file mode 100644 index 0000000..4677197 Binary files /dev/null and b/pub_screenshots/bar_chart_sample_7.gif differ diff --git a/pub_screenshots/line_chart.jpg b/pub_screenshots/line_chart.jpg new file mode 100644 index 0000000..2710ff1 Binary files /dev/null and b/pub_screenshots/line_chart.jpg differ diff --git a/pub_screenshots/line_chart_sample_10.gif b/pub_screenshots/line_chart_sample_10.gif new file mode 100644 index 0000000..eaee828 Binary files /dev/null and b/pub_screenshots/line_chart_sample_10.gif differ diff --git a/pub_screenshots/logo_1024.png b/pub_screenshots/logo_1024.png new file mode 100644 index 0000000..2e9c3b1 Binary files /dev/null and b/pub_screenshots/logo_1024.png differ diff --git a/pub_screenshots/pie_chart.jpg b/pub_screenshots/pie_chart.jpg new file mode 100644 index 0000000..ee1cefa Binary files /dev/null and b/pub_screenshots/pie_chart.jpg differ diff --git a/pub_screenshots/radar_chart_sample_1.jpg b/pub_screenshots/radar_chart_sample_1.jpg new file mode 100644 index 0000000..efa442d Binary files /dev/null and b/pub_screenshots/radar_chart_sample_1.jpg differ diff --git a/pub_screenshots/scatter_chart_sample_2.gif b/pub_screenshots/scatter_chart_sample_2.gif new file mode 100644 index 0000000..68144c7 Binary files /dev/null and b/pub_screenshots/scatter_chart_sample_2.gif differ diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..0fb989c --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,53 @@ +name: fl_chart +description: A highly customizable Flutter chart library that supports Line Chart, Bar Chart, Pie Chart, Scatter Chart, and Radar Chart. +version: 1.0.0 +homepage: https://flchart.dev/ +repository: https://github.com/imaNNeo/fl_chart +issue_tracker: https://github.com/imaNNeo/fl_chart/issues +documentation: https://github.com/imaNNeo/fl_chart + +environment: + sdk: ">=3.6.2 <4.0.0" + flutter: ">=3.27.4" + +funding: + - https://github.com/sponsors/imaNNeo + - https://www.buymeacoffee.com/fl_chart + +dependencies: + equatable: ^2.0.7 + flutter: + sdk: flutter + vector_math: ^2.1.4 + +dev_dependencies: + build_runner: ^2.4.15 + flutter_test: + sdk: flutter + mockito: ^5.4.6 + very_good_analysis: ^7.0.0 + +screenshots: + - description: "FL Chart Logo" + path: pub_screenshots/logo_1024.png + - description: "LineChartSample1 and LineChartSample2" + path: pub_screenshots/line_chart.jpg + - description: "LineChartSample10" + path: pub_screenshots/line_chart_sample_10.gif + - description: "BarChartSample1 and BarChartSample2" + path: pub_screenshots/bar_chart.jpg + - description: "BarChartSample7" + path: pub_screenshots/bar_chart_sample_7.gif + - description: "PieChartSample1, PieChartSample2 and PieChartSample3" + path: pub_screenshots/pie_chart.jpg + - description: "ScatterChartSample2" + path: pub_screenshots/scatter_chart_sample_2.gif + - description: "RadarChartSample1" + path: pub_screenshots/radar_chart_sample_1.jpg + +topics: + - chart + - charts + - visualization + - graph + - diagram diff --git a/repo_files/documentations/bar_chart.md b/repo_files/documentations/bar_chart.md new file mode 100644 index 0000000..d7fe677 --- /dev/null +++ b/repo_files/documentations/bar_chart.md @@ -0,0 +1,166 @@ + + +### How to use +```dart +BarChart( + BarChartData( + // read about it in the BarChartData section + ), + duration: Duration(milliseconds: 150), // Optional + curve: Curves.linear, // Optional +); +``` + +### Implicit Animations +When you change the chart's state, it animates to the new state internally (using [implicit animations](https://flutter.dev/docs/development/ui/animations/implicit-animations)). You can control the animation [duration](https://api.flutter.dev/flutter/dart-core/Duration-class.html) and [curve](https://api.flutter.dev/flutter/animation/Curves-class.html) using optional `duration` and `curve` properties, respectively. + +### BarChartData +|PropName |Description |default value| +|:---------------|:---------------|:-------| +|barGroups| list of [BarChartGroupData ](#BarChartGroupData) to show the bar lines together, you can provide one item per group to show normal bar chart|[]| +|groupsSpace| space between groups, it applies only when the [alignment](#BarChartAlignment) is `BarChartAlignment.start`, `BarChartAlignment.center` or `BarChartAlignment.end`|16| +|alignment| a [BarChartAlignment](#BarChartAlignment) that determines the alignment of the barGroups, inspired by [Flutter MainAxisAlignment](https://docs.flutter.io/flutter/rendering/MainAxisAlignment-class.html)| BarChartAlignment.spaceEvenly| +|titlesData| check the [FlTitlesData](base_chart.md#FlTitlesData)|FlTitlesData()| +|axisTitleData| check the [FlAxisTitleData](base_chart.md#FlAxisTitleData)| FlAxisTitleData()| +|rangeAnnotations| show range annotations behind the chart, check [RangeAnnotations](base_chart.md#RangeAnnotations) | RangeAnnotations()| +|backgroundColor| a background color which is drawn behind the chart| null | +|barTouchData| [BarTouchData](#bartouchdata-read-about-touch-handling) holds the touch interactivity details|BarTouchData()| +|gridData| check the [FlGridData](base_chart.md#FlGridData)|FlGridData()| +|borderData| check the [FlBorderData](base_chart.md#FlBorderData)|FlBorderData()| +|maxY| gets maximum y of y axis, if null, value will be read from the input barGroups (But it is more performant if you provide them) | null| +|minY| gets minimum y of y axis, if null, value will be read from the input barGroups (But it is more performant if you provide them) | null| +|baselineY| defines the baseline of y-axis | 0| +|extraLinesData| allows extra horizontal lines to be drawn on the chart. Vertical lines are ignored when used with BarChartData, please see [#1149](https://github.com/imaNNeo/fl_chart/issues/1149), check [ExtraLinesData](base_chart.md#ExtraLinesData)|ExtraLinesData()| +|rotationQuarterTurns|Rotates the chart 90 degrees (clockwise) in every quarter turns. This feature works like the [RotatedBox](https://api.flutter.dev/flutter/widgets/RotatedBox-class.html) widget. You can have horizontal BarChart by changing this value to |0| +|errorIndicatorData|Holds data for representing an error indicator (you see the error indicators if you provide the `toYErrorRange` in the [BarChartRodData](#BarChartRodData))|[ErrorIndicatorData()](base_chart.md#FlErrorIndicatorData)| + +### BarChartGroupData +|PropName |Description |default value| +|:---------------|:---------------|:-------| +|x| x position of the group on horizontal axis|null| +|barRods| list of [BarChartRodData](#BarChartRodData) that are a bar line| [] +|barsSpace| the space between barRods of the group|2| +|showingTooltipIndicators| indexes of barRods to show the tooltip on top of them, The point is that you need to disable touches to show these tooltips manually | []| + + +### BarChartAlignment +enum values {`start`, `end`, `center`, `spaceEvenly`, `spaceAround`, `spaceBetween`} + + +### BarChartRodData +|PropName|Description|default value| +|:-------|:----------|:------------| +|fromY|Position that this bar starts from|0| +|toY|This rod is from `fromY` to `toY` in the vertical axis|null| +|color|color of the rod bar|[Colors.cyan]| +|gradient| You can use any [Gradient](https://api.flutter.dev/flutter/dart-ui/Gradient-class.html) here. such as [LinearGradient](https://api.flutter.dev/flutter/painting/LinearGradient-class.html) or [RadialGradient](https://api.flutter.dev/flutter/painting/RadialGradient-class.html)|null| +|width|stroke width of the rod bar|8| +|borderRadius|Determines the edge rounding of the bar corners, see [BorderRadius](https://api.flutter.dev/flutter/painting/BorderRadius-class.html). When `null`, it defaults to completely round bars. |null| +|borderDashArray|Determines wether the border stroke is dashed. It is a circular array of dash offsets and lengths. For example, the array `[5, 10]` would result in dashes 5 pixels long followed by blank spaces 10 pixels long. The array `[5, 10, 5]` would result in a 5 pixel dash, a 10 pixel gap, a 5 pixel dash, a 5 pixel gap, a 10 pixel dash, etc.|null| +|borderSide|Determines the border stroke around of the bar, see [BorderSide](https://api.flutter.dev/flutter/painting/BorderSide-class.html). When `null`, it defaults to draw no stroke. |null| +|backDrawRodData|if provided, draws a rod in the background of the line bar, check the [BackgroundBarChartRodData](#BackgroundBarChartRodData)|null| +|rodStackItem|if you want to have stacked bar chart, provide a list of [BarChartRodStackItem](#BarChartRodStackItem), it will draw over your rod.|[]| +|toYErrorRange|If you want to show an error range on the rod, provide [FlErrorRange](base_chart.md#FlErrorRange)|null| + +### BackgroundBarChartRodData +|PropName|Description|default value| +|:-------|:----------|:------------| +|fromY|same as [BarChartRodData](#BarChartRodData)'s fromY|0| +|toY|same as [BarChartRodData](#BarChartRodData)'s y|8| +|show|determines to show or hide this section|false| +|color|same as [BarChartRodData](#BarChartRodData)'s colors|[Colors.blueGrey]| +|gradient|same as [BarChartRodData](#BarChartRodData)'s gradient|null| + +### BarChartRodStackItem +|PropName|Description|default value| +|:-------|:----------|:------------| +|fromY|draw stack item from this value|null| +|toY|draw stack item to this value|null| +|color|color of the stack item|null| +|borderSide|draw border stroke for each stack item|null| + +### BarTouchData ([read about touch handling](handle_touches.md)) +|PropName|Description|default value| +|:-------|:----------|:------------| +|enabled|determines to enable or disable touch behaviors|true| +|mouseCursorResolver|you can change the mouse cursor based on the provided [FlTouchEvent](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#fltouchevent) and [BarTouchResponse](#BarTouchResponse)|MouseCursor.defer| +|touchTooltipData|a [BarTouchTooltipData](#BarTouchTooltipData), that determines how show the tooltip on top of touched spots (appearance of the showing tooltip bubble)|BarTouchTooltipData()| +|touchExtraThreshold|an [EdgeInsets](https://api.flutter.dev/flutter/painting/EdgeInsets-class.html) class to hold a bounding threshold of touch accuracy|EdgeInsets.all(4)| +|allowTouchBarBackDraw| if sets true, touch works on backdraw bar line| false | +|handleBuiltInTouches| set this true if you want the built in touch handling (show a tooltip bubble and an indicator on touched spots) | true| +|touchCallback| listen to this callback to retrieve touch/pointer events and responses, it gives you a [FlTouchEvent](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#fltouchevent) and [BarTouchResponse](#BarTouchResponse)| null| +|longPressDuration| allows to customize the duration of the longPress gesture. If null, the duration of the longPressGesture is [kLongPressTimeout](https://api.flutter.dev/flutter/gestures/kLongPressTimeout-constant.html)| null| + +### BarTouchTooltipData + |PropName|Description|default value| + |:-------|:----------|:------------| + |tooltipBorder|border of the tooltip bubble|BorderSide.none| + |tooltipBorderRadius|background corner radius of the tooltip bubble|BorderRadius.circular(4)| + |tooltipPadding|padding of the tooltip|EdgeInsets.symmetric(horizontal: 16, vertical: 8)| + |tooltipMargin|margin between the tooltip and the touched spot|16| + |tooltipHorizontalAlignment|horizontal alginment of tooltip relative to the bar|FLHorizontalAlignment.center| + |tooltipHorizontalOffset|horizontal offset of tooltip|0| + |maxContentWidth|maximum width of the tooltip (if a text row is wider than this, then the text breaks to a new line|120| + |getTooltipItems|a callback that retrieve [BarTooltipItem](#BarTooltipItem) by the given [BarChartGroupData](#BarChartGroupData), groupIndex, [BarChartRodData](#BarChartRodData) and rodIndex |defaultBarTooltipItem| + |fitInsideHorizontally| forces tooltip to horizontally shift inside the chart's bounding box| false| + |fitInsideVertically| forces tooltip to vertically shift inside the chart's bounding box| false| + |direction| Controls showing tooltip on top or bottom, default is auto.| auto| + |getTooltipColor|a callback that retrieves the Color for each rod separately from the given [BarChartGroupData](#BarChartGroupData) to set the background color of the tooltip bubble|Colors.blueGrey.darken(15)| + +### BarTooltipItem +|PropName|Description|default value| +|:-------|:----------|:------------| +|text|text string of each row in the tooltip bubble|null| +|textStyle|[TextStyle](https://api.flutter.dev/flutter/dart-ui/TextStyle-class.html) of the showing text row|null| +|textAlign|[TextAlign](https://api.flutter.dev/flutter/dart-ui/TextAlign-class.html) of the showing text row|TextAlign.center| +|textDirection|[TextDirection](https://api.flutter.dev/flutter/dart-ui/TextDirection-class.html) of the showing text row|TextDirection.ltr| +|children|[List](https://api.flutter.dev/flutter/painting/InlineSpan-class.html) pass additional InlineSpan children for a more advance tooltip|null| + + +### BarTouchResponse +|PropName|Description|default value| +|:-------|:----------|:------------| +|touchLocation|the location of the touch event in the device pixels coordinates|required| +|touchChartCoordinate|the location of the touch event in the chart coordinates|required| +|spot|a [BarTouchedSpot](#BarTouchedSpot) class to hold data about touched spot| null | + +### BarTouchedSpot +|PropName|Description|default value| +|:-------|:----------|:------------| +|touchedBarGroup|the [BarChartGroupData](#BarChartGroupData) that user touched its rod's spot| null | +|touchedBarGroupIndex| index of touched barGroup| null| +|touchedRodData|the [BarChartRodData](#BarChartRodData) that user touched its spot|null| +|touchedRodDataIndex| index of touchedRod | null| +|touchedStackItem| [BarChartRodStackItem](#BarChartRodStackItem) is the touched stack (if you have stacked bar chart) |null| +|touchedStackItemIndex| index of barChartRodStackItem, -1 if nothing found | -1| + + +### Some Samples +---- +##### Sample 1 ([Source Code](/example/lib/presentation/samples/bar/bar_chart_sample1.dart)) + + + + + + +##### Sample 2 ([Source Code](/example/lib/presentation/samples/bar/bar_chart_sample2.dart)) + + +##### Sample 3 ([Source Code](/example/lib/presentation/samples/bar/bar_chart_sample3.dart)) + + +##### Sample 4 ([Source Code](/example/lib/presentation/samples/bar/bar_chart_sample4.dart)) + + +##### Sample 5 ([Source Code](/example/lib/presentation/samples/bar/bar_chart_sample5.dart)) + + +##### Sample 6 ([Source Code](/example/lib/presentation/samples/bar/bar_chart_sample6.dart)) + + +##### Sample 7 ([Source Code](/example/lib/presentation/samples/bar/bar_chart_sample7.dart)) + + +##### Gist - Toggleable Tooltip ([Source Code](https://gist.github.com/imaNNeo/bce3f0169ff3fd6c3f137cdeb5005c0e)) +https://user-images.githubusercontent.com/7009300/156784816-53f95dd9-f387-4600-8a92-d05b1aeea3da.mov diff --git a/repo_files/documentations/base_chart.md b/repo_files/documentations/base_chart.md new file mode 100644 index 0000000..3b59a48 --- /dev/null +++ b/repo_files/documentations/base_chart.md @@ -0,0 +1,209 @@ +# BaseChart + +### FlBorderData +|PropName |Description |default value| +|:---------------|:---------------|:-------| +|show| determines to show or hide the border |true| +|border| [Border](https://api.flutter.dev/flutter/painting/Border-class.html) details that determines which border should be drawn with which color| Border.all(color: Colors.black, width: 1.0, style: BorderStyle.solid)| + + +### FlTitlesData +|PropName |Description |default value| +|:---------------|:---------------|:-------| +|show| determines to show or hide the titles Around the chart|true| +|leftTitles| an [AxisTitle](#AxisTitle) that holds data to draw left titles | AxisTitles(sideTitles: SideTitles(reservedSize: 40, showTitles: true))| +|topTitles| an [AxisTitle](#AxisTitle) that holds data to draw top titles | AxisTitles(sideTitles: SideTitles(reservedSize: 6, showTitles: true))| +|rightTitles| an [AxisTitle](#AxisTitle) that holds data to draw right titles | AxisTitles(sideTitles: SideTitles(reservedSize: 40, showTitles: true))| +|bottomTitles| an [AxisTitle](#AxisTitle) that holds data to draw bottom titles | AxisTitles(sideTitles: SideTitles(reservedSize: 6, showTitles: true))| + +### AxisTitle +|PropName |Description |default value| +|:---------------|:---------------|:-------| +|axisNameSize| Determines the size of [axisName] | `16`| +|axisNameWidget| It shows the name of axis (you can pass a Widget)| `null`| +|sideTitles| It accepts a [SideTitles](#SideTitles) which is responsible to show your axis side titles| `SideTitles()`| +|drawBehindEverything| If titles are showing on top of your tooltip, you can draw them behind everything.| `true`| + +### SideTitles +|PropName |Description |default value| +|:---------------|:---------------|:-------| +|showTitles| determines whether to show or hide the titles | false| +|getTitlesWidget| A function to retrieve the title widget with given value on the related axis.|defaultGetTitle| +|reservedSize| It determines the maximum space that your titles need, |22| +|interval| Texts are showing with provided `interval`. If you don't provide anything, we try to find a suitable value to set as `interval` under the hood. | null | +|minIncluded| Determines whether to include title for minimum data value | true | +|maxIncluded| Determines whether to include title for maximum data value | true | + + +### SideTitleFitInsideData +|PropName |Description |default value| +|:---------------|:---------------|:-------| +|enabled| determines whether to enable fit inside to SideTitleWidget |true| +|axisPosition| position (in pixel) that applied to the center of child widget along its corresponding axis |null| +|parentAxisSize| child widget's corresponding axis maximum width/height |null| +|distanceFromEdge| distance between child widget and its closest corresponding axis edge | 6 | + +### FlGridData +|PropName|Description|default value| +|:-------|:----------|:------------| +|show|determines to show or hide the background grid data|true| +|drawHorizontalLine|determines to show or hide the horizontal grid lines|true| +|horizontalInterval|interval space of grid, left it null to be calculate automatically |null| +|getDrawingHorizontalLine|a function to get the line style of each grid line by giving the related axis value|defaultGridLine| +|checkToShowHorizontalLine|a function to check whether to show or hide the horizontal grid by giving the related axis value |showAllGrids| +|drawVerticalLine|determines to show or hide the vertical grid lines|true| +|verticalInterval|interval space of grid, left it null to be calculate automatically |null| +|getDrawingVerticalLine|a function to get the line style of each grid line by giving the related axis value|defaultGridLine| +|checkToShowVerticalLine|a function to determine whether to show or hide the vertical grid by giving the related axis value |showAllGrids| + + + +### FlSpot +|PropName|Description|default value| +|:-------|:----------|:------------| +|x|represents x on the coordinate system (x starts from left)|null| +|y|represents y on the coordinate system (y starts from bottom)|null| +|xError| Determines the error range of the data point using (FlErrorRange)[#FlErrorRange] (which ontains `lowerBy` and `upperValue`) for the x-axis|null| +|yError| Determines the error range of the data point using (FlErrorRange)[#FlErrorRange] (which ontains `upperBy` and `upperValue`) for the y-axis|null| + + +### FlLine +|propName|Description|default value| +|:-------|:----------|:------------| +|color|determines the color of line|Colors.black| +|gradient|gradient of the line (you have to provide either `color` or `gradient`|null| +|strokeWidth|determines the stroke width of the line|2| +|dashArray|A circular array of dash offsets and lengths. For example, the array `[5, 10]` would result in dashes 5 pixels long followed by blank spaces 10 pixels long. The array `[5, 10, 5]` would result in a 5 pixel dash, a 10 pixel gap, a 5 pixel dash, a 5 pixel gap, a 10 pixel dash, etc.|null| + + +### TouchedSpot +|PropName|Description|default value| +|:-------|:----------|:------------| +|spot|the touched [FlSpot](#FlSpot)|null| +|offset|[Offset](https://api.flutter.dev/flutter/dart-ui/Offset-class.html) of the touched spot|null| + +### RangeAnnotations +|PropName|Description|default value| +|:-------|:----------|:------------| +|horizontalRangeAnnotations|list of [horizontalRangeAnnotation](#HorizontalRangeAnnotation) to draw on the chart|[]| +|verticalRangeAnnotations|list of [VerticalRangeAnnotation](#VerticalRangeAnnotation) to draw on the chart|[]| + + +### HorizontalRangeAnnotation +|PropName|Description|default value| +|:-------|:----------|:------------| +|y1|start interval of horizontal rectangle|null| +|y2|end interval of horizontal rectangle|null| +|color|color of the rectangle|Colors.white| +|gradient|gradient of the rectangle|null| + + +### VerticalRangeAnnotation +|PropName|Description|default value| +|:-------|:----------|:------------| +|x1|start interval of vertical rectangle|null| +|x2|end interval of vertical rectangle|null| +|color|color of the rectangle|Colors.white| +|gradient|gradient of the rectangle|null| + +### FlTouchEvent +Base class for all supported touch/pointer events. + +|PropName|Description|Inspired from| +|:-------|:----------|:----------| +|FlPanDownEvent|Contains information of happened touch gesture|[GestureDragDownCallback](https://api.flutter.dev/flutter/gestures/GestureDragDownCallback.html)| +|FlPanStartEvent|When a pointer has contacted the screen and has begun to move.|[GestureDragStartCallback](https://api.flutter.dev/flutter/gestures/GestureDragStartCallback.html)| +|FlPanUpdateEvent|When a pointer that is in contact with the screen and moving has moved again.|[GestureDragUpdateCallback](https://api.flutter.dev/flutter/gestures/GestureDragUpdateCallback.html)| +|FlPanCancelEvent|When the pointer that previously triggered a `FlPanStartEvent` did not complete.|[GestureDragCancelCallback](https://api.flutter.dev/flutter/gestures/GestureDragCancelCallback.html)| +|FlPanEndEvent|When a pointer that was previously in contact with the screen and moving is no longer in contact with the screen.|[GestureDragEndCallback](https://api.flutter.dev/flutter/gestures/GestureDragEndCallback.html)| +|FlTapDownEvent|When a pointer that might cause a tap has contacted the screen.|[GestureTapDownCallback](https://api.flutter.dev/flutter/gestures/GestureTapDownCallback.html)| +|FlTapCancelEvent|When the pointer that previously triggered a `FlTapDownEvent` will not end up causing a tap.|[GestureTapCancelCallback](https://api.flutter.dev/flutter/gestures/GestureTapCancelCallback.html)| +|FlTapUpEvent|When a pointer that will trigger a tap has stopped contacting the screen.|[GestureTapUpCallback](https://api.flutter.dev/flutter/gestures/GestureTapUpCallback.html)| +|FlLongPressStart|Called When a pointer has remained in contact with the screen at the same location for a long period of time.|[GestureLongPressStartCallback](https://api.flutter.dev/flutter/gestures/GestureLongPressStartCallback.html)| +|FlLongPressMoveUpdate|When a pointer is moving after being held in contact at the same location for a long period of time. Reports the new position and its offset from the original down position.|[GestureLongPressMoveUpdateCallback](https://api.flutter.dev/flutter/gestures/GestureLongPressMoveUpdateCallback.html)| +|FlLongPressEnd|When a pointer stops contacting the screen after a long press gesture was detected. Also reports the position where the pointer stopped contacting the screen.|[GestureLongPressEndCallback](https://api.flutter.dev/flutter/gestures/GestureLongPressEndCallback.html)| +|FlPointerEnterEvent|The pointer has moved with respect to the device while the pointer is or is not in contact with the device, and it has entered our chart.|[PointerEnterEventListener](https://api.flutter.dev/flutter/services/PointerEnterEventListener.html)| +|FlPointerHoverEvent|The pointer has moved with respect to the device while the pointer is not in contact with the device.|[PointerHoverEventListener](https://api.flutter.dev/flutter/services/PointerHoverEventListener.html)| +|FlPointerExitEvent|The pointer has moved with respect to the device while the pointer is or is not in contact with the device, and exited our chart.|[PointerExitEventListener](https://api.flutter.dev/flutter/services/PointerExitEventListener.html)| + +### ExtraLinesData +|PropName|Description|default value| +|:-------|:----------|:------------| +|extraLinesOnTop|determines to paint the extraLines over the trendline or below it|true| +|horizontalLines|list of [HorizontalLine](#HorizontalLine) to draw on the chart|[]| +|verticalLines|list of [VerticalLine](#VerticalLine) to draw on the chart|[]| + + +### HorizontalLine +|PropName|Description|default value| +|:-------|:----------|:------------| +|y|draw straight line from left to right of the chart with dynamic y value|null| +|color|color of the line|Colors.black| +|gradient|gradient of the line (you have to provide either `color` or `gradient`|null| +|strokeWidth|strokeWidth of the line|2| +|strokeCap|strokeCap of the line,e.g. Setting to StrokeCap.round will draw the tow ends of line rounded. NOTE: this might not work on dash lines.|StrokeCap.butt| +|image|image to annotate the line. the Future must be complete at the time this is received by the chart|null| +|sizedPicture|[SizedPicture](#Sizedpicture) uses an svg to annotate the line with a picture. the Future must be complete at the time this is received by the chart|null| +|label|a [HorizontalLineLabel](#HorizontalLineLabel) object with label parameters|null + +### VerticalLine +|PropName|Description|default value| +|:-------|:----------|:------------| +|x|draw straight line from bottom to top of the chart with dynamic x value|null| +|color|color of the line|Colors.black| +|gradient|gradient of the line (you have to provide either `color` or `gradient`|null| +|strokeWidth|strokeWidth of the line|2| +|strokeCap|strokeCap of the line,e.g. Setting to StrokeCap.round will draw the tow ends of line rounded. NOTE: this might not work on dash lines.|StrokeCap.butt| +|image|image to annotate the line. the Future must be complete at the time this is received by the chart|null| +|sizedPicture|[SizedPicture](#SizedPicture) uses an svg to annotate the line with a picture. the Future must be complete at the time this is received by the chart|null| +|label|a [VerticalLineLabel](#VerticalLineLabel) object with label parameters|null + +### SizedPicture +|PropName|Description|default value| +|:-------|:----------|:------------| +|Picture|a Dart UI Picture which should be derived from the svg. see example for how to get a Picture from an svg.|null| +|width|the width of the picture|null| +|height|the height of the picture|null| + +### HorizontalLineLabel +|PropName|Description|default value| +|:-------|:----------|:------------| +|show| Determines showing or not showing label|false| +|padding|[EdgeInsets](https://api.flutter.dev/flutter/painting/EdgeInsets-class.html) object with label padding configuration|EdgeInsets.zero| +|style|[TextStyle](https://api.flutter.dev/flutter/dart-ui/TextStyle-class.html) which determines label text style|TextStyle(fontSize: 11, color: line.color)| +|alignment|[Alignment](https://api.flutter.dev/flutter/painting/Alignment-class.html) with label position relative to line|Alignment.topLeft| +|direction|Direction of the text (horizontal or vertical)|LabelDirection.horizontal| +|labelResolver|Getter function returning label title|defaultLineLabelResolver| + +### VerticalLineLabel +|PropName|Description|default value| +|:-------|:----------|:------------| +|show| Determines showing or not showing label|false| +|padding|[EdgeInsets](https://api.flutter.dev/flutter/painting/EdgeInsets-class.html) object with label padding configuration|EdgeInsets.zero| +|style|[TextStyle](https://api.flutter.dev/flutter/dart-ui/TextStyle-class.html) which determines label text style|TextStyle(fontSize: 11, color: line.color)| +|alignment|[Alignment](https://api.flutter.dev/flutter/painting/Alignment-class.html) with label position relative to line|Alignment.topLeft| +|direction|Direction of the text (horizontal or vertical)|LabelDirection.horizontal| +|labelResolver|Getter function returning label title|defaultLineLabelResolver| + +### FLHorizontalAlignment +enum values {`center`, `left`, `right`} + + +### FlErrorIndicatorData +|PropName| Description | default value | +|:-------|:------------------------------------------------------------|:-----------------------| +|show| Determines showing or not showing error indicator/threshold | true | +|painter| A callback that allows you to provide a custom painter for the error indicator| FlSimpleErrorPainter() | + +### FlErrorRange +|PropName| Description | default value | +|:-------|:-------------------------------------------------------------------------------|:-----------------------| +|lowerBy| Lower value of the error range. It is subtracted from the spot value and shoul be positive| null| +|upperBy| Upper value of the error range. It is added to the spot value and shoul be positive| null| + +### AxisSpotIndicator +|PropName|Description|default value| +|:-------|:----------|:------------| +|x|x value of the touched spot|required| +|y|y value of the touched spot|required| +|AxisSpotIndicatorPainter|a painter that is used to draw the touched spot indicator. You can use this to customize the appearance of the touched spot indicator (Or you can implement your own painter).|AxisLinesIndicatorPainter()| diff --git a/repo_files/documentations/candlestick_chart.md b/repo_files/documentations/candlestick_chart.md new file mode 100644 index 0000000..dfcfcc7 --- /dev/null +++ b/repo_files/documentations/candlestick_chart.md @@ -0,0 +1,104 @@ +# CandlestickChart + + + +### How to use +```dart +CandlestickChart( + CandlestickChartData( + // read about it in the CandlestickChartData section + ), + duration: Duration(milliseconds: 150), // Optional + curve: Curves.linear, // Optional +); +``` + +### Implicit Animations +When you change the chart's state, it animates to the new state internally (using [implicit animations](https://flutter.dev/docs/development/ui/animations/implicit-animations)). You can control the animation [duration](https://api.flutter.dev/flutter/dart-core/Duration-class.html) and [curve](https://api.flutter.dev/flutter/animation/Curves-class.html) using optional `duration` and `curve` properties, respectively. + +### CandlestickChartData +|PropName |Description |default value| +|:---------------|:---------------|:-------| +|candlestickSpots| Holds the data for the candlestick chart, which is a list of [CandlestickSpot](#CandlestickSpot) objects. Each [CandlestickSpot](#CandlestickSpot) represents a single data point in the chart.|[]| +|candlestickPainter| It is a painter that is used to draw each individual candlestick. You can use this to customize the appearance of the candlesticks (Or you can implement your own painter).|DefaultCandlestickPainter()| +|titlesData| check the [FlAxisTitleData](base_chart.md#FlAxisTitleData)| FlAxisTitleData()| +|candlestickTouchData| [CandlestickTouchData](#CandlestickTouchData) holds the touch interactivity details| CandlestickTouchData()| +|showingTooltipIndicators| indices of showing tooltip, The point is that you need to disable touches to show these tooltips manually|[]| +|gridData|check the [FlGridData](base_chart.md#FlGridData)|FlGridData()| +|borderData|check the [FlBorderData](base_chart.md#FlBorderData)|FlBorderData()| +|minX|gets minimum x of x axis, if null, value will read from the input lineBars (But it is more performant if you provide them)|null| +|maxX|gets maximum x of x axis, if null, value will read from the input lineBars (But it is more performant if you provide them)| null| +|baselineX|defines the baseline of x-axis | 0| +|minY|gets minimum y of y axis, if null, value will read from the input lineBars (But it is more performant if you provide them)| null| +|maxY|gets maximum y of y axis, if null, value will read from the input lineBars (But it is more performant if you provide them)| null| +|baselineY|defines the baseline of y-axis | 0| +|rangeAnnotations|show range annotations behind the chart, check [RangeAnnotations](base_chart.md#RangeAnnotations) | RangeAnnotations()| +|clipData|clip the chart to the border (prevent drawing outside the border) | FlClipData.none()| +|backgroundColor|a background color which is drawn behind th chart| null | +|rotationQuarterTurns|Rotates the chart 90 degrees (clockwise) in every quarter turns. This feature works like the [RotatedBox](https://api.flutter.dev/flutter/widgets/RotatedBox-class.html) widget|0| +|touchedPointIndicator|Shows the touched point in the chart, by default it shows a horizontal and vertical line exactly on the touched candle. If the `handleBuiltInTouches` is true in [CandlestickTouchData](#CandlestickTouchData), this parameter is used under the hood to highlight the selected point. But you can disable the `handleBuiltInTouches` and implement your own way to highlight the point. Look at [AxisSpotIndicator](base_chart.md#AxisSpotIndicator) for more information |null| + +### CandlestickSpot +|PropName |Description |default value| +|:---------------|:---------------|:-------| +|open| The open value of the candlestick (based on the [OHLC standard](https://en.wikipedia.org/wiki/Open-high-low-close_chart)|required| +|high| The high value of the candlestick (based on the [OHLC standard](https://en.wikipedia.org/wiki/Open-high-low-close_chart))|required| +|low| The low value of the candlestick (based on the [OHLC standard](https://en.wikipedia.org/wiki/Open-high-low-close_chart))|required| +|close| The close value of the candlestick (based on the [OHLC standard](https://en.wikipedia.org/wiki/Open-high-low-close_chart))|required| +|show| Determines to show or hide this individual candlestick|true| + + +### CandlestickTouchData ([read about touch handling](handle_touches.md)) +|PropName|Description|default value| +|:-------|:----------|:------------| +|enabled|determines to enable or disable touch behaviors|true| +|touchCallback| listen to this callback to retrieve touch/pointer events and responses, it gives you a [FlTouchEvent](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#fltouchevent) and [CandlestickTouchResponse](#CandlestickTouchResponse)| null| +|mouseCursorResolver|you can change the mouse cursor based on the provided [FlTouchEvent](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#fltouchevent) and [CandlestickTouchResponse](#CandlestickTouchResponse)|MouseCursor.defer| +|touchTooltipData|a [CandlestickTouchTooltipData](#CandlestickTouchTooltipData), that determines how show the tooltip on top of touched spot (appearance of the showing tooltip bubble)|CandlestickTouchTooltipData()| +|touchSpotThreshold|the threshold of the touch accuracy|4| +|handleBuiltInTouches| set this true if you want the built in touch handling (show a tooltip bubble and an indicator on touched/hovered spots) | true| +|longPressDuration| allows to customize the duration of the longPress gesture. If null, the duration of the longPressGesture is [kLongPressTimeout](https://api.flutter.dev/flutter/gestures/kLongPressTimeout-constant.html)| null| + +### CandlestickTouchTooltipData +|PropName|Description|default value| +|:-------|:----------|:------------| +|tooltipBorder|border of the tooltip bubble|BorderSide.none| +|tooltipBorderRadius|background corner radius of the tooltip bubble|BorderRadius.circular(4)| +|tooltipPadding|padding of the tooltip|EdgeInsets.symmetric(horizontal: 16, vertical: 8)| +|tooltipHorizontalAlignment|horizontal alginment of tooltip relative to the spot|FLHorizontalAlignment.center| +|tooltipHorizontalOffset|horizontal offset of tooltip|0| +|maxContentWidth|maximum width of the tooltip (if a text row is wider than this, then the text breaks to a new line|120| +|getTooltipItems|a callback that retrieve a [CandlestickTooltipItem](#CandlestickTooltipItem) by the given [CandlestickSpot](#CandlestickSpot) |defaultCandlestickTooltipItem| +|fitInsideHorizontally| forces tooltip to horizontally shift inside the chart's bounding box| false| +|fitInsideVertically| forces tooltip to vertically shift inside the chart's bounding box| false| +|showOnTopOfTheChartBoxArea| forces the tooltip container to top of the line| false| +|getTooltipColor|a callback that retrieves the Color for each touched spots separately from the given [CandlestickSpot](#CandlestickSpot) to set the background color of the tooltip bubble|Colors.blueGrey.darken(80)| + +### CandlestickTooltipItem +|PropName|Description|default value| +|:-------|:----------|:------------| +|text|text string of each row in the tooltip bubble|null| +|textStyle|[TextStyle](https://api.flutter.dev/flutter/dart-ui/TextStyle-class.html) of the showing text row|null| +|textDirection|[TextDirection](https://api.flutter.dev/flutter/dart-ui/TextDirection-class.html) of the showing text row|TextDirection.ltr| +|bottomMargin| bottom margin of the tooltip (to the top of most top spot) | 0| +|children|[List](https://api.flutter.dev/flutter/painting/InlineSpan-class.html) pass additional InlineSpan children for a more advance tooltip|null| + + +### CandlestickTouchResponse +###### you can listen to touch behaviors callback and retrieve this object when any touch action happened. +|PropName|Description|default value| +|:-------|:----------|:------------| +|touchLocation|the location of the touch event in the device pixels coordinates|required| +|touchChartCoordinate|the location of the touch event in the chart coordinates|required| +|touchedSpot|Instance of [CandlestickTouchedSpot](#CandlestickTouchedSpot) which holds data about the touched spot|null| + +### CandlestickTouchedSpot +|PropName|Description|default value| +|:-------|:----------|:------------| +|spot|touched [CandlestickSpot](#CandlestickSpot)|null| +|spotIndex|index of touched [CandlestickSpot](#CandlestickSpot)|null| + +### some samples +---- +##### Sample 1 ([Source Code](/example/lib/presentation/samples/candlestick/candlestick_chart_sample1.dart)) + diff --git a/repo_files/documentations/handle_animations.md b/repo_files/documentations/handle_animations.md new file mode 100644 index 0000000..1f781b2 --- /dev/null +++ b/repo_files/documentations/handle_animations.md @@ -0,0 +1,33 @@ +### Animations +|Sample1 |Sample2 |Sample3 | +|:------------:|:------------:|:-------------:| +| [![](https://github.com/imaNNeo/fl_chart/raw/main/repo_files/images/line_chart/line_chart_sample_1_anim.gif)](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/line_chart.md#sample-1-source-code) | [![](https://github.com/imaNNeo/fl_chart/raw/main/repo_files/images/line_chart/line_chart_sample_2_anim.gif)](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/line_chart.md#sample-2-source-code) | [![](https://github.com/imaNNeo/fl_chart/raw/main/repo_files/images/bar_chart/bar_chart_sample_1_anim.gif)](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/bar_chart.md#sample-1-source-code) | + +##### How? +We handle all animations Implicitly, This is power of the [ImplicitlyAnimatedWidget](https://api.flutter.dev/flutter/widgets/ImplicitlyAnimatedWidget-class.html), just like [AnimatedContainer](https://api.flutter.dev/flutter/widgets/AnimatedContainer-class.html). It means you don't need to do anything, just change any value and the animation is handled under the hood, if you are curious about it, check the source code, reading the source code is the best way to learn things. + + +##### Properties +You can change the [Duration](https://api.flutter.dev/flutter/dart-core/Duration-class.html) and [Curve](https://api.flutter.dev/flutter/animation/Curves-class.html) of animation using `duration` and `curve` properties respectively. + +```dart +LineChart( + duration: Duration(milliseconds: 150), + curve: Curves.linear, + LineChartData( + isShowingMainData ? sampleData1() : sampleData2(), + ), +) +``` + +##### How to disable + +If you want to disable the animations, you can set `Duration.zero` as `duration`. +```dart +LineChart( + duration: Duration.zero, + LineChartData( + // Your chart data here + ), +) + diff --git a/repo_files/documentations/handle_touches.md b/repo_files/documentations/handle_touches.md new file mode 100644 index 0000000..2d8056a --- /dev/null +++ b/repo_files/documentations/handle_touches.md @@ -0,0 +1,51 @@ +### Touch Interactivity + +|LineChart |BarChart |PieChart | +|:------------:|:------------:|:-------------:| +| [![](https://github.com/imaNNeo/fl_chart/raw/main/repo_files/images/line_chart/line_chart_sample_1.gif)](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/line_chart.md#sample-1-source-code) [![](https://github.com/imaNNeo/fl_chart/raw/main/repo_files/images/line_chart/line_chart_sample_2.gif)](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/line_chart.md#sample-2-source-code) | [![](https://github.com/imaNNeo/fl_chart/raw/main/repo_files/images/bar_chart/bar_chart_sample_1.gif)](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/bar_chart.md#sample-1-source-code) [![](https://github.com/imaNNeo/fl_chart/raw/main/repo_files/images/bar_chart/bar_chart_sample_2.gif)](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/bar_chart.md#sample-2-source-code) | [![](https://github.com/imaNNeo/fl_chart/raw/main/repo_files/images/pie_chart/pie_chart_sample_1.gif)](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/pie_chart.md#sample-1-source-code) [![](https://github.com/imaNNeo/fl_chart/raw/main/repo_files/images/pie_chart/pie_chart_sample_2.gif)](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/pie_chart.md#sample-2-source-code) | + + + +#### The Interaction Flow +When an interaction happens, our renderers give us a [FlTouchEvent](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#fltouchevent). +We pass it to correspond painter class. Then it calculates and gives us a TouchResponse (per interaction). +Then we call the touchCallback function that provided through the chart's data. + +If you set `handleBuiltInTouches` true, it will handle touch by showing a tooltip or an indicator on the touched spot (in the line, bar and scatter charts), you can also handle your own touch handling along with the built in touches. + + +#### How to use? (for example in `LineChart`) +##### In the Line and Bar Charts we show a built in tooltip on the touched spots, then you just need to config how to show it, just fill the `touchTooltipData` in the `LineTouchData`. +##### +```dart +LineChart( + LineChartData( + lineTouchData: LineTouchData( + touchTooltipData: TouchTooltipData ( + getTooltipColor: (touchedSpot) => Colors.blueGrey.withOpacity(0.8), + . + . + . + ) + ) + ) +) +``` +##### But if you want more customization on touch behaviors, implement the `touchCallback` and handle it. +```dart + +LineChart( + LineChartData( + lineTouchData: LineTouchData( + touchCallback: (FlTouchEvent event, LineTouchResponse touchResponse) { + if (event is FlTapUpEvent) { + // handle tap here + } + }, + . + . + . + ) + ) +) +``` diff --git a/repo_files/documentations/handle_transformations.md b/repo_files/documentations/handle_transformations.md new file mode 100644 index 0000000..4a6bf23 --- /dev/null +++ b/repo_files/documentations/handle_transformations.md @@ -0,0 +1,120 @@ +# FL Chart Transformation Guide + +The transformation feature in `fl_chart` allows users to interact with charts through scaling and panning, similar to Flutter's `InteractiveViewer` widget. + +## Basic Usage + +To enable transformations, provide a `FlTransformationConfig` to your chart: + +```dart +LineChart( + LineChartData(...), + transformationConfig: FlTransformationConfig( + scaleAxis: FlScaleAxis.horizontal, + minScale: 1.0, + maxScale: 2.5, + ), +) +``` + +### Configuration Options +See [FlTransformationConfig](https://github.com/imaNNeo/fl_chart/blob/main/lib/src/chart/base/axis_chart/transformation_config.dart) for more information. + +### Chart-Specific Limitations + +- **Bar Chart**: When using `BarChartAlignment.center`, `end`, or `start`, horizontal scaling is not supported +- **Line Chart**: Supports all transformation types +- **Scatter Chart**: Supports all transformation types + +## Advanced Usage: Custom Transformation Controller + +For more control over transformations, you can provide a `TransformationController`. This allows you to: +- Programmatically control the chart's transformation +- Reset to initial state +- Implement custom zoom/pan controls + +### Limitations +At this moment, transformations made with a custom `TransformationController` are not prevented from moving the chart out of the screen. Developers are responsible for ensuring that the chart remains within the visible area and within the transformation limits. + +See the implementation of [AxisChartScaffoldWidget](https://github.com/imaNNeo/fl_chart/blob/main/lib/src/chart/base/axis_chart/axis_chart_scaffold_widget.dart) for how to prevent the chart from moving out of the screen when using a custom `TransformationController`. + +### Example Implementation + +```dart +class ChartWithControls extends StatefulWidget { + @override + State createState() => _ChartWithControlsState(); +} + +class _ChartWithControlsState extends State { + late TransformationController _controller; + + @override + void initState() { + super.initState(); + _controller = TransformationController(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + AspectRatio( + aspectRatio: 1.4, + child: LineChart( + LineChartData(...), + transformationConfig: FlTransformationConfig( + scaleAxis: FlScaleAxis.horizontal, + minScale: 1.0, + maxScale: 25.0, + transformationController: _controller, + ), + ), + ), + Row( + children: [ + IconButton( + icon: Icon(Icons.zoom_in), + onPressed: () { + _controller.value *= Matrix4.diagonal3Values(1.1, 1.1, 1); + }, + ), + IconButton( + icon: Icon(Icons.zoom_out), + onPressed: () { + _controller.value *= Matrix4.diagonal3Values(0.9, 0.9, 1); + }, + ), + IconButton( + icon: Icon(Icons.refresh), + onPressed: () { + _controller.value = Matrix4.identity(); + }, + ), + ], + ), + ], + ); + } +} +``` + +### Common Transformation Operations +See [Matrix4](https://pub.dev/documentation/vector_math/latest/vector_math_64/Matrix4-class.html) for more information on how to manipulate the matrix. + +## Best Practices + +1. Always dispose of the `TransformationController` when you're done with it +2. Set appropriate `minScale` and `maxScale` values to prevent excessive zooming +3. Consider your chart's alignment when choosing a `scaleAxis` +4. Provide visual feedback for transformation limits +5. Consider adding reset functionality for better user experience +6. If you have touch indicators, consider allowing users to disable panning when zoomed in. This way the touch indicators will be shown when users hold and drag to explore the chart's data, instead of panning the chart. + +Remember that transformations are purely visual and don't affect the underlying data. They're particularly useful for exploring detailed data sets or allowing users to focus on specific regions of interest in your charts. diff --git a/repo_files/documentations/index.md b/repo_files/documentations/index.md new file mode 100644 index 0000000..afc7fbd --- /dev/null +++ b/repo_files/documentations/index.md @@ -0,0 +1,25 @@ +## FL Chart Documentation +FlChart allows you to draw your charts in the Flutter, currently we support these type of charts, +click and learn more about them. + +- [LineChart](line_chart.md) + +- [BarChart](bar_chart.md) + +- [PieChart](pie_chart.md) + +- [ScatterChart](scatter_chart.md) + +- [RadarChart](radar_chart.md) + +- [CandlestickChart](candlestick_chart.md) + +----------- + +- [Migration Guides](migration_guides/INDEX.md) + +- [Handle Touches](handle_touches.md) + +- [Handle Animations](handle_animations.md) + +- [Handle Transformations](handle_transformations.md) diff --git a/repo_files/documentations/line_chart.md b/repo_files/documentations/line_chart.md new file mode 100644 index 0000000..d8f30a7 --- /dev/null +++ b/repo_files/documentations/line_chart.md @@ -0,0 +1,227 @@ + + +### How to use +```dart +LineChart( + LineChartData( + // read about it in the LineChartData section + ), + swapAnimationDuration: Duration(milliseconds: 150), // Optional + swapAnimationCurve: Curves.linear, // Optional +); +``` + +### Implicit Animations +When you change the chart's state, it animates to the new state internally (using [implicit animations](https://flutter.dev/docs/development/ui/animations/implicit-animations)). You can control the animation [duration](https://api.flutter.dev/flutter/dart-core/Duration-class.html) and [curve](https://api.flutter.dev/flutter/animation/Curves-class.html) using optional `swapAnimationDuration` and `swapAnimationCurve` properties, respectively. + +### LineChartData +|PropName |Description |default value| +|:---------------|:---------------|:-------| +|lineBarsData| list of [LineChartBarData ](#LineChartBarData ) to show the chart's lines, they stack and can be drawn on top of each other|[]| +|betweenBarsData| list of [BetweenBarsData](#BetweenBarsData ) to fill the area between 2 chart lines|[]| +|titlesData| check the [FlTitlesData](base_chart.md#FlTitlesData)| FlTitlesData()| +|extraLinesData| [ExtraLinesData](base_chart.md#ExtraLinesData) object to hold drawing details of extra horizontal and vertical lines. Check [ExtraLinesData](base_chart.md#ExtraLinesData)|ExtraLinesData()| +|lineTouchData| [LineTouchData](#linetouchdata-read-about-touch-handling) holds the touch interactivity details| LineTouchData()| +|rangeAnnotations| show range annotations behind the chart, check [RangeAnnotations](base_chart.md#RangeAnnotations) | RangeAnnotations()| +|showingTooltipIndicators| show the tooltip based on provided list of [LineBarSpot](#LineBarSpot), The point is that you need to disable touches to show these tooltips manually| [] | +|gridData| check the [FlGridData](base_chart.md#FlGridData)|FlGridData()| +|borderData| check the [FlBorderData](base_chart.md#FlBorderData)|FlBorderData()| +|minX| gets minimum x of x axis, if null, value will read from the input lineBars (But it is more performant if you provide them)|null| +|maxX| gets maximum x of x axis, if null, value will read from the input lineBars (But it is more performant if you provide them)| null| +|baselineX| defines the baseline of x-axis | 0| +|minY| gets minimum y of y axis, if null, value will read from the input lineBars (But it is more performant if you provide them)| null| +|maxY| gets maximum y of y axis, if null, value will read from the input lineBars (But it is more performant if you provide them)| null| +|baselineY| defines the baseline of y-axis | 0| +|clipData| clip the chart to the border (prevent drawing outside the border) | FlClipData.none()| +|backgroundColor| a background color which is drawn behind th chart| null | +|rotationQuarterTurns|Rotates the chart 90 degrees (clockwise) in every quarter turns. This feature works like the [RotatedBox](https://api.flutter.dev/flutter/widgets/RotatedBox-class.html) widget|0| + + + +### LineChartBarData +|PropName |Description |default value| +|:---------------|:---------------|:-------| +|show| determines to show or hide the bar line|true| +|spots| list of [FlSpot](base_chart.md#FlSpot)'s x and y coordinates that the line go through it| [] +|color|color of the line|[Colors.redAccent]| +|gradient| You can use any [Gradient](https://api.flutter.dev/flutter/dart-ui/Gradient-class.html) here. such as [LinearGradient](https://api.flutter.dev/flutter/painting/LinearGradient-class.html) or [RadialGradient](https://api.flutter.dev/flutter/painting/RadialGradient-class.html)|null| +|gradientArea| determines the area where the gradient is applied |null| +|barWidth| gets the stroke width of the line bar|2.0| +|isCurved| curves the corners of the line on the spot's positions| false| +|curveSmoothness| smoothness radius of the curve corners (works when isCurved is true) | 0.35| +|preventCurveOverShooting|prevent overshooting when draw curve line on linear sequence spots, check this [issue](https://github.com/imaNNeo/fl_chart/issues/25)| false| +|preventCurveOvershootingThreshold|threshold for applying prevent overshooting algorithm | 10.0| +|isStrokeCapRound| determines whether start and end of the bar line is Qubic or Round | false| +|isStrokeJoinRound| determines whether stroke joins have a round shape or a sharp edge | false| +|belowBarData| check the [BarAreaData](#BarAreaData) |BarAreaData| +|aboveBarData| check the [BarAreaData](#BarAreaData) |BarAreaData| +|dotData| check the [FlDotData](#FlDotData) | FlDotData()| +|showingIndicators| show indicators based on provided indexes | []| +|dashArray|A circular array of dash offsets and lengths. For example, the array `[5, 10]` would result in dashes 5 pixels long followed by blank spaces 10 pixels long. The array `[5, 10, 5]` would result in a 5 pixel dash, a 10 pixel gap, a 5 pixel dash, a 5 pixel gap, a 10 pixel dash, etc.|null| +|shadow|It drops a shadow behind your bar, see [Shadow](https://api.flutter.dev/flutter/dart-ui/Shadow-class.html).|Shadow()| +|isStepLineChart|If sets true, it draws the chart in Step Line Chart style, using `lineChartStepData`.|false| +|lineChartStepData|Holds data for representing a Step Line Chart, and works only if [isStepChart] is true.|[LineChartStepData](#LineChartStepData)()| +|errorIndicatorData|Holds data for representing an error indicator (you see the error indicators if you provide the `xError` or `yError` in the [FlSpot](base_chart.md#FlSpot)).|[ErrorIndicatorData()](base_chart.md#FlErrorIndicatorData)| + +### LineChartStepData +|PropName|Description|default value| +|:-------|:----------|:------------| +|stepDirection|Determines the direction of each step, could be between 0.0 (forward), and 1.0 (backward)|LineChartStepData.stepDirectionMiddle| + +### BetweenBarsData +|PropName|Description|default value| +|:-------|:----------|:------------| +|fromIndex|index of the first LineChartBarData inside LineChartData (zero-based index)|required| +|toIndex|index of the second LineChartBarData inside LineChartData (zero-based index)|required| +|color|color of the area|[Colors.blueGrey]| +|gradient| You can use any [Gradient](https://api.flutter.dev/flutter/dart-ui/Gradient-class.html) here. such as [LinearGradient](https://api.flutter.dev/flutter/painting/LinearGradient-class.html) or [RadialGradient](https://api.flutter.dev/flutter/painting/RadialGradient-class.html)|null| + +### BarAreaData +|PropName|Description|default value| +|:-------|:----------|:------------| +|show|determines to show or hide the below, or above bar area|false| +|color|color of the below, or above bar area|[Colors.blueGrey]| +|gradient| You can use any [Gradient](https://api.flutter.dev/flutter/dart-ui/Gradient-class.html) here. such as [LinearGradient](https://api.flutter.dev/flutter/painting/LinearGradient-class.html) or [RadialGradient](https://api.flutter.dev/flutter/painting/RadialGradient-class.html)|null| +|spotsLine| draw a line from each spot the the bottom, or top of the chart|[BarAreaSpotsLine](#BarAreaSpotsLine)()| +|cutOffY| cut the drawing below or above area to this y value (set `applyCutOffY` true if you want to set it)|null| +|applyCutOffY| determines should or shouldn't apply cutOffY (`scutOffY` should be provided)|false| + + +### BarAreaSpotsLine +|PropName|Description|default value| +|:-------|:----------|:------------| +|show|determines show or hide the below, or above spots line|true| +|flLineStyle|a [FlLine](base_chart.md#FlLine) object that determines style of the line|[Colors.blueGrey]| +|checkToShowSpotLine|a function to determine whether to show or hide the below or above line on the given spot|showAllSpotsBelowLine| +|applyCutOffY|Determines to inherit the cutOff properties from its parent [BarAreaData](#BarAreaData)|true| + +### FlDotData +|PropName|Description|default value| +|:-------|:----------|:------------| +|show|determines to show or hide the dots|true| +|checkToShowDot|a function to determine whether to show or hide the dot on the given spot|showAllDots| +|getDotPainter|a function to determine how the dot is drawn on the given spot|_defaultGetDotPainter| + +### LineTouchData ([read about touch handling](handle_touches.md)) +|PropName|Description|default value| +|:-------|:----------|:------------| +|enabled|determines to enable or disable touch behaviors|true| +|mouseCursorResolver|you can change the mouse cursor based on the provided [FlTouchEvent](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#fltouchevent) and [LineTouchResponse](#LineTouchResponse)|MouseCursor.defer| +|touchTooltipData|a [LineTouchTooltipData](#LineTouchTooltipData), that determines how show the tooltip on top of touched spots (appearance of the showing tooltip bubble)|LineTouchTooltipData| +|getTouchedSpotIndicator| a callback that retrieves list of [TouchedSpotIndicatorData](#TouchedSpotIndicatorData) by the given list of [LineBarSpot](#LineBarSpot) for showing the indicators on touched spots|defaultTouchedIndicators| +|touchSpotThreshold|the threshold of the touch accuracy|10| +|distanceCalculator| a function to calculate the distance between a spot and a touch event| _xDistance| +|handleBuiltInTouches| set this true if you want the built in touch handling (show a tooltip bubble and an indicator on touched spots) | true| +|getTouchLineStart| controls where the line starts, default is bottom of the chart| defaultGetTouchLineStart| +|getTouchLineEnd| controls where the line ends, default is the touch point| defaultGetTouchLineEnd| +|touchCallback| listen to this callback to retrieve touch/pointer events and responses, it gives you a [FlTouchEvent](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#fltouchevent) and [LineTouchResponse](#LineTouchResponse)| null| +|longPressDuration| allows to customize the duration of the longPress gesture. If null, the duration of the longPressGesture is [kLongPressTimeout](https://api.flutter.dev/flutter/gestures/kLongPressTimeout-constant.html)| null| + + +### LineTouchTooltipData + |PropName|Description|default value| + |:-------|:----------|:------------| + |tooltipBorder|border of the tooltip bubble|BorderSide.none| + |tooltipBorderRadius|background corner radius of the tooltip bubble|BorderRadius.circular(4)| + |tooltipPadding|padding of the tooltip|EdgeInsets.symmetric(horizontal: 16, vertical: 8)| + |tooltipMargin|margin between the tooltip and the touched spot|16| + |tooltipHorizontalAlignment|horizontal alginment of tooltip relative to the spot|FLHorizontalAlignment.center| + |tooltipHorizontalOffset|horizontal offset of tooltip|0| + |maxContentWidth|maximum width of the tooltip (if a text row is wider than this, then the text breaks to a new line|120| + |getTooltipItems|a callback that retrieve list of [LineTooltipItem](#LineTooltipItem) by the given list of [LineBarSpot](#LineBarSpot) |defaultLineTooltipItem| + |fitInsideHorizontally| forces tooltip to horizontally shift inside the chart's bounding box| false| + |fitInsideVertically| forces tooltip to vertically shift inside the chart's bounding box| false| + |showOnTopOfTheChartBoxArea| forces the tooltip container to top of the line| false| + |getTooltipColor|a callback that retrieves the Color for each touched spots separately from the given [LineBarSpot](#LineBarSpot) to set the background color of the tooltip bubble|Colors.blueGrey.darken(15)| + +### LineTooltipItem +|PropName|Description|default value| +|:-------|:----------|:------------| +|text|text string of each row in the tooltip bubble|null| +|textStyle|[TextStyle](https://api.flutter.dev/flutter/dart-ui/TextStyle-class.html) of the showing text row|null| +|textAlign|[TextAlign](https://api.flutter.dev/flutter/dart-ui/TextAlign-class.html) of the showing text row|TextAlign.center| +|textDirection|[TextDirection](https://api.flutter.dev/flutter/dart-ui/TextDirection-class.html) of the showing text row|TextDirection.ltr| +|children|[List](https://api.flutter.dev/flutter/painting/InlineSpan-class.html) pass additional InlineSpan children for a more advance tooltip|null| + +### TouchedSpotIndicatorData +|PropName|Description|default value| +|:-------|:----------|:------------| +|indicatorBelowLine|a [FlLine](base_chart.md#FlLine) to show the below line indicator on the touched spot|null| +|touchedSpotDotData|a [FlDotData](#FlDotData) to show a dot indicator on the touched spot|null| + + +### LineBarSpot +|PropName|Description|default value| +|:-------|:----------|:------------| +|bar|the [LineChartBarData](#LineChartBarData) that contains a spot|null| +|barIndex|index of the target [LineChartBarData](#LineChartBarData) inside [LineChartData](#LineChartData)|null| +|spotIndex|index of the target [FlSpot](base_chart.md#FlSpot) inside [LineChartBarData](#LineChartBarData)|null| + + +### TouchLineBarSpot +|PropName|Description|default value| +|:-------|:----------|:------------| +|bar|the [LineChartBarData](#LineChartBarData) that contains a spot|null| +|barIndex|index of the target [LineChartBarData](#LineChartBarData) inside [LineChartData](#LineChartData)|null| +|spotIndex|index of the target [FlSpot](base_chart.md#FlSpot) inside [LineChartBarData](#LineChartBarData)|null| +|distance|distance to the touch event|null| + + +### LineTouchResponse +|PropName|Description|default value| +|:-------|:----------|:------------| +|touchLocation|the location of the touch event in the device pixels coordinates|required| +|touchChartCoordinate|the location of the touch event in the chart coordinates|required| +|lineBarSpots|a list of [TouchLineBarSpot](#TouchLineBarSpot)|null| + +### ShowingTooltipIndicators +|PropName|Description|default value| +|:-------|:----------|:------------| +|showingSpots|Determines the spots that each tooltip should be shown.|null| + + +### some samples +---- +##### Sample 1 ([Source Code](/example/lib/presentation/samples/line/line_chart_sample1.dart)) + + + + + +##### Sample 2 ([Source Code](/example/lib/presentation/samples/line/line_chart_sample2.dart)) + + + + + +##### Sample 3 ([Source Code](/example/lib/presentation/samples/line/line_chart_sample3.dart)) + + + +##### Sample 4 ([Source Code](/example/lib/presentation/samples/line/line_chart_sample4.dart)) + + + +##### Sample 5 ([Source Code](/example/lib/presentation/samples/line/line_chart_sample5.dart)) + + + +##### Sample 6 - Reversed ([Source Code](/example/lib/presentation/samples/line/line_chart_sample6.dart)) + + + +##### Sample 7 ([Source Code](/example/lib/presentation/samples/line/line_chart_sample7.dart)) + + + +##### Sample 8 ([Source Code](/example/lib/presentation/samples/line/line_chart_sample8.dart)) + + +##### Sample 9 ([Source Code](/example/lib/presentation/samples/line/line_chart_sample9.dart)) + + +##### Sample 10 ([Source Code](/example/lib/presentation/samples/line/line_chart_sample10.dart)) + + +##### Sample 11 ([Source Code](/example/lib/presentation/samples/line/line_chart_sample11.dart)) +https://user-images.githubusercontent.com/7009300/152555425-3b53ac8c-257f-49b0-8d75-1a878c03ccaa.mp4 diff --git a/repo_files/documentations/migration_guides/0.50.0/MIGRATION_00_50_00.md b/repo_files/documentations/migration_guides/0.50.0/MIGRATION_00_50_00.md new file mode 100644 index 0000000..515909a --- /dev/null +++ b/repo_files/documentations/migration_guides/0.50.0/MIGRATION_00_50_00.md @@ -0,0 +1,184 @@ +# Migrate to 0.50.0 + +## Widgets as titles (instead of boring strings) [#183](https://github.com/imaNNeo/fl_chart/issues/183) +We did a lot of hard-work to bring widgets to our titles around the axis-based charts. +It means that you can now put a widget as a title instead of a string. +Look at the below samples: + +**LineChartSample 8** ([Source Code](https://github.com/imaNNeo/fl_chart/blob/main/example/lib/presentation/samples/line/line_chart_sample8.dart)) + + + +**BarChartSample 7** ([Source Code](https://github.com/imaNNeo/fl_chart/blob/main/example/lib/presentation/samples/bar/bar_chart_sample7.dart)) + + + +**Breaking:** +Previously in [FlTitlesData](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#FlTitlesData), there were four [SideTitles](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#sidetitles). Now we have four [AxisTitle](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#axistitle) instead and [SideTitles](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#sidetitles) can be placed inside [AxisTitle](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#AxisTitle). +In fact, we removed `AxisTitlesData` class (which used to hold four `AxisTitle`). Now you can put them in [FlTitlesData](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#fltitlesdata). + + bool? showTitle, + String? titleText, + double? reservedSize, + TextStyle? textStyle, + TextDirection? textDirection, + TextAlign? textAlign, + +Look at the below sample. + +Previously: +```dart +AxisBasedChartData( // Line, Bar and Scatter + axisTitleData: FlAxisTitleData( + bottomTitle: AxisTitle( + showTitle: true, + margin: 0, + titleText: '2019', + reservedSize: 80, + textStyle: TextStyle(color: Colors.green), + textAlign: TextAlign.right, + textDirection: TextDirection.rtl, + ), + ), + titlesData: FlTitlesData( + bottomTitles: SideTitles( + showTitles: true, + getTitles: (value) { + return 'My Text' + }, + reservedSize: 14, + interval: 1, + margin: 8, + getTextStyles: (context, value) => TextStyle(color: Colors.red), + textDirection: TextDirection.rtl, + textAlign: TextAlign.center, + ), + ) +) +``` + +Now in `0.50.0`: +```dart +AxisBasedChartData( // Line, Bar and Scatter + titlesData: FlTitlesData( + show: true, + bottomTitles: AxisTitles( + axisNameWidget: Text( // You can use any widget here + '2019', + style: TextStyle(color: Colors.green), + textAlign: TextAlign.right, + textDirection: TextDirection.rtl, + ), + axisNameSize: 80, + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, titleMeta) { + return Padding( // You can use any widget here + padding: EdgeInsets.only(top: 8.0), + child: Text( + 'My Text', + style: TextStyle(color: Colors.red), + textDirection: TextDirection.rtl, + textAlign: TextAlign.center, + ), + ); + }, + reservedSize: 14, + interval: 1, + ), + ), + ) +) +``` + +* Instead of setting `rotateAngle` property, now you can wrap your widget with [RotatedBox](https://api.flutter.dev/flutter/widgets/RotatedBox-class.html) to rotate it. +* Instead of setting `checkToShowTitle` property, you can pass an empty [SizedBox](https://api.flutter.dev/flutter/widgets/SizedBox-class.html) wherever you want to skip drawing a title. + +----- + +## Gradient and solid color #948 +We made some changes on our approach for handling `solid` color and `gradient` colors. + +Previously, we had these properties to handle gradient or solid color: +```dart +List colors, +List stops, +Offset gradientFrom, +Offset gradientTo, +``` +It was supposed to work on both solid color and gradient color. +If you pass just one color in the `colors` property, it was a solid color. +On the other hand, if you provide more than one color, it was a linear gradient. + +Now we are using a new approach with the properties below: +```dart +Color? color, +Gradient? gradient, +``` + +* If you fill `color` property, it will be a solid color. +* If you fill `gradient` property, it would be any [Gradient](https://api.flutter.dev/flutter/dart-ui/Gradient-class.html) you want. Such as [LinearGradient](https://api.flutter.dev/flutter/painting/LinearGradient-class.html) and [RadialGradient](https://api.flutter.dev/flutter/painting/RadialGradient-class.html). +* You need to fill one of them. + +These are the affected classes: +* [BarChartRodData](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/bar_chart.md#barchartroddata) +* [BackgroundBarChartRodData](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/bar_chart.md#backgroundbarchartroddata) +* [BarAreaData](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/line_chart.md#BarAreaData) +* [BetweenBarsData](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/line_chart.md#betweenbarsdata) +* [LineChartBarData](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/line_chart.md#linechartbardata) + +Check the below sample: + +Previously: +```dart +LineChartBarData( + colors: [Colors.red] +) +``` + +Now in `0.50.0`: +```dart +LineChartBarData( + color: Colors.red +) +``` +----- +Previously: +```dart +LineChartBarData( + colors: [Colors.green, Colors.blue], +) +``` + +Now in `0.50.0`: +```dart +LineChartBarData( + gradient: LinearGradient( + colors: [Colors.green, Colors.blue], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ) +) +``` +----- +Previously: +```dart +LineChartBarData( + colors: [Colors.green, Colors.blue], + colorStops: [0.1, 0.10], + gradientFrom: Offset(0, 0), // topLeft + gradientTo: Offset(1, 1), // bottomRight +) +``` + +Now in `0.50.0`: +```dart +LineChartBarData( + gradient: LinearGradient( + colors: [Colors.green, Colors.blue], + stops: [0.1, 0.10], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ) +) +``` \ No newline at end of file diff --git a/repo_files/documentations/migration_guides/0.55.0/MIGRATION_00_55_00.md b/repo_files/documentations/migration_guides/0.55.0/MIGRATION_00_55_00.md new file mode 100644 index 0000000..670cca2 --- /dev/null +++ b/repo_files/documentations/migration_guides/0.55.0/MIGRATION_00_55_00.md @@ -0,0 +1,57 @@ +# Migrate to new version + +## The ability to rotate the RadarChart titles [#1057](https://github.com/imaNNeo/fl_chart/issues/1057) + +We added the ability to customize the rotation angles of the RadarChart titles. +To do that we add to break one thing and added a new type. + + + +**Breaking:** + +We only changed [RadarChartData.getTitle](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/radar_chart.md#RadarChartData). +Its type [GetTitleByIndexFunction] changed from `string Function(int index)` to `RadarChartTitle Function(int index, double angle)`. + +To reuse the example from the code: + +Previously: +```dart +getTitle: (index) { + switch (index) { + case 0: + return 'Mobile or Tablet'; + case 2: + return 'Desktop'; + case 1: + return 'TV'; + default: + return ''; + } +} +``` + +Now in new version: + +```dart +getTitle: (index, angle) { + switch (index) { + case 0: + return RadarChartTitle(text: 'Mobile or Tablet', angle: angle); + case 2: + return RadarChartTitle(text: 'Desktop', angle: angle); + case 1: + return RadarChartTitle(text: 'TV', angle: angle); + default: + return const RadarChartTitle(text: ''); + } +} +``` + +If you take the provided `angle` and forward it to the [RadarChartTitle] it will behave like in previous versions. +But you can now render all the titles horizontally by avoiding the [RadarChartTitle.angle] prop (`0` by default). + +Apply a relative angle, for example: `RadarChartTitle(text: 'Desktop', angle: angle + 90);` + + +or an absolute angle, for example: `RadarChartTitle(text: 'Desktop', angle: 90);` + \ No newline at end of file diff --git a/repo_files/documentations/migration_guides/0.55.0/attachments/radar_chart_sample_1.gif b/repo_files/documentations/migration_guides/0.55.0/attachments/radar_chart_sample_1.gif new file mode 100644 index 0000000..98e555b Binary files /dev/null and b/repo_files/documentations/migration_guides/0.55.0/attachments/radar_chart_sample_1.gif differ diff --git a/repo_files/documentations/migration_guides/0.55.0/attachments/radar_chart_sample_2.png b/repo_files/documentations/migration_guides/0.55.0/attachments/radar_chart_sample_2.png new file mode 100644 index 0000000..8277616 Binary files /dev/null and b/repo_files/documentations/migration_guides/0.55.0/attachments/radar_chart_sample_2.png differ diff --git a/repo_files/documentations/migration_guides/0.55.0/attachments/radar_chart_sample_3.png b/repo_files/documentations/migration_guides/0.55.0/attachments/radar_chart_sample_3.png new file mode 100644 index 0000000..da8df18 Binary files /dev/null and b/repo_files/documentations/migration_guides/0.55.0/attachments/radar_chart_sample_3.png differ diff --git a/repo_files/documentations/migration_guides/0.67.0/MIGRATION_00_67_00.md b/repo_files/documentations/migration_guides/0.67.0/MIGRATION_00_67_00.md new file mode 100644 index 0000000..9ece377 --- /dev/null +++ b/repo_files/documentations/migration_guides/0.67.0/MIGRATION_00_67_00.md @@ -0,0 +1,85 @@ +# Migrate to new version + +## Replaced tooltipBgColor + +**Breaking: [#1595](https://github.com/imaNNeo/fl_chart/pull/1595)** + +We added the ability to customize the tooltip background color for each point. + +The property `Color tooltipBgColor` from Bar, Line and Scatter Charts is replaced with a callback `Color Function(spot) getTooltipColor` + +#### BarChartData + +Previously: +```dart +BarChartData( + barTouchData: BarTouchData( + touchTooltipData: BarTouchTooltipData( + tooltipBgColor: Colors.blueGrey, + ) + ) +) +``` + +Now in new version: + +```dart +BarChartData( + barTouchData: BarTouchData( + touchTooltipData: BarTouchTooltipData( + getTooltipColor: (BarChartGroupData group) => Colors.blueGrey, + ) + ) +) +``` + +#### LineChartData + +Previously: +```dart +LineChartData( + lineTouchData: LineTouchData( + touchTooltipData: LineTouchTooltipData( + tooltipBgColor: Colors.blueGrey, + ) + ) +) +``` + +Now in new version: + +```dart +LineChartData( + lineTouchData: LineTouchData( + touchTooltipData: LineTouchTooltipData( + getTooltipColor: (LineBarSpot touchedSpot) => Colors.blueGrey, + ) + ) +) +``` + +#### ScatterChartData + +Previously: +```dart +ScatterChartData( + scatterTouchData: ScatterTouchData( + touchTooltipData: ScatterTouchTooltipData( + tooltipBgColor: Colors.blueGrey, + ) + ) +) +``` + +Now in new version: + +```dart +ScatterChartData( + scatterTouchData: ScatterTouchData( + touchTooltipData: ScatterTouchTooltipData( + getTooltipColor: (ScatterSpot touchedBarSpot) => Colors.blueGrey, + ) + ) +) +``` + diff --git a/repo_files/documentations/migration_guides/0.70.0/MIGRATION_00_70_00.md b/repo_files/documentations/migration_guides/0.70.0/MIGRATION_00_70_00.md new file mode 100644 index 0000000..a82c8ac --- /dev/null +++ b/repo_files/documentations/migration_guides/0.70.0/MIGRATION_00_70_00.md @@ -0,0 +1,7 @@ +# Migrate to version 0.70.0 + +## Fixed the equatable functionality in our PieChartSectionData +Please check any code that compares `PieChartSectionData` classes or other objects containing `PieChartSectionData` and make sure it is not affected by this change. + +## `BarChart` is not const anymore +We added an assert to check if transformations are allowed depending on the `BarChartData.alignment` property. If you are using `BarChart` as a const, you need to remove the const keyword from the `BarChart` constructor. The compiler will show you an error if you try to use `BarChart` as a const. diff --git a/repo_files/documentations/migration_guides/INDEX.md b/repo_files/documentations/migration_guides/INDEX.md new file mode 100644 index 0000000..026e7ee --- /dev/null +++ b/repo_files/documentations/migration_guides/INDEX.md @@ -0,0 +1,7 @@ +Here are fl_chart's migration guides: + +#### [Migrate to 0.50.0](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/migration_guides/0.50.0/MIGRATION_00_50_00.md) + +#### [Migrate to 0.55.0](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/migration_guides/0.55.0/MIGRATION_00_55_00.md) + +#### [Migrate to 0.67.0](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/migration_guides/0.67.0/MIGRATION_00_67_00.md) \ No newline at end of file diff --git a/repo_files/documentations/pie_chart.md b/repo_files/documentations/pie_chart.md new file mode 100644 index 0000000..78522d4 --- /dev/null +++ b/repo_files/documentations/pie_chart.md @@ -0,0 +1,82 @@ + + +### How to use +```dart +PieChart( + PieChartData( + // read about it in the PieChartData section + ), + duration: Duration(milliseconds: 150), // Optional + curve: Curves.linear, // Optional +); +``` + +**If you have a padding widget around the PieChart, make sure to set `PieChartData.centerSpaceRadius` to `double.infinity`** + + +### Implicit Animations +When you change the chart's state, it animates to the new state internally (using [implicit animations](https://flutter.dev/docs/development/ui/animations/implicit-animations)). You can control the animation [duration](https://api.flutter.dev/flutter/dart-core/Duration-class.html) and [curve](https://api.flutter.dev/flutter/animation/Curves-class.html) using optional `duration` and `curve` properties, respectively. + +### PieChartData +|PropName |Description |default value| +|:---------------|:---------------|:-------| +|sections| list of [PieChartSectionData ](#PieChartSectionData) that is shown on the pie chart|[]| +|centerSpaceRadius| free space in the middle of the PieChart, set `double.infinity` if you want it to be calculated according to the view size| double.nan| +|centerSpaceColor| colors the free space in the middle of the PieChart|Colors.transparent| +|sectionsSpace| space between the sections (margin of them). It does not work on html-rendere, read more about it [here](https://github.com/imaNNeo/fl_chart/issues/955) |2| +|startDegreeOffset| degree offset of the sections around the pie chart, should be between 0 and 360|0| +|pieTouchData| [PieTouchData](#pietouchdata-read-about-touch-handling) holds the touch interactivity details| PieTouchData()| +|borderData| shows a border around the chart, check the [FlBorderData](base_chart.md#FlBorderData)|FlBorderData()| +|titleSunbeamLayout| whether to rotate the titles on each section of the chart|false| + + +### PieChartSectionData +|PropName |Description |default value| +|:---------------|:---------------|:-------| +|value| value is the weight of each section, for example if all values is 25, and we have 4 section, then the sum is 100 and each section takes 1/4 of the whole circle (360/4) degree|10| +|color| colors the section| Colors.red +|gradient| You can use any [Gradient](https://api.flutter.dev/flutter/dart-ui/Gradient-class.html) here. such as [LinearGradient](https://api.flutter.dev/flutter/painting/LinearGradient-class.html) or [RadialGradient](https://api.flutter.dev/flutter/painting/RadialGradient-class.html) (you have to provide either `color` or `gradient`)|null| +|radius| the width radius of each section|40| +|showTitle| determines to show or hide the titles on each section|true| +|titleStyle| TextStyle of the titles| TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold)| +|title| title of the section| value| +|borderSide| Defines border stroke around the section | BorderSide(width: 0)| +|badgeWidget| badge component of the section| null| +|titlePositionPercentageOffset|the place of the title in the section, this field should be between 0 and 1|0.5| +|badgePositionPercentageOffset|the place of the badge component in the section, this field should be between 0 and 1|0.5| + + +### PieTouchData ([read about touch handling](handle_touches.md)) +|PropName|Description|default value| +|:-------|:----------|:------------| +|enabled|determines to enable or disable touch behaviors|true| +|mouseCursorResolver|you can change the mouse cursor based on the provided [FlTouchEvent](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#fltouchevent) and [PieTouchResponse](#PieTouchResponse)|MouseCursor.defer| +|touchCallback| listen to this callback to retrieve touch/pointer events and responses, it gives you a [FlTouchEvent](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#fltouchevent) and [PieTouchResponse](#PieTouchResponse)| null| +|longPressDuration| allows to customize the duration of the longPress gesture. If null, the duration of the longPressGesture is [kLongPressTimeout](https://api.flutter.dev/flutter/gestures/kLongPressTimeout-constant.html)| null| + +### PieTouchResponse +|PropName|Description|default value| +|:-------|:----------|:------------| +|touchLocation|the location of the touch event in the device pixels coordinates|required| +|touchedSection|Instance of [PieTouchedSection](#PieTouchedSection) which holds data about the touched section|null| + +### PieTouchedSection +|PropName|Description|default value| +|:-------|:----------|:------------| +|touchedSection|the [PieChartSectionData](#PieChartSectionData) that user touched| null | +|touchedSectionIndex| index of the touched section | null| +|touchAngle|the angle of the touch|null| +|touchRadius| the radius of the touch|null| + +### some samples +---- +##### Sample 1 ([Source Code](/example/lib/presentation/samples/pie/pie_chart_sample1.dart)) + + + +##### Sample 2 ([Source Code](/example/lib/presentation/samples/pie/pie_chart_sample2.dart)) + + + +##### Sample 3 ([Source Code](/example/lib/presentation/samples/pie/pie_chart_sample3.dart)) + diff --git a/repo_files/documentations/radar_chart.md b/repo_files/documentations/radar_chart.md new file mode 100644 index 0000000..3ca7179 --- /dev/null +++ b/repo_files/documentations/radar_chart.md @@ -0,0 +1,86 @@ +# RadarChart + + +### How to use +```dart +RadarChart( + RadarChartData( + // read about it in the RadarChartData section + ), + swapAnimationDuration: Duration(milliseconds: 150), // Optional + swapAnimationCurve: Curves.linear, // Optional +); +``` + +### Implicit Animations +When you change the chart's state, it animates to the new state internally (using [implicit animations](https://flutter.dev/docs/development/ui/animations/implicit-animations)). You can control the animation [duration](https://api.flutter.dev/flutter/dart-core/Duration-class.html) and [curve](https://api.flutter.dev/flutter/animation/Curves-class.html) using optional `swapAnimationDuration` and `swapAnimationCurve` properties, respectively. + + +### RadarChartData +|PropName |Description |default value| +|:---------------|:---------------|:-------| +|dataSets| list of [RadarDataSet ](#RadarDataSet) that is shown on the radar chart|[]| +|radarBackgroundColor| This property fills the background of the radar with the specified color.| Colors.transparent| +|radarShape| the shape of the border and background |RadarShape.circle| +|radarBorderData| shows a border for radar chart|BorderSide(color: Colors.black, width: 2)| +|getTitle| This function helps the radar chart to draw titles outside the chart. The default angle provided when called is making the title tangent to the radar chart. |null| +|titleTextStyle|TextStyle of the titles|TextStyle(color: Colors.black, fontSize: 12)| +|titlePositionPercentageOffset|this field is the place of showing title on the RadarChart. The higher the value of this field, the more titles move away from the chart. this field should be between 0 and 1.|0.2| +|tickCount|Defines the number of ticks that should be paint in RadarChart|1| +|ticksTextStyle|TextStyle of the tick titles|TextStyle(fontSize: 10, color: Colors.black)| +|tickBorderData|Style of the tick borders|BorderSide(color: Colors.black, width: 2)| +|gridBorderData|Style of the grid borders|BorderSide(color: Colors.black, width: 2)| +|radarTouchData|[RadarTouchData](#radartouchdata-read-about-touch-handling) handles the touch behaviors and responses.|RadarTouchData()| +|isMinValueAtCenter|If true, the minimum value of the [RadarChart] will be at the center of the chart.|false| + +### RadarDataSet +|PropName |Description |default value| +|:---------------|:---------------|:-------| +|dataEntries|Each RadarDataSet contains list of [RadarEntries ](#RadarEntry) that is shown in RadarChart.|[]| +|fillColor|fills the DataSet with the specified color.|Colors.black12| +|fillGradient|fills the DataSet with the specified gradient colors.| null | +|borderColor|Paint the DataSet border with the specified color.|Colors.blueAccent| +|borderWidth|defines the width of [RadarDataSet](#RadarDataSet) border.|2.0| +|entryRadius|defines the radius of each [RadarEntries ](#RadarEntry).|5.0| + +### RadarEntry +|PropName |Description |default value| +|:---------------|:---------------|:-------| +|value| RadarChart uses this field to render every point in chart.| null | + +### RadarTouchData ([read about touch handling](handle_touches.md)) +|PropName|Description|default value| +|:-------|:----------|:------------| +|enabled|determines to enable or disable touch behaviors|true| +|mouseCursorResolver|you can change the mouse cursor based on the provided [FlTouchEvent](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#fltouchevent) and [RadarTouchResponse](#RadarTouchResponse)|MouseCursor.defer| +|touchCallback| listen to this callback to retrieve touch/pointer events and responses, it gives you a [FlTouchEvent](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#fltouchevent) and [RadarTouchResponse](#RadarTouchResponse)| null| +|longPressDuration| allows to customize the duration of the longPress gesture. If null, the duration of the longPressGesture is [kLongPressTimeout](https://api.flutter.dev/flutter/gestures/kLongPressTimeout-constant.html)| null| +|touchSpotThreshold|the threshold of the touch accuracy. we find the nearest spots on touched position based on this field.|10| + + +### RadarTouchResponse +|PropName|Description|default value| +|:-------|:----------|:------------| +|touchLocation|the location of the touch event in the device pixels coordinates|required| +|touchedSpot|the [RadarTouchedSpot](#RadarTouchedSpot) that user touched| null | + +### RadarTouchedSpot +|PropName|Description|default value| +|:-------|:----------|:------------| +|touchedDataSet|the [RadarDataSet](#RadarDataSet) that user touched| null | +|touchedDataSetIndex| index of the [RadarDataSet](#RadarDataSet) that user touched| null | +|touchedRadarEntry|the [RadarEntry](#RadarEntry) that user touched| null | +|touchedRadarEntryIndex| index of the [RadarEntry](#RadarEntry) that user touched| null | + +### RadarChartTitle +|PropName|Description|default value| +|:-------|:----------|:------------| +|text|the text of the title|required| +|children| A list of [InlineSpan](https://api.flutter.dev/flutter/painting/InlineSpan-class.html) that you can provide to have different texts with different styels. Just like how [TextSpan](https://api.flutter.dev/flutter/painting/TextSpan-class.html) works|null| +|angle|the angle used to rotate the title (in degree)|0| +|positionPercentageOffset|this field is the place of showing title. The higher the value of this field, the more titles move away from the chart. this field should be between 0 and 1|null| + +### some samples +---- +##### Sample 1 ([Source Code](/example/lib/presentation/samples/radar/radar_chart_sample1.dart)) + diff --git a/repo_files/documentations/scatter_chart.md b/repo_files/documentations/scatter_chart.md new file mode 100644 index 0000000..33f311e --- /dev/null +++ b/repo_files/documentations/scatter_chart.md @@ -0,0 +1,105 @@ +# ScatterChart + + + +### How to use +```dart +ScatterChart( + ScatterChartData( + // read about it in the ScatterChartData section + ), + duration: Duration(milliseconds: 150), // Optional + curve: Curves.linear, // Optional +); +``` + +### Implicit Animations +When you change the chart's state, it animates to the new state internally (using [implicit animations](https://flutter.dev/docs/development/ui/animations/implicit-animations)). You can control the animation [duration](https://api.flutter.dev/flutter/dart-core/Duration-class.html) and [curve](https://api.flutter.dev/flutter/animation/Curves-class.html) using optional `duration` and `curve` properties, respectively. + +### ScatterChartData +|PropName |Description |default value| +|:---------------|:---------------|:-------| +|scatterSpots| list of [ScatterSpot ](#ScatterSpot ) to show the scatter spots on the chart|[]| +|titlesData| check the [FlTitlesData](base_chart.md#FlTitlesData)| FlTitlesData()| +|axisTitleData| check the [FlAxisTitleData](base_chart.md#FlAxisTitleData)| FlAxisTitleData()| +|scatterTouchData| [ScatterTouchData](#scattertouchdata-read-about-touch-handling) holds the touch interactivity details| ScatterTouchData()| +|showingTooltipIndicators| indices of showing tooltip, The point is that you need to disable touches to show these tooltips manually|[]| +|rotationQuarterTurns|Rotates the chart 90 degrees (clockwise) in every quarter turns. This feature works like the [RotatedBox](https://api.flutter.dev/flutter/widgets/RotatedBox-class.html) widget|0| +|errorIndicatorData|Holds data for representing an error indicator (you see the error indicators if you provide the `xError` or `yError` in the [ScatterSpot](#ScatterSpot))|[ErrorIndicatorData()](base_chart.md#FlErrorIndicatorData)| + +### ScatterSpot +|PropName |Description |default value| +|:---------------|:---------------|:-------| +|show| determines to show or hide the spot|true| +|radius| radius of the showing spot| [8] +|color| colors of the spot|// a color based on the values| +|renderPriority| sort by this to manage overlap|0| +|xError| Determines the error range of the data point using (FlErrorRange)[base_chart.md#FlErrorRange] (which ontains `lowerBy` and `upperValue`) for the x-axis|null| +|yError| Determines the error range of the data point using (FlErrorRange)[base_chart.md#FlErrorRange] (which ontains `upperBy` and `upperValue`) for the y-axis|null| + + +### ScatterTouchData ([read about touch handling](handle_touches.md)) +|PropName|Description|default value| +|:-------|:----------|:------------| +|enabled|determines to enable or disable touch behaviors|true| +|mouseCursorResolver|you can change the mouse cursor based on the provided [FlTouchEvent](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#fltouchevent) and [ScatterTouchResponse](#ScatterTouchResponse)|MouseCursor.defer| +|touchTooltipData|a [ScatterTouchTooltipData](#ScatterTouchTooltipData), that determines how show the tooltip on top of touched spot (appearance of the showing tooltip bubble)|ScatterTouchTooltipData()| +|touchSpotThreshold|the threshold of the touch accuracy|0| +|handleBuiltInTouches| set this true if you want the built in touch handling (show a tooltip bubble and an indicator on touched spots) | true| +|touchCallback| listen to this callback to retrieve touch/pointer events and responses, it gives you a [FlTouchEvent](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#fltouchevent) and [ScatterTouchResponse](#ScatterTouchResponse)| null| +|longPressDuration| allows to customize the duration of the longPress gesture. If null, the duration of the longPressGesture is [kLongPressTimeout](https://api.flutter.dev/flutter/gestures/kLongPressTimeout-constant.html)| null| + +### ScatterTouchTooltipData +|PropName|Description|default value| +|:-------|:----------|:------------| +|tooltipBorder|border of the tooltip bubble|BorderSide.none| +|tooltipBorderRadius|background corner radius of the tooltip bubble|BorderRadius.circular(4)| +|tooltipPadding|padding of the tooltip|EdgeInsets.symmetric(horizontal: 16, vertical: 8)| +|tooltipHorizontalAlignment|horizontal alginment of tooltip relative to the spot|FLHorizontalAlignment.center| +|tooltipHorizontalOffset|horizontal offset of tooltip|0| +|maxContentWidth|maximum width of the tooltip (if a text row is wider than this, then the text breaks to a new line|120| +|getTooltipItems|a callback that retrieve a [ScatterTooltipItem](#ScatterTooltipItem) by the given [ScatterSpot](#ScatterSpot) |defaultScatterTooltipItem| +|fitInsideHorizontally| forces tooltip to horizontally shift inside the chart's bounding box| false| +|fitInsideVertically| forces tooltip to vertically shift inside the chart's bounding box| false| +|getTooltipColor|a callback that retrieves the Color for each touched spots separately from the given [ScatterSpot](#ScatterSpot) to set the background color of the tooltip bubble|Colors.blueGrey.darken(15)| + +### ScatterTooltipItem +|PropName|Description|default value| +|:-------|:----------|:------------| +|text|text string of each row in the tooltip bubble|null| +|textStyle|[TextStyle](https://api.flutter.dev/flutter/dart-ui/TextStyle-class.html) of the showing text row|null| +|textDirection|[TextDirection](https://api.flutter.dev/flutter/dart-ui/TextDirection-class.html) of the showing text row|TextDirection.ltr| +|bottomMargin| bottom margin of the tooltip (to the top of most top spot) | 0| +|children|[List](https://api.flutter.dev/flutter/painting/InlineSpan-class.html) pass additional InlineSpan children for a more advance tooltip|null| + + +### ScatterTouchResponse +###### you can listen to touch behaviors callback and retrieve this object when any touch action happened. +|PropName|Description|default value| +|:-------|:----------|:------------| +|touchLocation|the location of the touch event in the device pixels coordinates|required| +|touchChartCoordinate|the location of the touch event in the chart coordinates|required| +|touchedSpot|Instance of [ScatterTouchedSpot](#ScatterTouchedSpot) which holds data about the touched section|null| + +### ScatterTouchedSpot +|PropName|Description|default value| +|:-------|:----------|:------------| +|spot|touched [ScatterSpot](#ScatterSpot)|null| +|spotIndex|index of touched [ScatterSpot](#ScatterSpot)|null| + +### ScatterLabelSettings +|PropName|Description|default value| +|:-------|:----------|:------------| +|showLabel|Determines whether to show or hide the labels.|false| +|getLabelTextStyleFunction|This function gives you the index value of the spot in the list and returns the text style.|null| +|getLabelFunction|This function gives you the index value of the spot in the list and returns the label.|spot.radius.toString()| +|textDirection|Determines the direction of the text for the labels.|TextDirection.ltr| + +### some samples +---- +##### Sample 1 ([Source Code](/example/lib/presentation/samples/scatter/scatter_chart_sample1.dart)) + + + +##### Sample 2 ([Source Code](/example/lib/presentation/samples/scatter/scatter_chart_sample2.dart)) + diff --git a/repo_files/drawio/flchart.drawio b/repo_files/drawio/flchart.drawio new file mode 100644 index 0000000..73ef866 --- /dev/null +++ b/repo_files/drawio/flchart.drawio @@ -0,0 +1 @@ +7V1Jk+I2FP41XZUc6PKCF44N3WRSNanqyhySzk3YAjxjLMaIBvLrI9mSV9lsNjZEfZixnp5kLU/f+7SZJ32y2v8WgvXyD+RC/0lT3P2T/vqkaZqhauQ/KjnEkpGixIJF6LmxSE0F37x/IRNyta3nwk1OESPkY2+dFzooCKCDczIQhmiXV5sjP//WNVjAkuCbA/yy9C/PxUsmVXk1aMQX6C2W7NW2wSJmwPmxCNE2YO970vR59BdHrwDPi+lvlsBFu4xIf3vSJyFCOH5a7SfQp23Lmy1ON62ITcodwgCfksD5oU787+//fHzR0O7ny3SxU/UB770NPvAGgS5pHxZEIV6iBQqA/5ZKx1GlIc1WIaFU5ytCayJUifA7xPjAOhtsMSKiJV75LBbuPfx35vmDZvVssNDrnuUcBQ4ssMEh+gEnyEdhVE5dUdTX8YSqBe4LNQQidnyw2XhOLJx6Pn/hnDzmkhoKnCeZ8n4nXTKOm4O2QWUzM9EGbUOHaWlDazSc2aYCZo7uaO6AWzIIFxBX6ViJFZDRBdEK4vBAEoXQB9j7zBcAMDNfJHppV5MH1tvinq8q3Sfwt5Abr+mTco7XOVswf26pdY5XpBpe8KS/kFhlvSf/Ri2lxPIBpt1O44aZOAz3eAB8b8HSOaT9YJiPdqGDQlJXxHSoWYW+F8D01eRpwf6PCjjjgqk/WYIQczlpg1lRl8jWRdkyLEouqrIP5ziOtGlksbRjsIGseIqTLeas9Pp8IQtj8ROG2CNY9RK342vU0mPWqq9xIcaIaM39yP7nkcmP5yjAbPCpGgtPwcrz6WD6Av1PSHPNj8n8oE7AigYctKJjKnr2wQz64wT6+KAKUADFg5T+lUaaYEwy7OSNIBhwtDHgPiMqDx0WOxgxzGU+SdU0gi6RZJfB+CHTWmbg3TCuH29CpO0UaJVni6PrRxZchUhL2j08xKk0g4c/eI40kKaLQjxhoUddW1EsXWQV06mi2GWraBJ/zX4DsPkACPyy9zY9xmAJubeEXFXLQ67Ow8cAV1daAlz+sisQ9/t2teb6rLkbY7vaMbpLBhantUlXExljtcppxLeaNZehl5qsCKSbguRr0ZYlfUdegFPDswuGpyoFDx67AJaqYFNJMa4ws+sde5tmVm9lqavP+nmWqsbTP6p15gmDuL8rcPAGpMF6ANIAZqRrgUMHcGaS1Ef+kBTvHXisDdbx0y+/Hilgq8V5BRjQdTMYPdWURRKbJoiNZuT9iyngNSMBr1HttoiN0e1MUrvBTPKcGaMLNsuoZmqD3sA+Zfpo3sgTiP2Q3jPeoWTNIl1vkMTjfFMTL9XfarVCbG7DXpubtLamre1acIuSkpYBh4zCms7CNtWTuYGVd7aGbmQt96i+qRgFS49LcOmMr7L97pqDfyVqPSbePO/NGgRcljJvmhd9FVhR+soSKEmV3lPFpB7ZjE5dFWyveilxP1IVSe9vsVU0LCCOIuD3toDfW23Re/se6X2f2b2wlU/aH+qW4F8/z5ME//aU61RrE28Sd2txZq8t7jKD06TF1Vqc0QXPTzzqiTy/pD+yG+X5dU1411R/DMIHY/q8RvdP9HlNJM8/jeeXSL0AGauX8dW+8Xz1LtfxzyL6l7jNxBkKqMTtpgfq8JT5Qbcrslav2ZqcH1xpbmK6NurU5Ea9NjlpcW1YnN3FBGGonzdBKOo3vRFQ24Z3PUN49x5tL4DX6P5nCLwmcoZwgxnC0OzbDIGfPOqNu73saOnZZ3/a8bc1mwxV/ra12cWpq3P6tXTvstU5s9aZHtW3VKVW3zbtOv2WnPUjLOcVt7n77bTlzZtb+K0Bo8bp2rjAb6kCv9XazRujb2fFzrp5k3Fcpm7lPNezouv3472igr3D0CO9CkP27tZc2s3uT1zk0qz8GLGseg9l6Wqdfkse6hHud+ROL0n3JN1T0T2Zw67dk3r9Kub/85snjd/dHKiF03daoc9jt1O6u1nOyKjPp+U7oPx1AuSmw53CbNIRKRrp9I9+LigVCbCOZjDYRFZFEU01RZD2J5xD0lkOzCBZ/OYKMKM4n7fPvK0xDMnaEBNxwGOOQx8XcXHluW40ZHZLD8NvaxDxhl0I1qVh1AS8DPPwopbRRbRmUzSQ5rivWmrrPnHfRi+dM40UdoSY1aN1l2YOO5U3HxTx+sfN4Md4AOJYONcjqaOkjqUzO92vbPTtTLZE9wYPT4vRXS9wS6u439M2uj/CwjVB9zpETwRy6UDifyX+d790YGgS//uL/42cVCrhvznqmN0/wpXjwpkcie4S3Uvnbbpn9327/ybRvcGTzxXornbM7kcPhO6SuEtoF0F798Rdv35Z/hIcb/3+06kA2/7WHb8bcu3WnXEa/p79ZaGRUSju85FPCx1J0M4ZEF2rdAdsJ/HCHcLfgyUMPQzubo+w/i7MdXuHBdMToJToy46t7R3qj8AFRB94lbM+SQ0qPtc9Ev4yh2jYtfeh+Ef4ORzRsJN0XI450RxX1dsbciSY/qRZzIrS343T3/4D \ No newline at end of file diff --git a/repo_files/images/announcements/1.0.0.png b/repo_files/images/announcements/1.0.0.png new file mode 100644 index 0000000..e959eef Binary files /dev/null and b/repo_files/images/announcements/1.0.0.png differ diff --git a/repo_files/images/architecture/fl_chart_architecture.jpg b/repo_files/images/architecture/fl_chart_architecture.jpg new file mode 100644 index 0000000..a70c569 Binary files /dev/null and b/repo_files/images/architecture/fl_chart_architecture.jpg differ diff --git a/repo_files/images/architecture/fl_chart_architecture.txt b/repo_files/images/architecture/fl_chart_architecture.txt new file mode 100644 index 0000000..cecae6c --- /dev/null +++ b/repo_files/images/architecture/fl_chart_architecture.txt @@ -0,0 +1 @@ +https://drive.google.com/file/d/1bj-2TqTRUh80dRKJk10drPNeA3fp3EA8/view \ No newline at end of file diff --git a/repo_files/images/bar_chart/bar_chart.jpg b/repo_files/images/bar_chart/bar_chart.jpg new file mode 100644 index 0000000..06b348a Binary files /dev/null and b/repo_files/images/bar_chart/bar_chart.jpg differ diff --git a/repo_files/images/bar_chart/bar_chart_sample_1.gif b/repo_files/images/bar_chart/bar_chart_sample_1.gif new file mode 100644 index 0000000..46eaff4 Binary files /dev/null and b/repo_files/images/bar_chart/bar_chart_sample_1.gif differ diff --git a/repo_files/images/bar_chart/bar_chart_sample_1_anim.gif b/repo_files/images/bar_chart/bar_chart_sample_1_anim.gif new file mode 100644 index 0000000..1943892 Binary files /dev/null and b/repo_files/images/bar_chart/bar_chart_sample_1_anim.gif differ diff --git a/repo_files/images/bar_chart/bar_chart_sample_2.gif b/repo_files/images/bar_chart/bar_chart_sample_2.gif new file mode 100644 index 0000000..5def9f5 Binary files /dev/null and b/repo_files/images/bar_chart/bar_chart_sample_2.gif differ diff --git a/repo_files/images/bar_chart/bar_chart_sample_3.png b/repo_files/images/bar_chart/bar_chart_sample_3.png new file mode 100644 index 0000000..4f6915d Binary files /dev/null and b/repo_files/images/bar_chart/bar_chart_sample_3.png differ diff --git a/repo_files/images/bar_chart/bar_chart_sample_4.png b/repo_files/images/bar_chart/bar_chart_sample_4.png new file mode 100644 index 0000000..092f873 Binary files /dev/null and b/repo_files/images/bar_chart/bar_chart_sample_4.png differ diff --git a/repo_files/images/bar_chart/bar_chart_sample_5.gif b/repo_files/images/bar_chart/bar_chart_sample_5.gif new file mode 100644 index 0000000..c3810e5 Binary files /dev/null and b/repo_files/images/bar_chart/bar_chart_sample_5.gif differ diff --git a/repo_files/images/bar_chart/bar_chart_sample_6.png b/repo_files/images/bar_chart/bar_chart_sample_6.png new file mode 100644 index 0000000..657ad7b Binary files /dev/null and b/repo_files/images/bar_chart/bar_chart_sample_6.png differ diff --git a/repo_files/images/bar_chart/bar_chart_sample_7.gif b/repo_files/images/bar_chart/bar_chart_sample_7.gif new file mode 100644 index 0000000..4677197 Binary files /dev/null and b/repo_files/images/bar_chart/bar_chart_sample_7.gif differ diff --git a/repo_files/images/bar_chart/bar_chart_video_thumbnail.png b/repo_files/images/bar_chart/bar_chart_video_thumbnail.png new file mode 100644 index 0000000..69b9494 Binary files /dev/null and b/repo_files/images/bar_chart/bar_chart_video_thumbnail.png differ diff --git a/repo_files/images/bitcoin_public_key.jpg b/repo_files/images/bitcoin_public_key.jpg new file mode 100644 index 0000000..60be877 Binary files /dev/null and b/repo_files/images/bitcoin_public_key.jpg differ diff --git a/repo_files/images/blank.png b/repo_files/images/blank.png new file mode 100644 index 0000000..2498277 Binary files /dev/null and b/repo_files/images/blank.png differ diff --git a/repo_files/images/buy_me_a_coffee.jpeg b/repo_files/images/buy_me_a_coffee.jpeg new file mode 100644 index 0000000..02e29e4 Binary files /dev/null and b/repo_files/images/buy_me_a_coffee.jpeg differ diff --git a/repo_files/images/candlestick_chart/candlestick_chart_sample_1.gif b/repo_files/images/candlestick_chart/candlestick_chart_sample_1.gif new file mode 100644 index 0000000..7f873b0 Binary files /dev/null and b/repo_files/images/candlestick_chart/candlestick_chart_sample_1.gif differ diff --git a/repo_files/images/landing_logo.png b/repo_files/images/landing_logo.png new file mode 100644 index 0000000..04471ff Binary files /dev/null and b/repo_files/images/landing_logo.png differ diff --git a/repo_files/images/line_chart/line_chart_sample_1.gif b/repo_files/images/line_chart/line_chart_sample_1.gif new file mode 100644 index 0000000..22b70ed Binary files /dev/null and b/repo_files/images/line_chart/line_chart_sample_1.gif differ diff --git a/repo_files/images/line_chart/line_chart_sample_10.gif b/repo_files/images/line_chart/line_chart_sample_10.gif new file mode 100644 index 0000000..eaee828 Binary files /dev/null and b/repo_files/images/line_chart/line_chart_sample_10.gif differ diff --git a/repo_files/images/line_chart/line_chart_sample_1_anim.gif b/repo_files/images/line_chart/line_chart_sample_1_anim.gif new file mode 100644 index 0000000..c3407a7 Binary files /dev/null and b/repo_files/images/line_chart/line_chart_sample_1_anim.gif differ diff --git a/repo_files/images/line_chart/line_chart_sample_2.gif b/repo_files/images/line_chart/line_chart_sample_2.gif new file mode 100644 index 0000000..ea93146 Binary files /dev/null and b/repo_files/images/line_chart/line_chart_sample_2.gif differ diff --git a/repo_files/images/line_chart/line_chart_sample_2_anim.gif b/repo_files/images/line_chart/line_chart_sample_2_anim.gif new file mode 100644 index 0000000..64dca4a Binary files /dev/null and b/repo_files/images/line_chart/line_chart_sample_2_anim.gif differ diff --git a/repo_files/images/line_chart/line_chart_sample_3.gif b/repo_files/images/line_chart/line_chart_sample_3.gif new file mode 100644 index 0000000..5a99585 Binary files /dev/null and b/repo_files/images/line_chart/line_chart_sample_3.gif differ diff --git a/repo_files/images/line_chart/line_chart_sample_4.png b/repo_files/images/line_chart/line_chart_sample_4.png new file mode 100644 index 0000000..a85c99f Binary files /dev/null and b/repo_files/images/line_chart/line_chart_sample_4.png differ diff --git a/repo_files/images/line_chart/line_chart_sample_5.png b/repo_files/images/line_chart/line_chart_sample_5.png new file mode 100644 index 0000000..36a1991 Binary files /dev/null and b/repo_files/images/line_chart/line_chart_sample_5.png differ diff --git a/repo_files/images/line_chart/line_chart_sample_6.png b/repo_files/images/line_chart/line_chart_sample_6.png new file mode 100644 index 0000000..5f771c6 Binary files /dev/null and b/repo_files/images/line_chart/line_chart_sample_6.png differ diff --git a/repo_files/images/line_chart/line_chart_sample_7.png b/repo_files/images/line_chart/line_chart_sample_7.png new file mode 100644 index 0000000..7818b06 Binary files /dev/null and b/repo_files/images/line_chart/line_chart_sample_7.png differ diff --git a/repo_files/images/line_chart/line_chart_sample_8.png b/repo_files/images/line_chart/line_chart_sample_8.png new file mode 100644 index 0000000..0d339d8 Binary files /dev/null and b/repo_files/images/line_chart/line_chart_sample_8.png differ diff --git a/repo_files/images/line_chart/line_chart_sample_9.gif b/repo_files/images/line_chart/line_chart_sample_9.gif new file mode 100644 index 0000000..fc41b75 Binary files /dev/null and b/repo_files/images/line_chart/line_chart_sample_9.gif differ diff --git a/repo_files/images/line_chart/line_chart_video_thumbnail.png b/repo_files/images/line_chart/line_chart_video_thumbnail.png new file mode 100644 index 0000000..f20fb2e Binary files /dev/null and b/repo_files/images/line_chart/line_chart_video_thumbnail.png differ diff --git a/repo_files/images/overview_thumbnail.png b/repo_files/images/overview_thumbnail.png new file mode 100644 index 0000000..f4906cf Binary files /dev/null and b/repo_files/images/overview_thumbnail.png differ diff --git a/repo_files/images/pie_chart/pie_chart.jpg b/repo_files/images/pie_chart/pie_chart.jpg new file mode 100644 index 0000000..ee1cefa Binary files /dev/null and b/repo_files/images/pie_chart/pie_chart.jpg differ diff --git a/repo_files/images/pie_chart/pie_chart_sample_1.gif b/repo_files/images/pie_chart/pie_chart_sample_1.gif new file mode 100644 index 0000000..d890cf8 Binary files /dev/null and b/repo_files/images/pie_chart/pie_chart_sample_1.gif differ diff --git a/repo_files/images/pie_chart/pie_chart_sample_2.gif b/repo_files/images/pie_chart/pie_chart_sample_2.gif new file mode 100644 index 0000000..c98d3c1 Binary files /dev/null and b/repo_files/images/pie_chart/pie_chart_sample_2.gif differ diff --git a/repo_files/images/pie_chart/pie_chart_sample_3.gif b/repo_files/images/pie_chart/pie_chart_sample_3.gif new file mode 100644 index 0000000..f2747d1 Binary files /dev/null and b/repo_files/images/pie_chart/pie_chart_sample_3.gif differ diff --git a/repo_files/images/pie_chart/pie_chart_video_thumbnail.png b/repo_files/images/pie_chart/pie_chart_video_thumbnail.png new file mode 100644 index 0000000..7443ebd Binary files /dev/null and b/repo_files/images/pie_chart/pie_chart_video_thumbnail.png differ diff --git a/repo_files/images/radar_chart/radar_chart_sample_1.jpg b/repo_files/images/radar_chart/radar_chart_sample_1.jpg new file mode 100644 index 0000000..efa442d Binary files /dev/null and b/repo_files/images/radar_chart/radar_chart_sample_1.jpg differ diff --git a/repo_files/images/scatter_chart/scatter_chart.png b/repo_files/images/scatter_chart/scatter_chart.png new file mode 100644 index 0000000..4021af8 Binary files /dev/null and b/repo_files/images/scatter_chart/scatter_chart.png differ diff --git a/repo_files/images/scatter_chart/scatter_chart_sample_1.gif b/repo_files/images/scatter_chart/scatter_chart_sample_1.gif new file mode 100644 index 0000000..f4d6729 Binary files /dev/null and b/repo_files/images/scatter_chart/scatter_chart_sample_1.gif differ diff --git a/repo_files/images/scatter_chart/scatter_chart_sample_2.gif b/repo_files/images/scatter_chart/scatter_chart_sample_2.gif new file mode 100644 index 0000000..68144c7 Binary files /dev/null and b/repo_files/images/scatter_chart/scatter_chart_sample_2.gif differ diff --git a/repo_files/sponsors/become_a_hero_dark.png b/repo_files/sponsors/become_a_hero_dark.png new file mode 100644 index 0000000..350da0e Binary files /dev/null and b/repo_files/sponsors/become_a_hero_dark.png differ diff --git a/repo_files/sponsors/become_a_hero_empty.png b/repo_files/sponsors/become_a_hero_empty.png new file mode 100644 index 0000000..39d9021 Binary files /dev/null and b/repo_files/sponsors/become_a_hero_empty.png differ diff --git a/repo_files/sponsors/become_a_hero_light.png b/repo_files/sponsors/become_a_hero_light.png new file mode 100644 index 0000000..6544cb5 Binary files /dev/null and b/repo_files/sponsors/become_a_hero_light.png differ diff --git a/repo_files/sponsors/become_a_sponsor_dark.png b/repo_files/sponsors/become_a_sponsor_dark.png new file mode 100644 index 0000000..899c816 Binary files /dev/null and b/repo_files/sponsors/become_a_sponsor_dark.png differ diff --git a/repo_files/sponsors/become_a_sponsor_light.png b/repo_files/sponsors/become_a_sponsor_light.png new file mode 100644 index 0000000..0059184 Binary files /dev/null and b/repo_files/sponsors/become_a_sponsor_light.png differ diff --git a/scripts/makefile_scripts.sh b/scripts/makefile_scripts.sh new file mode 100644 index 0000000..ec26e3c --- /dev/null +++ b/scripts/makefile_scripts.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# Opens a link with the default browser of OS (It works cross-platform) +# +## You can call it like `open_link balad.ir` to open balad website on your default browser +open_link () { + case "$(uname -s)" in + Darwin) + # macOS + open "$1" + ;; + + Linux) + # Linux: + xdg-open "$1" + ;; + + CYGWIN*|MINGW32*|MSYS*|MINGW*) + # Windows + start "$1" + ;; + + *) + echo 'Not supported OS' + ;; + esac +} diff --git a/test/chart/bar_chart/bar_chart_data_test.dart b/test/chart/bar_chart/bar_chart_data_test.dart new file mode 100644 index 0000000..28921d9 --- /dev/null +++ b/test/chart/bar_chart/bar_chart_data_test.dart @@ -0,0 +1,147 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../data_pool.dart'; + +void main() { + group('BarChart data equality check', () { + test('BarChartGroupData equality test', () { + expect(barChartGroupData1 == barChartGroupData1Clone, true); + expect(barChartGroupData1 == barChartGroupData2, false); + expect(barChartGroupData1 == barChartGroupData3, false); + expect(barChartGroupData1 == barChartGroupData4, false); + expect(barChartGroupData1 == barChartGroupData5, false); + expect(barChartGroupData1 == barChartGroupData6, false); + expect(barChartGroupData1 == barChartGroupData7, false); + expect(barChartGroupData1 == barChartGroupData8, false); + expect(barChartGroupData1 == barChartGroupData9, false); + }); + + test('BarChartRodData equality test', () { + expect(barChartRodData1 == barChartRodData1Clone, true); + expect(barChartRodData1 == barChartRodData2, false); + expect(barChartRodData1 == barChartRodData3, false); + expect(barChartRodData1 == barChartRodData4, false); + expect(barChartRodData1 == barChartRodData5, false); + expect(barChartRodData1 == barChartRodData6, false); + expect(barChartRodData1 == barChartRodData7, false); + expect(barChartRodData1 == barChartRodData8, false); + }); + + test('BarChartRodStackItem equality test', () { + expect(barChartRodStackItem1 == barChartRodStackItem1Clone, true); + expect( + barChartRodStackItem1 == barChartRodStackItem1Clone.copyWith(fromY: 2), + false, + ); + expect( + barChartRodStackItem1 == barChartRodStackItem1Clone.copyWith(toY: 2), + true, + ); + expect( + barChartRodStackItem1 == barChartRodStackItem1Clone.copyWith(toY: 3), + false, + ); + expect( + barChartRodStackItem1 == + barChartRodStackItem1Clone.copyWith(color: Colors.red), + false, + ); + expect( + barChartRodStackItem1 == + barChartRodStackItem1Clone.copyWith(color: Colors.green), + true, + ); + expect(barChartRodStackItem1 == barChartRodStackItem2, false); + }); + + test('BackgroundBarChartRodData equality test', () { + expect( + backgroundBarChartRodData1 == backgroundBarChartRodData1Clone, + true, + ); + expect(backgroundBarChartRodData1 == backgroundBarChartRodData2, false); + expect(backgroundBarChartRodData2 == backgroundBarChartRodData3, false); + + final changed = BackgroundBarChartRodData( + toY: 21, + color: Colors.blue, + show: false, + ); + + expect(backgroundBarChartRodData1 == changed, false); + + final changed2 = BackgroundBarChartRodData( + toY: 22, + color: Colors.blue, + show: true, + ); + + expect(backgroundBarChartRodData1 == changed2, false); + }); + + test('BarTouchData equality test', () { + expect(barTouchData1 == barTouchData1Clone, true); + expect(barTouchData1 == barTouchData2, false); + expect(barTouchData1 == barTouchData3, false); + expect(barTouchData1 == barTouchData4, false); + expect(barTouchData1 == barTouchData5, false); + expect(barTouchData1 == barTouchData6, false); + expect(barTouchData1 == barTouchData7, false); + expect(barTouchData1 == barTouchData8, false); + expect(barTouchData1 == barTouchData9, false); + expect(barTouchData1 == barTouchData10, false); + }); + + test('BarTouchTooltipData equality test', () { + expect(barTouchTooltipData1 == barTouchTooltipData1Clone, true); + expect(barTouchTooltipData1 == barTouchTooltipData2, false); + expect(barTouchTooltipData1 == barTouchTooltipData3, false); + expect(barTouchTooltipData1 == barTouchTooltipData4, false); + expect(barTouchTooltipData1 == barTouchTooltipData5, false); + expect(barTouchTooltipData1 == barTouchTooltipData6, false); + expect(barTouchTooltipData1 == barTouchTooltipData7, false); + expect(barTouchTooltipData1 == barTouchTooltipData8, false); + expect(barTouchTooltipData1 == barTouchTooltipData9, false); + expect(barTouchTooltipData1 == barTouchTooltipData10, false); + expect(barTouchTooltipData1 == barTouchTooltipData11, false); + }); + + test('BarTooltipItem equality test', () { + expect(barTooltipItem1 == barTooltipItem1Clone, true); + expect(barTooltipItem1 == barTooltipItem2, false); + expect(barTooltipItem1 == barTooltipItem3, false); + expect(barTooltipItem1 == barTooltipItem4, false); + expect(barTooltipItem1 == barTooltipItem5, false); + }); + + test('BarTouchedSpot equality test', () { + expect(barTouchedSpot1 == barTouchedSpot1Clone, true); + expect(barTouchedSpot1 == barTouchedSpot2, false); + expect(barTouchedSpot1 == barTouchedSpot3, false); + expect(barTouchedSpot1 == barTouchedSpot4, false); + expect(barTouchedSpot1 == barTouchedSpot5, false); + expect(barTouchedSpot1 == barTouchedSpot6, false); + expect(barTouchedSpot1 == barTouchedSpot7, false); + }); + + test('BarChartData equality test', () { + expect(barChartData1 == barChartData1Clone, true); + expect(barChartData1 == barChartData2, false); + expect(barChartData1 == barChartData3, false); + expect(barChartData1 == barChartData4, false); + expect(barChartData1 == barChartData5, false); + expect(barChartData1 == barChartData6, false); + expect(barChartData1 == barChartData7, false); + expect(barChartData1 == barChartData8, false); + expect(barChartData1 == barChartData9, false); + expect(barChartData1 == barChartData10, false); + expect(barChartData1 == barChartData11, false); + expect(barChartData1 == barChartData12, false); + expect(barChartData1 == barChartData13, false); + expect(barChartData1 == barChartData14, false); + expect(barChartData1 == barChartData15, false); + }); + }); +} diff --git a/test/chart/bar_chart/bar_chart_helper_test.dart b/test/chart/bar_chart/bar_chart_helper_test.dart new file mode 100644 index 0000000..9a1631a --- /dev/null +++ b/test/chart/bar_chart/bar_chart_helper_test.dart @@ -0,0 +1,119 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/bar_chart/bar_chart_helper.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../data_pool.dart'; + +void main() { + group('Check caching of BarChartHelper.calculateMaxAxisValues', () { + test('Test validity 1', () { + final barChartHelper = BarChartHelper(); + final barGroups = [barChartGroupData1, barChartGroupData2]; + final (minY, maxY) = barChartHelper.calculateMaxAxisValues(barGroups); + expect(minY, 0); + expect(maxY, 1132); + }); + + test('Test validity 2', () { + final barChartHelper = BarChartHelper(); + final barGroups = [ + barChartGroupData1.copyWith( + barRods: [ + BarChartRodData(toY: -10), + BarChartRodData(toY: -40), + BarChartRodData(toY: 0), + BarChartRodData(toY: 10), + BarChartRodData(toY: 5), + ], + ), + ]; + final (minY, maxY) = barChartHelper.calculateMaxAxisValues(barGroups); + expect(minY, -40); + expect(maxY, 10); + }); + + test('Test validity 3', () { + final barChartHelper = BarChartHelper(); + final barGroups = [ + barChartGroupData1.copyWith(barRods: []), + ]; + final (minY, maxY) = barChartHelper.calculateMaxAxisValues(barGroups); + expect(minY, 0); + expect(maxY, 0); + }); + + test('Test validity 4', () { + final barChartHelper = BarChartHelper(); + final barGroups = [ + barChartGroupData1.copyWith( + barRods: [ + BarChartRodData(fromY: 0, toY: -10), + BarChartRodData(fromY: -10, toY: -40), + BarChartRodData(toY: 0), + BarChartRodData(toY: 10), + BarChartRodData(toY: 5), + BarChartRodData(fromY: 10, toY: -50), + BarChartRodData(fromY: 39, toY: -50), + ], + ), + ]; + final (minY, maxY) = barChartHelper.calculateMaxAxisValues(barGroups); + expect(minY, -50); + expect(maxY, 39); + }); + + test('Test equality', () { + final barChartHelper = BarChartHelper(); + final barGroups = [barChartGroupData1, barChartGroupData2]; + final result1 = barChartHelper.calculateMaxAxisValues(barGroups); + final result2 = barChartHelper.calculateMaxAxisValues(barGroups); + expect(result1, result2); + }); + + test('Test calculateMaxAxisValues with all positive values', () { + final barChartHelper = BarChartHelper(); + final barGroups = [ + barChartGroupData1.copyWith( + barRods: barChartGroupData1.barRods + .map( + (rod) => rod.copyWith( + fromY: 5, + backDrawRodData: BackgroundBarChartRodData(show: false), + ), + ) + .toList(), + ), + barChartGroupData2.copyWith( + barRods: barChartGroupData2.barRods + .map( + (rod) => rod.copyWith( + fromY: 8, + backDrawRodData: BackgroundBarChartRodData(show: false), + ), + ) + .toList(), + ), + ]; + final (minY, _) = barChartHelper.calculateMaxAxisValues(barGroups); + expect(minY, 5); + }); + + test('Test calculateMaxAxisValues with all negative values', () { + final barChartHelper = BarChartHelper(); + final barGroups = [ + barChartGroupData1.copyWith( + barRods: barChartGroupData1.barRods + .map((rod) => rod.copyWith(fromY: -5)) + .toList(), + ), + barChartGroupData2.copyWith( + barRods: barChartGroupData2.barRods + .map((rod) => rod.copyWith(fromY: -8)) + .toList(), + ), + ]; + final (minY, _) = barChartHelper.calculateMaxAxisValues(barGroups); + expect(minY, -8); + }); + }); +} diff --git a/test/chart/bar_chart/bar_chart_painter_test.dart b/test/chart/bar_chart/bar_chart_painter_test.dart new file mode 100644 index 0000000..c3595ba --- /dev/null +++ b/test/chart/bar_chart/bar_chart_painter_test.dart @@ -0,0 +1,3148 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/bar_chart/bar_chart_helper.dart'; +import 'package:fl_chart/src/chart/bar_chart/bar_chart_painter.dart'; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart'; +import 'package:fl_chart/src/extensions/bar_chart_data_extension.dart'; +import 'package:fl_chart/src/extensions/path_extension.dart'; +import 'package:fl_chart/src/utils/canvas_wrapper.dart'; +import 'package:fl_chart/src/utils/utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import '../../helper_methods.dart'; +import '../data_pool.dart'; +import 'bar_chart_painter_test.mocks.dart'; + +@GenerateMocks([Canvas, CanvasWrapper, BuildContext, Utils]) +void main() { + const tolerance = 0.01; + group('paint()', () { + test('test 1', () { + final utilsMainInstance = Utils(); + const viewSize = Size(400, 400); + final barGroups = [ + BarChartGroupData( + x: 1, + barRods: [ + BarChartRodData(fromY: 1, toY: 10), + BarChartRodData(fromY: 2, toY: 10), + ], + showingTooltipIndicators: [ + 1, + 2, + ], + ), + BarChartGroupData( + x: 2, + barRods: [ + BarChartRodData(fromY: 3, toY: 10), + BarChartRodData(fromY: 4, toY: 10), + ], + ), + ]; + + final (minY, maxY) = BarChartHelper().calculateMaxAxisValues(barGroups); + + final data = BarChartData( + barGroups: barGroups, + minY: minY, + maxY: maxY, + ); + + final barChartPainter = BarChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.getThemeAwareTextStyle(any, any)) + .thenAnswer((realInvocation) => textStyle1); + when(mockUtils.calculateRotationOffset(any, any)) + .thenAnswer((realInvocation) => Offset.zero); + when(mockUtils.convertRadiusToSigma(any)) + .thenAnswer((realInvocation) => 4.0); + when(mockUtils.getEfficientInterval(any, any)) + .thenAnswer((realInvocation) => 1.0); + when(mockUtils.getBestInitialIntervalValue(any, any, any)) + .thenAnswer((realInvocation) => 1.0); + when(mockUtils.normalizeBorderRadius(any, any)) + .thenAnswer((realInvocation) => BorderRadius.zero); + when(mockUtils.normalizeBorderSide(any, any)).thenAnswer( + (realInvocation) => const BorderSide(color: MockData.color0), + ); + + final mockBuildContext = MockBuildContext(); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + barChartPainter.paint( + mockBuildContext, + mockCanvasWrapper, + holder, + ); + Utils.changeInstance(utilsMainInstance); + }); + }); + + group('scaling related', () { + final utilsMainInstance = Utils(); + late MockUtils mockUtils; + + setUp(() { + mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + + when(mockUtils.getThemeAwareTextStyle(any, any)) + .thenAnswer((realInvocation) => textStyle1); + when(mockUtils.calculateRotationOffset(any, any)) + .thenAnswer((realInvocation) => Offset.zero); + when(mockUtils.convertRadiusToSigma(any)) + .thenAnswer((realInvocation) => 4.0); + when(mockUtils.getEfficientInterval(any, any)) + .thenAnswer((realInvocation) => 1.0); + when(mockUtils.getBestInitialIntervalValue(any, any, any)) + .thenAnswer((realInvocation) => 1.0); + when(mockUtils.normalizeBorderRadius(any, any)) + .thenAnswer((realInvocation) => BorderRadius.zero); + when(mockUtils.normalizeBorderSide(any, any)).thenAnswer( + (realInvocation) => const BorderSide(color: MockData.color0), + ); + }); + + tearDown(() { + Utils.changeInstance(utilsMainInstance); + }); + + test('clips to canvas size if chart virtual rect is provided', () { + const viewSize = Size(400, 400); + final chartVirtualRect = (Offset.zero & viewSize).inflate(100); + + final barGroups = [ + BarChartGroupData( + x: 0, + barRods: [ + BarChartRodData( + toY: 10, + width: 10, + color: const Color(0x00000000), + borderRadius: const BorderRadius.all(Radius.circular(0.1)), + ), + BarChartRodData( + toY: 8, + width: 11, + color: const Color(0x11111111), + borderRadius: const BorderRadius.all(Radius.circular(0.2)), + ), + BarChartRodData( + toY: 8, + width: 12, + color: const Color(0x22222222), + borderRadius: const BorderRadius.all(Radius.circular(0.3)), + ), + ], + barsSpace: 5, + showingTooltipIndicators: [ + 1, + 2, + ], + ), + BarChartGroupData( + x: 1, + barRods: [ + BarChartRodData( + toY: 10, + width: 10, + borderRadius: const BorderRadius.all(Radius.circular(0.4)), + ), + BarChartRodData(toY: 8, width: 10), + ], + barsSpace: 5, + ), + BarChartGroupData( + x: 2, + barRods: [ + BarChartRodData(toY: 10, width: 10), + BarChartRodData(toY: 8, width: 10), + BarChartRodData(toY: 8, width: 10), + BarChartRodData(toY: 8, width: 10), + ], + barsSpace: 5, + ), + ]; + + final tooltipData = BarTouchTooltipData( + tooltipBorderRadius: BorderRadius.circular(8), + getTooltipColor: (group) => const Color(0xf33f33f3), + maxContentWidth: 80, + rotateAngle: 12, + tooltipBorder: const BorderSide(color: Color(0xf33f33f3), width: 2), + getTooltipItem: ( + group, + groupIndex, + rod, + rodIndex, + ) { + return BarTooltipItem( + 'helllo1', + textStyle1, + textAlign: TextAlign.right, + textDirection: TextDirection.rtl, + children: [ + const TextSpan(text: 'helllo2'), + const TextSpan(text: 'helllo3'), + ], + ); + }, + ); + + final (minY, maxY) = BarChartHelper().calculateMaxAxisValues(barGroups); + + final data = BarChartData( + groupsSpace: 10, + barGroups: barGroups, + barTouchData: BarTouchData( + touchTooltipData: tooltipData, + ), + alignment: BarChartAlignment.center, + minY: minY, + maxY: maxY, + ); + + final barChartPainter = BarChartPainter(); + final holder = PaintHolder( + data, + data, + TextScaler.noScaling, + chartVirtualRect, + ); + + final mockBuildContext = MockBuildContext(); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenReturn(viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + barChartPainter.paint( + mockBuildContext, + mockCanvasWrapper, + holder, + ); + + final canvasRect = Offset.zero & viewSize; + verifyInOrder([ + mockCanvasWrapper.saveLayer(canvasRect, any), + mockCanvasWrapper.clipRect(canvasRect), + mockCanvasWrapper.drawRRect(any, any), + mockCanvasWrapper.restore(), + mockCanvasWrapper.drawRotated( + size: anyNamed('size'), + rotationOffset: anyNamed('rotationOffset'), + drawOffset: anyNamed('drawOffset'), + angle: anyNamed('angle'), + drawCallback: anyNamed('drawCallback'), + ), + ]); + }); + + test('only draws points within canvas when chart virtual rect is provided', + () { + const viewSize = Size(200, 200); + const zoomedSize = Size(300, 300); + final chartVirtualRect = const Offset(0, -150) & zoomedSize; + const maxToY = 10.0; // Y coordinate -150 - outside of canvas + const minToY = 5.0; // Y coordinate 150 - inside of canvas + + final barGroups = [ + BarChartGroupData( + x: 0, + barRods: [ + BarChartRodData( + toY: maxToY, + width: 10, + color: const Color(0x00000000), + borderRadius: const BorderRadius.all(Radius.circular(0.1)), + ), + BarChartRodData( + toY: minToY, + width: 11, + color: const Color(0x11111111), + borderRadius: const BorderRadius.all(Radius.circular(0.2)), + ), + ], + barsSpace: 5, + showingTooltipIndicators: [ + 0, + 1, + ], + ), + ]; + + final tooltipData = BarTouchTooltipData( + tooltipBorderRadius: BorderRadius.circular(8), + getTooltipColor: (group) => const Color(0xf33f33f3), + maxContentWidth: 80, + rotateAngle: 12, + tooltipBorder: const BorderSide(color: Color(0xf33f33f3), width: 2), + getTooltipItem: ( + group, + groupIndex, + rod, + rodIndex, + ) { + return BarTooltipItem( + 'helllo1', + textStyle1, + textAlign: TextAlign.right, + textDirection: TextDirection.rtl, + children: [ + const TextSpan(text: 'helllo2'), + const TextSpan(text: 'helllo3'), + ], + ); + }, + ); + + final (minY, maxY) = BarChartHelper().calculateMaxAxisValues(barGroups); + + final data = BarChartData( + groupsSpace: 10, + barGroups: barGroups, + barTouchData: BarTouchData( + touchTooltipData: tooltipData, + ), + alignment: BarChartAlignment.center, + minY: minY, + maxY: maxY, + ); + + final barChartPainter = BarChartPainter(); + final holder = PaintHolder( + data, + data, + TextScaler.noScaling, + chartVirtualRect, + ); + + final mockBuildContext = MockBuildContext(); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenReturn(viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + barChartPainter.paint( + mockBuildContext, + mockCanvasWrapper, + holder, + ); + + verify( + mockCanvasWrapper.drawRotated( + size: anyNamed('size'), + rotationOffset: anyNamed('rotationOffset'), + drawOffset: anyNamed('drawOffset'), + angle: anyNamed('angle'), + drawCallback: anyNamed('drawCallback'), + ), + ).called(1); + }); + + test('does not clip if chart virtual rect is null', () { + { + const viewSize = Size(400, 400); + + final barGroups = [ + BarChartGroupData( + x: 0, + barRods: [ + BarChartRodData( + toY: 10, + width: 10, + color: const Color(0x00000000), + borderRadius: const BorderRadius.all(Radius.circular(0.1)), + ), + BarChartRodData( + toY: 8, + width: 11, + color: const Color(0x11111111), + borderRadius: const BorderRadius.all(Radius.circular(0.2)), + ), + BarChartRodData( + toY: 8, + width: 12, + color: const Color(0x22222222), + borderRadius: const BorderRadius.all(Radius.circular(0.3)), + ), + ], + barsSpace: 5, + showingTooltipIndicators: [ + 1, + 2, + ], + ), + BarChartGroupData( + x: 1, + barRods: [ + BarChartRodData( + toY: 10, + width: 10, + borderRadius: const BorderRadius.all(Radius.circular(0.4)), + ), + BarChartRodData(toY: 8, width: 10), + ], + barsSpace: 5, + ), + BarChartGroupData( + x: 2, + barRods: [ + BarChartRodData(toY: 10, width: 10), + BarChartRodData(toY: 8, width: 10), + BarChartRodData(toY: 8, width: 10), + BarChartRodData(toY: 8, width: 10), + ], + barsSpace: 5, + ), + ]; + + final tooltipData = BarTouchTooltipData( + tooltipBorderRadius: BorderRadius.circular(8), + getTooltipColor: (group) => const Color(0xf33f33f3), + maxContentWidth: 80, + rotateAngle: 12, + tooltipBorder: const BorderSide(color: Color(0xf33f33f3), width: 2), + getTooltipItem: ( + group, + groupIndex, + rod, + rodIndex, + ) { + return BarTooltipItem( + 'helllo1', + textStyle1, + textAlign: TextAlign.right, + textDirection: TextDirection.rtl, + children: [ + const TextSpan(text: 'helllo2'), + const TextSpan(text: 'helllo3'), + ], + ); + }, + ); + + final (minY, maxY) = BarChartHelper().calculateMaxAxisValues(barGroups); + + final data = BarChartData( + groupsSpace: 10, + barGroups: barGroups, + barTouchData: BarTouchData( + touchTooltipData: tooltipData, + ), + alignment: BarChartAlignment.center, + minY: minY, + maxY: maxY, + ); + + final barChartPainter = BarChartPainter(); + final holder = PaintHolder( + data, + data, + TextScaler.noScaling, + ); + + final mockBuildContext = MockBuildContext(); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenReturn(viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + barChartPainter.paint( + mockBuildContext, + mockCanvasWrapper, + holder, + ); + + final canvasRect = Offset.zero & viewSize; + verifyNever(mockCanvasWrapper.saveLayer(canvasRect, any)); + verifyNever(mockCanvasWrapper.clipRect(canvasRect)); + verifyNever(mockCanvasWrapper.restore()); + } + }); + + test('draws all points if chart virtual rect is null', () { + const viewSize = Size(200, 200); + const maxToY = 10.0; // Y coordinate -150 - outside of canvas + const minToY = 5.0; // Y coordinate 150 - inside of canvas + + final barGroups = [ + BarChartGroupData( + x: 0, + barRods: [ + BarChartRodData( + toY: maxToY, + width: 10, + color: const Color(0x00000000), + borderRadius: const BorderRadius.all(Radius.circular(0.1)), + ), + BarChartRodData( + toY: minToY, + width: 11, + color: const Color(0x11111111), + borderRadius: const BorderRadius.all(Radius.circular(0.2)), + ), + ], + barsSpace: 5, + showingTooltipIndicators: [ + 0, + 1, + ], + ), + ]; + + final tooltipData = BarTouchTooltipData( + tooltipBorderRadius: BorderRadius.circular(8), + getTooltipColor: (group) => const Color(0xf33f33f3), + maxContentWidth: 80, + rotateAngle: 12, + tooltipBorder: const BorderSide(color: Color(0xf33f33f3), width: 2), + getTooltipItem: ( + group, + groupIndex, + rod, + rodIndex, + ) { + return BarTooltipItem( + 'helllo1', + textStyle1, + textAlign: TextAlign.right, + textDirection: TextDirection.rtl, + children: [ + const TextSpan(text: 'helllo2'), + const TextSpan(text: 'helllo3'), + ], + ); + }, + ); + + final (minY, maxY) = BarChartHelper().calculateMaxAxisValues(barGroups); + + final data = BarChartData( + groupsSpace: 10, + barGroups: barGroups, + barTouchData: BarTouchData( + touchTooltipData: tooltipData, + ), + alignment: BarChartAlignment.center, + minY: minY, + maxY: maxY, + ); + + final barChartPainter = BarChartPainter(); + final holder = PaintHolder( + data, + data, + TextScaler.noScaling, + ); + + final mockBuildContext = MockBuildContext(); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenReturn(viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + barChartPainter.paint( + mockBuildContext, + mockCanvasWrapper, + holder, + ); + + verify( + mockCanvasWrapper.drawRotated( + size: anyNamed('size'), + rotationOffset: anyNamed('rotationOffset'), + drawOffset: anyNamed('drawOffset'), + angle: anyNamed('angle'), + drawCallback: anyNamed('drawCallback'), + ), + ).called(2); + }); + }); + + group('calculateGroupsX()', () { + test('test 1', () { + const viewSize = Size(200, 100); + + final barGroups = [ + BarChartGroupData( + x: 0, + barRods: [ + BarChartRodData(toY: 10, width: 10), + BarChartRodData(toY: 8, width: 10), + BarChartRodData(toY: 8, width: 10), + ], + barsSpace: 5, + ), + BarChartGroupData( + x: 1, + barRods: [ + BarChartRodData(toY: 10, width: 10), + BarChartRodData(toY: 8, width: 10), + ], + barsSpace: 5, + ), + BarChartGroupData( + x: 2, + barRods: [ + BarChartRodData(toY: 10, width: 10), + BarChartRodData(toY: 8, width: 10), + BarChartRodData(toY: 8, width: 10), + BarChartRodData(toY: 8, width: 10), + ], + barsSpace: 5, + ), + ]; + + final data = BarChartData( + titlesData: const FlTitlesData(show: false), + groupsSpace: 10, + barGroups: barGroups, + ); + + List callWithAlignment(BarChartAlignment alignment) { + return data + .copyWith(alignment: alignment) + .calculateGroupsX(viewSize.width); + } + + expect(callWithAlignment(BarChartAlignment.center), [50, 92.5, 142.5]); + expect(callWithAlignment(BarChartAlignment.start), [20, 62.5, 112.5]); + expect(callWithAlignment(BarChartAlignment.end), [80.0, 122.5, 172.5]); + expect( + callWithAlignment(BarChartAlignment.spaceEvenly), + [40, 92.5, 152.5], + ); + expect( + callWithAlignment(BarChartAlignment.spaceAround), + [ + closeTo(33.33, tolerance), + 92.5, + closeTo(159.16, tolerance), + ], + ); + expect( + callWithAlignment(BarChartAlignment.spaceBetween), + [20, 92.5, 172.5], + ); + }); + }); + + group('calculateGroupAndBarsPosition()', () { + test('test 1', () { + const viewSize = Size(200, 100); + + final barGroups = [ + BarChartGroupData( + x: 0, + barRods: [ + BarChartRodData(toY: 10, width: 10), + BarChartRodData(toY: 8, width: 10), + BarChartRodData(toY: 8, width: 10), + ], + barsSpace: 5, + ), + BarChartGroupData( + x: 1, + barRods: [ + BarChartRodData(toY: 10, width: 10), + BarChartRodData(toY: 8, width: 10), + ], + barsSpace: 5, + ), + ]; + + final data = BarChartData( + titlesData: const FlTitlesData(show: false), + groupsSpace: 10, + alignment: BarChartAlignment.center, + barGroups: barGroups, + ); + + final barChartPainter = BarChartPainter(); + + final groupsX = data.calculateGroupsX(viewSize.width); + late Exception exception; + try { + barChartPainter.calculateGroupAndBarsPosition( + viewSize, + groupsX + [groupsX.last], + barGroups, + ); + } catch (e) { + exception = e as Exception; + } + + expect(true, exception.toString().contains('inconsistent')); + }); + + test('test 2', () { + const viewSize = Size(200, 100); + + final barGroups = [ + BarChartGroupData( + x: 0, + barRods: [ + BarChartRodData(toY: 10, width: 10), + BarChartRodData(toY: 8, width: 10), + BarChartRodData(toY: 8, width: 10), + ], + barsSpace: 5, + ), + BarChartGroupData( + x: 1, + barRods: [ + BarChartRodData(toY: 10, width: 10), + BarChartRodData(toY: 8, width: 10), + ], + barsSpace: 5, + ), + BarChartGroupData( + x: 2, + barRods: [ + BarChartRodData(toY: 10, width: 10), + BarChartRodData(toY: 8, width: 10), + BarChartRodData(toY: 8, width: 10), + BarChartRodData(toY: 8, width: 10), + ], + barsSpace: 5, + ), + ]; + + final data = BarChartData( + titlesData: const FlTitlesData(show: false), + groupsSpace: 10, + barGroups: barGroups, + ); + + final barChartPainter = BarChartPainter(); + + List callWithAlignment(BarChartAlignment alignment) { + final groupsX = data + .copyWith(alignment: alignment) + .calculateGroupsX(viewSize.width); + return barChartPainter.calculateGroupAndBarsPosition( + viewSize, + groupsX, + barGroups, + ); + } + + final centerResult = callWithAlignment(BarChartAlignment.center); + expect(centerResult.map((e) => e.groupX).toList(), [50, 92.5, 142.5]); + expect(centerResult[0].barsX, [35, 50, 65]); + expect(centerResult[1].barsX, [85, 100]); + expect(centerResult[2].barsX, [120, 135, 150, 165]); + + final startResult = callWithAlignment(BarChartAlignment.start); + expect(startResult.map((e) => e.groupX).toList(), [20.0, 62.5, 112.5]); + expect(startResult[0].barsX, [5, 20, 35]); + expect(startResult[1].barsX, [55, 70]); + expect(startResult[2].barsX, [90, 105, 120, 135]); + }); + }); + + group('drawBars()', () { + test('test 1', () { + const viewSize = Size(200, 100); + + final barGroups = [ + BarChartGroupData( + x: 0, + barRods: [ + BarChartRodData( + toY: 10, + width: 10, + color: const Color(0x00000000), + borderRadius: const BorderRadius.all(Radius.circular(0.1)), + ), + BarChartRodData( + toY: 8, + width: 11, + color: const Color(0x11111111), + borderRadius: const BorderRadius.all(Radius.circular(0.2)), + ), + BarChartRodData( + toY: 8, + width: 12, + color: const Color(0x22222222), + borderRadius: const BorderRadius.all(Radius.circular(0.3)), + ), + ], + barsSpace: 5, + ), + BarChartGroupData( + x: 1, + barRods: [ + BarChartRodData( + toY: 10, + width: 10, + borderRadius: const BorderRadius.all(Radius.circular(0.4)), + ), + BarChartRodData(toY: 8, width: 10), + ], + barsSpace: 5, + ), + BarChartGroupData( + x: 2, + barRods: [ + BarChartRodData( + toY: 10, + width: 10, + backDrawRodData: BackgroundBarChartRodData( + toY: 8, + show: true, + ), + ), + BarChartRodData( + toY: 8, + width: 10, + backDrawRodData: BackgroundBarChartRodData( + show: false, + ), + ), + BarChartRodData( + toY: 8, + width: 10, + backDrawRodData: BackgroundBarChartRodData( + toY: -3, + show: true, + ), + ), + BarChartRodData( + toY: 8, + width: 10, + backDrawRodData: BackgroundBarChartRodData( + toY: 0, + ), + ), + ], + barsSpace: 5, + ), + ]; + + final (minY, maxY) = BarChartHelper().calculateMaxAxisValues(barGroups); + + final data = BarChartData( + titlesData: const FlTitlesData(show: false), + groupsSpace: 10, + barGroups: barGroups, + alignment: BarChartAlignment.center, + minY: minY, + maxY: maxY, + ); + + final barChartPainter = BarChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final groupsX = data.calculateGroupsX(viewSize.width); + final barGroupsPosition = barChartPainter.calculateGroupAndBarsPosition( + viewSize, + groupsX, + barGroups, + ); + + final results = >[]; + when(mockCanvasWrapper.drawRRect(captureAny, captureAny)) + .thenAnswer((inv) { + final rRect = inv.positionalArguments[0] as RRect; + final paint = inv.positionalArguments[1] as Paint; + results.add({ + 'rRect': RRect.fromLTRBAndCorners( + rRect.left, + rRect.top, + rRect.right, + rRect.bottom, + topLeft: rRect.tlRadius, + topRight: rRect.trRadius, + bottomRight: rRect.brRadius, + bottomLeft: rRect.blRadius, + ), + 'paint_color': paint.color, + }); + }); + + barChartPainter.drawBars(mockCanvasWrapper, barGroupsPosition, holder); + expect(results.length, 11); + + expect( + HelperMethods.equalsRRects( + results[0]['rRect'] as RRect, + RRect.fromLTRBR( + 28.5, + 0, + 38.5, + 76.923, + const Radius.circular(0.1), + ), + ), + true, + ); + expect( + results[0]['paint_color'] as Color, + isSameColorAs(const Color(0x00000000)), + ); + + expect( + HelperMethods.equalsRRects( + results[1]['rRect'] as RRect, + RRect.fromLTRBR( + 43.5, + 15.384, + 54.5, + 76.923, + const Radius.circular(0.2), + ), + ), + true, + ); + expect( + results[1]['paint_color'] as Color, + isSameColorAs(const Color(0x11111111)), + ); + + expect( + HelperMethods.equalsRRects( + results[2]['rRect'] as RRect, + RRect.fromLTRBR( + 59.5, + 15.384, + 71.5, + 76.923, + const Radius.circular(0.3), + ), + ), + true, + ); + expect( + results[2]['paint_color'] as Color, + isSameColorAs(const Color(0x22222222)), + ); + + expect( + HelperMethods.equalsRRects( + results[3]['rRect'] as RRect, + RRect.fromLTRBR( + 81.5, + 0, + 91.5, + 76.923, + const Radius.circular(0.4), + ), + ), + true, + ); + expect( + HelperMethods.equalsRRects( + results[4]['rRect'] as RRect, + RRect.fromLTRBR( + 96.5, + 15.384, + 106.5, + 76.923, + const Radius.circular(5), + ), + ), + true, + ); + + expect( + HelperMethods.equalsRRects( + results[5]['rRect'] as RRect, + RRect.fromLTRBR( + 116.5, + 15.384, + 126.5, + 76.923, + const Radius.circular(5), + ), + ), + true, + ); + + expect( + HelperMethods.equalsRRects( + results[6]['rRect'] as RRect, + RRect.fromLTRBR( + 116.5, + 0, + 126.5, + 76.923, + const Radius.circular(5), + ), + ), + true, + ); + expect( + HelperMethods.equalsRRects( + results[7]['rRect'] as RRect, + RRect.fromLTRBR( + 131.5, + 15.384, + 141.5, + 76.923, + const Radius.circular(5), + ), + ), + true, + ); + + expect( + HelperMethods.equalsRRects( + results[8]['rRect'] as RRect, + RRect.fromLTRBR( + 146.5, + 76.923, + 156.5, + 100, + const Radius.circular(5), + ), + ), + true, + ); + + expect( + HelperMethods.equalsRRects( + results[9]['rRect'] as RRect, + RRect.fromLTRBR( + 146.5, + 15.384, + 156.5, + 76.923, + const Radius.circular(5), + ), + ), + true, + ); + expect( + HelperMethods.equalsRRects( + results[10]['rRect'] as RRect, + RRect.fromLTRBR( + 161.5, + 15.384, + 171.5, + 76.923, + const Radius.circular(5), + ), + ), + true, + ); + }); + test('test 2', () { + const viewSize = Size(200, 100); + + final barGroups = [ + BarChartGroupData( + x: 0, + groupVertically: true, + barRods: [ + BarChartRodData( + fromY: -9, + toY: -10, + width: 10, + color: const Color(0x00000000), + borderRadius: const BorderRadius.all(Radius.circular(0.1)), + ), + BarChartRodData( + fromY: -11, + toY: -20, + width: 11, + color: const Color(0x11111111), + borderRadius: const BorderRadius.all(Radius.circular(0.2)), + ), + BarChartRodData( + fromY: -21, + toY: -30, + width: 12, + color: const Color(0x22222222), + borderRadius: const BorderRadius.all(Radius.circular(0.3)), + ), + ], + barsSpace: 5, + ), + BarChartGroupData( + x: 1, + groupVertically: true, + barRods: [ + BarChartRodData( + fromY: 9, + toY: 10, + width: 10, + color: const Color(0x00000000), + borderRadius: const BorderRadius.all(Radius.circular(0.1)), + ), + BarChartRodData( + fromY: 11, + toY: 20, + width: 11, + color: const Color(0x11111111), + borderRadius: const BorderRadius.all(Radius.circular(0.2)), + ), + BarChartRodData( + fromY: 21, + toY: 30, + width: 12, + color: const Color(0x22222222), + borderRadius: const BorderRadius.all(Radius.circular(0.3)), + ), + ], + barsSpace: 5, + ), + ]; + + final (minY, maxY) = BarChartHelper().calculateMaxAxisValues(barGroups); + + final data = BarChartData( + titlesData: const FlTitlesData(show: false), + groupsSpace: 10, + barGroups: barGroups, + alignment: BarChartAlignment.center, + minY: minY, + maxY: maxY, + ); + + final barChartPainter = BarChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final groupsX = data.calculateGroupsX(viewSize.width); + final barGroupsPosition = barChartPainter.calculateGroupAndBarsPosition( + viewSize, + groupsX, + barGroups, + ); + + final results = >[]; + when(mockCanvasWrapper.drawRRect(captureAny, captureAny)) + .thenAnswer((inv) { + final rrect = inv.positionalArguments[0] as RRect; + final paint = inv.positionalArguments[1] as Paint; + results.add({ + 'rrect': RRect.fromLTRBAndCorners( + rrect.left, + rrect.top, + rrect.right, + rrect.bottom, + topLeft: rrect.tlRadius, + topRight: rrect.trRadius, + bottomRight: rrect.brRadius, + bottomLeft: rrect.blRadius, + ), + 'paint_color': paint.color, + }); + }); + + barChartPainter.drawBars(mockCanvasWrapper, barGroupsPosition, holder); + expect(results.length, 6); + + expect( + HelperMethods.equalsRRects( + results[0]['rrect'] as RRect, + RRect.fromLTRBR( + 84, + 65, + 94, + 66.666, + const Radius.circular(0.1), + ), + ), + true, + ); + expect( + results[0]['paint_color'] as Color, + isSameColorAs(const Color(0x00000000)), + ); + + expect( + HelperMethods.equalsRRects( + results[1]['rrect'] as RRect, + RRect.fromLTRBR( + 83.5, + 68.333, + 94.5, + 83.333, + const Radius.circular(0.2), + ), + ), + true, + ); + expect( + results[1]['paint_color'] as Color, + isSameColorAs(const Color(0x11111111)), + ); + + expect( + HelperMethods.equalsRRects( + results[2]['rrect'] as RRect, + RRect.fromLTRBR( + 83, + 85, + 95, + 100, + const Radius.circular(0.3), + ), + ), + true, + ); + expect( + results[2]['paint_color'] as Color, + isSameColorAs(const Color(0x22222222)), + ); + + expect( + HelperMethods.equalsRRects( + results[3]['rrect'] as RRect, + RRect.fromLTRBR( + 106, + 33.333, + 116, + 35, + const Radius.circular(0.1), + ), + ), + true, + ); + expect( + HelperMethods.equalsRRects( + results[4]['rrect'] as RRect, + RRect.fromLTRBR( + 105.5, + 16.666, + 116.5, + 31.666, + const Radius.circular(0.2), + ), + ), + true, + ); + + expect( + HelperMethods.equalsRRects( + results[5]['rrect'] as RRect, + RRect.fromLTRBR( + 105, + 0, + 117, + 15, + const Radius.circular(0.3), + ), + ), + true, + ); + }); + + test('test 3', () { + const viewSize = Size(200, 100); + + final barGroups = [ + BarChartGroupData( + x: 0, + barRods: [ + BarChartRodData( + fromY: -10, + toY: 10, + color: const Color(0x00000000), + rodStackItems: [ + BarChartRodStackItem(-5, -10, const Color(0x11111111)), + BarChartRodStackItem(0, -5, const Color(0x22222222)), + BarChartRodStackItem(0, 5, const Color(0x33333333)), + BarChartRodStackItem(5, 10, const Color(0x44444444)), + ], + ), + ], + ), + ]; + + final data = BarChartData(barGroups: barGroups); + + final barChartPainter = BarChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final groupsX = data.calculateGroupsX(viewSize.width); + final barGroupsPosition = barChartPainter.calculateGroupAndBarsPosition( + viewSize, + groupsX, + barGroups, + ); + + final results = >[]; + when(mockCanvasWrapper.drawRRect(captureAny, captureAny)) + .thenAnswer((inv) { + final paint = inv.positionalArguments[1] as Paint; + results.add({ + 'paint_color': paint.color, + }); + }); + + barChartPainter.drawBars(mockCanvasWrapper, barGroupsPosition, holder); + expect(results.length, 5); + expect( + results[1]['paint_color'], + isSameColorAs(const Color(0x11111111)), + ); + expect( + results[2]['paint_color'], + isSameColorAs(const Color(0x22222222)), + ); + expect( + results[3]['paint_color'], + isSameColorAs(const Color(0x33333333)), + ); + expect( + results[4]['paint_color'], + isSameColorAs(const Color(0x44444444)), + ); + }); + + test('test 4', () { + const viewSize = Size(200, 100); + + final barGroups = [ + BarChartGroupData( + x: 0, + barRods: [ + BarChartRodData( + fromY: 0, + toY: 5, + borderRadius: BorderRadius.zero, + color: Colors.white, + ), + ], + ), + BarChartGroupData( + x: 1, + barRods: [ + BarChartRodData( + fromY: 0, + toY: 10, + borderRadius: BorderRadius.zero, + color: Colors.white, + ), + ], + ), + BarChartGroupData( + x: 3, + barRods: [ + BarChartRodData( + fromY: 0, + toY: 15, + borderDashArray: [4, 4], + borderSide: const BorderSide( + color: Colors.white, + width: 2, + ), + borderRadius: BorderRadius.zero, + color: Colors.transparent, + ), + ], + ), + ]; + + final (minY, maxY) = BarChartHelper().calculateMaxAxisValues(barGroups); + + final data = BarChartData( + barGroups: barGroups, + minY: minY, + maxY: maxY, + ); + + final barChartPainter = BarChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final groupsX = data.calculateGroupsX(viewSize.width); + final barGroupsPosition = barChartPainter.calculateGroupAndBarsPosition( + viewSize, + groupsX, + barGroups, + ); + + final rodDataResults = >[]; + final borderResult = >[]; + + when(mockCanvasWrapper.drawRRect(captureAny, captureAny)) + .thenAnswer((inv) { + final rrect = inv.positionalArguments[0] as RRect; + final paint = inv.positionalArguments[1] as Paint; + rodDataResults.add({ + 'rrect': rrect, + 'paint_color': paint.color, + }); + }); + + when(mockCanvasWrapper.drawPath(captureAny, captureAny)) + .thenAnswer((inv) { + final path = inv.positionalArguments[0] as Path; + final paint = inv.positionalArguments[1] as Paint; + borderResult.add({ + 'path': path, + 'paint_color': paint.color, + }); + }); + + barChartPainter.drawBars(mockCanvasWrapper, barGroupsPosition, holder); + expect(rodDataResults.length, 3); + expect(rodDataResults[0]['paint_color'], Colors.white); + expect(rodDataResults[1]['paint_color'], Colors.white); + expect(rodDataResults[2]['paint_color'], Colors.transparent); + + expect(borderResult.length, 1); + expect(borderResult[0]['paint_color'], Colors.white); + final rrect = rodDataResults[2]['rrect'] as RRect; + final path = Path()..addRRect(rrect); + final expectedPath = path.toDashedPath([4, 4]); + final currentPath = borderResult[0]['path'] as Path; + + expect(HelperMethods.equalsPaths(expectedPath, currentPath), true); + }); + + test('test 5', () { + const viewSize = Size(200, 100); + + final barGroups = [ + BarChartGroupData( + x: 0, + barRods: [ + BarChartRodData( + fromY: 0, + toY: 5, + borderRadius: BorderRadius.zero, + color: Colors.white, + ), + ], + ), + BarChartGroupData( + x: 1, + barRods: [ + BarChartRodData( + fromY: 0, + toY: 10, + borderRadius: BorderRadius.zero, + color: Colors.white, + ), + ], + ), + BarChartGroupData( + x: 3, + barRods: [ + BarChartRodData( + fromY: 0, + toY: 15, + borderSide: const BorderSide( + color: Colors.white, + width: 2, + ), + borderRadius: BorderRadius.zero, + color: Colors.transparent, + ), + ], + ), + ]; + + final (minY, maxY) = BarChartHelper().calculateMaxAxisValues(barGroups); + + final data = BarChartData( + barGroups: barGroups, + minY: minY, + maxY: maxY, + ); + + final barChartPainter = BarChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final groupsX = data.calculateGroupsX(viewSize.width); + final barGroupsPosition = barChartPainter.calculateGroupAndBarsPosition( + viewSize, + groupsX, + barGroups, + ); + + final rodDataResults = >[]; + final borderResult = >[]; + + when(mockCanvasWrapper.drawRRect(captureAny, captureAny)) + .thenAnswer((inv) { + final rrect = inv.positionalArguments[0] as RRect; + final paint = inv.positionalArguments[1] as Paint; + rodDataResults.add({ + 'rrect': rrect, + 'paint_color': paint.color, + }); + }); + + when(mockCanvasWrapper.drawPath(captureAny, captureAny)) + .thenAnswer((inv) { + final path = inv.positionalArguments[0] as Path; + final paint = inv.positionalArguments[1] as Paint; + borderResult.add({ + 'path': path, + 'paint_color': paint.color, + }); + }); + + barChartPainter.drawBars(mockCanvasWrapper, barGroupsPosition, holder); + expect(rodDataResults.length, 3); + expect(rodDataResults[0]['paint_color'], Colors.white); + expect(rodDataResults[1]['paint_color'], Colors.white); + expect(rodDataResults[2]['paint_color'], Colors.transparent); + + expect(borderResult.length, 1); + expect(borderResult[0]['paint_color'], Colors.white); + final rrect = rodDataResults[2]['rrect'] as RRect; + final path = Path()..addRRect(rrect); + final expectedPath = path.toDashedPath(null); + final currentPath = borderResult[0]['path'] as Path; + + expect(HelperMethods.equalsPaths(expectedPath, currentPath), true); + }); + }); + + group('drawTouchTooltip()', () { + test('test 1', () { + final mockUtils = MockUtils(); + when(mockUtils.getThemeAwareTextStyle(any, any)).thenReturn(textStyle1); + when(mockUtils.getEfficientInterval(any, any)).thenReturn(11); + when(mockUtils.normalizeBorderRadius(any, any)) + .thenReturn(BorderRadius.zero); + when(mockUtils.normalizeBorderSide(any, any)).thenReturn(BorderSide.none); + when(mockUtils.calculateRotationOffset(any, any)).thenReturn(Offset.zero); + when(mockUtils.getBestInitialIntervalValue(any, any, any)).thenReturn(0); + when(mockUtils.formatNumber(any, any, captureAny)).thenAnswer((inv) { + final value = inv.positionalArguments[0] as double; + return '${value.toInt()}'; + }); + Utils.changeInstance(mockUtils); + + const viewSize = Size(200, 100); + + final barGroups = [ + BarChartGroupData( + x: 0, + barRods: [ + BarChartRodData( + toY: 10, + width: 10, + color: const Color(0x00000000), + borderRadius: const BorderRadius.all(Radius.circular(0.1)), + ), + BarChartRodData( + toY: 8, + width: 11, + color: const Color(0x11111111), + borderRadius: const BorderRadius.all(Radius.circular(0.2)), + ), + BarChartRodData( + toY: 8, + width: 12, + color: const Color(0x22222222), + borderRadius: const BorderRadius.all(Radius.circular(0.3)), + ), + ], + barsSpace: 5, + ), + BarChartGroupData( + x: 1, + barRods: [ + BarChartRodData( + toY: 10, + width: 10, + borderRadius: const BorderRadius.all(Radius.circular(0.4)), + ), + BarChartRodData(toY: 8, width: 10), + ], + barsSpace: 5, + ), + BarChartGroupData( + x: 2, + barRods: [ + BarChartRodData(toY: 10, width: 10), + BarChartRodData(toY: 8, width: 10), + BarChartRodData(toY: 8, width: 10), + BarChartRodData(toY: 8, width: 10), + ], + barsSpace: 5, + ), + ]; + + final tooltipData = BarTouchTooltipData( + tooltipBorderRadius: BorderRadius.circular(8), + getTooltipColor: (group) => const Color(0xf33f33f3), + maxContentWidth: 80, + rotateAngle: 12, + tooltipBorder: const BorderSide(color: Color(0xf33f33f3), width: 2), + getTooltipItem: ( + group, + groupIndex, + rod, + rodIndex, + ) { + return BarTooltipItem( + 'helllo1', + textStyle1, + textAlign: TextAlign.right, + textDirection: TextDirection.rtl, + children: [ + const TextSpan(text: 'helllo2'), + const TextSpan(text: 'helllo3'), + ], + ); + }, + ); + + final (minY, maxY) = BarChartHelper().calculateMaxAxisValues(barGroups); + + final data = BarChartData( + groupsSpace: 10, + barGroups: barGroups, + barTouchData: BarTouchData( + touchTooltipData: tooltipData, + ), + alignment: BarChartAlignment.center, + minY: minY, + maxY: maxY, + ); + + final barChartPainter = BarChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final mockBuildContext = MockBuildContext(); + + final groupsX = data.calculateGroupsX(viewSize.width); + final barGroupsPosition = barChartPainter.calculateGroupAndBarsPosition( + viewSize, + groupsX, + barGroups, + ); + + final angles = []; + when( + mockCanvasWrapper.drawRotated( + size: anyNamed('size'), + rotationOffset: anyNamed('rotationOffset'), + drawOffset: anyNamed('drawOffset'), + angle: anyNamed('angle'), + drawCallback: anyNamed('drawCallback'), + ), + ).thenAnswer((inv) { + final callback = + inv.namedArguments[const Symbol('drawCallback')] as DrawCallback; + callback(); + angles.add(inv.namedArguments[const Symbol('angle')] as double); + }); + + barChartPainter.drawTouchTooltip( + mockBuildContext, + mockCanvasWrapper, + barGroupsPosition, + tooltipData, + barGroups[0], + 0, + barGroups[0].barRods[0], + 0, + holder, + ); + final result1 = + verify(mockCanvasWrapper.drawRRect(captureAny, captureAny)) + ..called(2); + final rrect = result1.captured[0] as RRect; + expect(rrect.blRadius, const Radius.circular(8)); + expect(rrect.width, 112); + expect(rrect.height, 90); + expect(rrect.left, -22.5); + expect(rrect.top, -106); + + final bgTooltipPaint = result1.captured[1] as Paint; + expect(bgTooltipPaint.color, isSameColorAs(const Color(0xf33f33f3))); + expect(bgTooltipPaint.style, PaintingStyle.fill); + + final rRectBorder = result1.captured[2] as RRect; + final paintBorder = result1.captured[3] as Paint; + + expect(rRectBorder.blRadius, const Radius.circular(8)); + expect(rRectBorder.width, 112); + expect(rRectBorder.height, 90); + expect(rRectBorder.left, -22.5); + expect(rRectBorder.top, -106); + expect(paintBorder.color, isSameColorAs(const Color(0xf33f33f3))); + expect(paintBorder.strokeWidth, 2); + expect(paintBorder.style, PaintingStyle.stroke); + + expect(angles.length, 1); + expect(angles[0], 12); + + final result2 = verify(mockCanvasWrapper.drawText(captureAny, captureAny)) + ..called(1); + final textPainter = result2.captured[0] as TextPainter; + expect((textPainter.text as TextSpan?)!.text, 'helllo1'); + expect((textPainter.text as TextSpan?)!.style, textStyle1); + expect(textPainter.textAlign, TextAlign.right); + expect(textPainter.textDirection, TextDirection.rtl); + expect( + (textPainter.text as TextSpan?)!.children![0], + const TextSpan(text: 'helllo2'), + ); + expect( + (textPainter.text as TextSpan?)!.children![1], + const TextSpan(text: 'helllo3'), + ); + + final drawOffset = result2.captured[1] as Offset; + expect(drawOffset, const Offset(-6.5, -98)); + }); + + test('test 2', () { + final mockUtils = MockUtils(); + when(mockUtils.getThemeAwareTextStyle(any, any)).thenReturn(textStyle1); + when(mockUtils.getEfficientInterval(any, any)).thenReturn(11); + when(mockUtils.normalizeBorderRadius(any, any)) + .thenReturn(BorderRadius.zero); + when(mockUtils.normalizeBorderSide(any, any)).thenReturn(BorderSide.none); + when(mockUtils.calculateRotationOffset(any, any)).thenReturn(Offset.zero); + when(mockUtils.getBestInitialIntervalValue(any, any, any)).thenReturn(0); + when(mockUtils.formatNumber(any, any, captureAny)).thenAnswer((inv) { + final value = inv.positionalArguments[0] as double; + return '${value.toInt()}'; + }); + Utils.changeInstance(mockUtils); + + const viewSize = Size(200, 100); + + final barGroups = [ + BarChartGroupData( + x: 0, + barRods: [ + BarChartRodData( + toY: 10, + width: 10, + color: const Color(0x00000000), + borderRadius: const BorderRadius.all(Radius.circular(0.1)), + ), + BarChartRodData( + toY: 8, + width: 11, + color: const Color(0x11111111), + borderRadius: const BorderRadius.all(Radius.circular(0.2)), + ), + BarChartRodData( + toY: 8, + width: 12, + color: const Color(0x22222222), + borderRadius: const BorderRadius.all(Radius.circular(0.3)), + ), + ], + barsSpace: 5, + ), + BarChartGroupData( + x: 1, + barRods: [ + BarChartRodData( + toY: 10, + width: 10, + borderRadius: const BorderRadius.all(Radius.circular(0.4)), + ), + BarChartRodData(toY: 8, width: 10), + ], + barsSpace: 5, + ), + BarChartGroupData( + x: 2, + barRods: [ + BarChartRodData(toY: 10, width: 10), + BarChartRodData(toY: 8, width: 10), + BarChartRodData(toY: 8, width: 10), + BarChartRodData(toY: 8, width: 10), + ], + barsSpace: 5, + ), + ]; + + final tooltipData = BarTouchTooltipData( + tooltipBorderRadius: const BorderRadius.only( + topLeft: Radius.circular(10), + topRight: Radius.circular(8), + ), + getTooltipColor: (group) => const Color(0xf33f33f3), + maxContentWidth: 80, + rotateAngle: 12, + direction: TooltipDirection.bottom, + tooltipBorder: const BorderSide(color: Color(0xf33f33f3), width: 2), + tooltipHorizontalAlignment: FLHorizontalAlignment.left, + tooltipHorizontalOffset: -1.5, + getTooltipItem: ( + group, + groupIndex, + rod, + rodIndex, + ) { + return BarTooltipItem( + 'helllo1', + textStyle1, + textAlign: TextAlign.right, + textDirection: TextDirection.rtl, + children: [ + const TextSpan(text: 'helllo2'), + const TextSpan(text: 'helllo3'), + ], + ); + }, + ); + + final (minY, maxY) = BarChartHelper().calculateMaxAxisValues(barGroups); + + final data = BarChartData( + groupsSpace: 10, + barGroups: barGroups, + barTouchData: BarTouchData( + touchTooltipData: tooltipData, + ), + alignment: BarChartAlignment.center, + minY: minY, + maxY: maxY, + ); + + final barChartPainter = BarChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final mockBuildContext = MockBuildContext(); + + final groupsX = data.calculateGroupsX(viewSize.width); + final barGroupsPosition = barChartPainter.calculateGroupAndBarsPosition( + viewSize, + groupsX, + barGroups, + ); + + final angles = []; + when( + mockCanvasWrapper.drawRotated( + size: anyNamed('size'), + rotationOffset: anyNamed('rotationOffset'), + drawOffset: anyNamed('drawOffset'), + angle: anyNamed('angle'), + drawCallback: anyNamed('drawCallback'), + ), + ).thenAnswer((inv) { + final callback = + inv.namedArguments[const Symbol('drawCallback')] as DrawCallback; + callback(); + angles.add(inv.namedArguments[const Symbol('angle')] as double); + }); + + barChartPainter.drawTouchTooltip( + mockBuildContext, + mockCanvasWrapper, + barGroupsPosition, + tooltipData, + barGroups[0], + 0, + barGroups[0].barRods[0], + 0, + holder, + ); + final result1 = + verify(mockCanvasWrapper.drawRRect(captureAny, captureAny)) + ..called(2); + final rrect = result1.captured[0] as RRect; + expect(rrect.tlRadius, const Radius.circular(10)); + expect(rrect.trRadius, const Radius.circular(8)); + expect(rrect.blRadius, Radius.zero); + expect(rrect.brRadius, Radius.zero); + expect(rrect.width, 112); + expect(rrect.height, 90); + expect(rrect.left, -80); + expect(rrect.top, 116); + + final bgTooltipPaint = result1.captured[1] as Paint; + expect(bgTooltipPaint.color, isSameColorAs(const Color(0xf33f33f3))); + expect(bgTooltipPaint.style, PaintingStyle.fill); + + final rRectBorder = result1.captured[2] as RRect; + final paintBorder = result1.captured[3] as Paint; + + expect(rRectBorder.tlRadius, const Radius.circular(10)); + expect(rRectBorder.trRadius, const Radius.circular(8)); + expect(rRectBorder.blRadius, Radius.zero); + expect(rRectBorder.brRadius, Radius.zero); + expect(rRectBorder.width, 112); + expect(rRectBorder.height, 90); + expect(rRectBorder.left, -80); + expect(rRectBorder.top, 116); + expect(paintBorder.color, isSameColorAs(const Color(0xf33f33f3))); + expect(paintBorder.strokeWidth, 2); + expect(paintBorder.style, PaintingStyle.stroke); + + expect(angles.length, 1); + expect(angles[0], 12); + + final result2 = verify(mockCanvasWrapper.drawText(captureAny, captureAny)) + ..called(1); + final textPainter = result2.captured[0] as TextPainter; + expect((textPainter.text as TextSpan?)!.text, 'helllo1'); + expect((textPainter.text as TextSpan?)!.style, textStyle1); + expect(textPainter.textAlign, TextAlign.right); + expect(textPainter.textDirection, TextDirection.rtl); + expect( + (textPainter.text as TextSpan?)!.children![0], + const TextSpan(text: 'helllo2'), + ); + expect( + (textPainter.text as TextSpan?)!.children![1], + const TextSpan(text: 'helllo3'), + ); + + final drawOffset = result2.captured[1] as Offset; + expect(drawOffset, const Offset(-64, 124)); + }); + + test('test 3', () { + final mockUtils = MockUtils(); + when(mockUtils.getThemeAwareTextStyle(any, any)).thenReturn(textStyle1); + when(mockUtils.getEfficientInterval(any, any)).thenReturn(11); + when(mockUtils.normalizeBorderRadius(any, any)) + .thenReturn(BorderRadius.zero); + when(mockUtils.normalizeBorderSide(any, any)).thenReturn(BorderSide.none); + when(mockUtils.calculateRotationOffset(any, any)).thenReturn(Offset.zero); + when(mockUtils.getBestInitialIntervalValue(any, any, any)).thenReturn(0); + when(mockUtils.formatNumber(any, any, captureAny)).thenAnswer((inv) { + final value = inv.positionalArguments[0] as double; + return '${value.toInt()}'; + }); + Utils.changeInstance(mockUtils); + + const viewSize = Size(200, 100); + + final barGroups = [ + BarChartGroupData( + x: 0, + barRods: [ + BarChartRodData( + toY: 10, + width: 10, + color: const Color(0x00000000), + borderRadius: const BorderRadius.all(Radius.circular(0.1)), + ), + BarChartRodData( + toY: -10, + width: 10, + color: const Color(0x11111111), + borderRadius: const BorderRadius.all(Radius.circular(0.2)), + ), + ], + ), + ]; + + final tooltipData = BarTouchTooltipData( + tooltipBorderRadius: BorderRadius.circular(8), + getTooltipColor: (group) => const Color(0xf33f33f3), + maxContentWidth: 8000, + rotateAngle: 12, + fitInsideHorizontally: true, + fitInsideVertically: true, + direction: TooltipDirection.top, + tooltipHorizontalAlignment: FLHorizontalAlignment.right, + tooltipBorder: const BorderSide(color: Color(0xf33f33f3), width: 2), + getTooltipItem: ( + group, + groupIndex, + rod, + rodIndex, + ) { + return BarTooltipItem( + 'helllo1asdfasdfasdfasdfasdfasdfhelllo1asdfasdfasdfasd' + 'fasdfasdfhelllo1asdfasdfasdfasdfasdfasdfhelllo1asdf' + 'asdfasdfasdfasdfasdfhelllo1asdfasdfasdfasdfasdfasdfh' + 'elllo1asdfasdfasdfasdfasdfasdf', + textStyle1, + textAlign: TextAlign.right, + textDirection: TextDirection.rtl, + children: List.generate( + 500, + (index) => const TextSpan(text: '\nhelllo3'), + ), + ); + }, + ); + + final (minY, maxY) = BarChartHelper().calculateMaxAxisValues(barGroups); + + final data = BarChartData( + groupsSpace: 10, + barGroups: barGroups, + barTouchData: BarTouchData( + touchTooltipData: tooltipData, + ), + alignment: BarChartAlignment.center, + minY: minY, + maxY: maxY, + ); + + final barChartPainter = BarChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final mockBuildContext = MockBuildContext(); + + final groupsX = data.calculateGroupsX(viewSize.width); + final barGroupsPosition = barChartPainter.calculateGroupAndBarsPosition( + viewSize, + groupsX, + barGroups, + ); + + final angles = []; + when( + mockCanvasWrapper.drawRotated( + size: anyNamed('size'), + rotationOffset: anyNamed('rotationOffset'), + drawOffset: anyNamed('drawOffset'), + angle: anyNamed('angle'), + drawCallback: anyNamed('drawCallback'), + ), + ).thenAnswer((inv) { + final callback = + inv.namedArguments[const Symbol('drawCallback')] as DrawCallback; + callback(); + angles.add(inv.namedArguments[const Symbol('angle')] as double); + }); + + barChartPainter.drawTouchTooltip( + mockBuildContext, + mockCanvasWrapper, + barGroupsPosition, + tooltipData, + barGroups[0], + 0, + barGroups[0].barRods[1], + 1, + holder, + ); + final result1 = + verify(mockCanvasWrapper.drawRRect(captureAny, captureAny)) + ..called(2); + final rrect = result1.captured[0] as RRect; + expect(rrect.blRadius, const Radius.circular(8)); + expect(rrect.width, 2636); + expect(rrect.height, 7034.0); + expect(rrect.left, -2436); + expect(rrect.top, -6934.0); + + final bgTooltipPaint = result1.captured[1] as Paint; + expect(bgTooltipPaint.color, isSameColorAs(const Color(0xf33f33f3))); + expect(bgTooltipPaint.style, PaintingStyle.fill); + + final rRectBorder = result1.captured[2] as RRect; + final paintBorder = result1.captured[3] as Paint; + + expect(rRectBorder.blRadius, const Radius.circular(8)); + expect(rRectBorder.width, 2636); + expect(rRectBorder.height, 7034.0); + expect(rRectBorder.left, -2436); + expect(rRectBorder.top, -6934.0); + expect(paintBorder.color, isSameColorAs(const Color(0xf33f33f3))); + expect(paintBorder.strokeWidth, 2); + expect(paintBorder.style, PaintingStyle.stroke); + + expect(angles.length, 1); + expect(angles[0], 12); + + final result2 = verify(mockCanvasWrapper.drawText(captureAny, captureAny)) + ..called(1); + + final drawOffset = result2.captured[1] as Offset; + expect(drawOffset, const Offset(-2420, -6926)); + }); + }); + + group('drawStackItemBorderStroke()', () { + test('test 1', () { + const viewSize = Size(200, 100); + + final rodStackItems = [ + BarChartRodStackItem( + 0, + 3, + const Color(0x11111110), + const BorderSide(color: Color(0x11111111)), + ), + BarChartRodStackItem( + 3, + 8, + const Color(0x22222220), + const BorderSide(color: Color(0x22222221), width: 2), + ), + BarChartRodStackItem( + 8, + 10, + const Color(0x33333330), + const BorderSide(color: Color(0x33333331), width: 3), + ), + ]; + + final barRod = BarChartRodData( + toY: 10, + width: 10, + color: const Color(0x00000000), + borderRadius: const BorderRadius.all(Radius.circular(0.1)), + rodStackItems: rodStackItems, + ); + + final barGroups = [ + BarChartGroupData(x: 0, barRods: [barRod], barsSpace: 5), + ]; + + final (minY, maxY) = BarChartHelper().calculateMaxAxisValues(barGroups); + + final data = BarChartData( + groupsSpace: 10, + barGroups: barGroups, + barTouchData: const BarTouchData(), + minY: minY, + maxY: maxY, + ); + + final barChartPainter = BarChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final results = >[]; + when( + mockCanvasWrapper.drawRRect( + captureAny, + captureAny, + ), + ).thenAnswer((inv) { + final rrect = inv.positionalArguments[0] as RRect; + final paint = inv.positionalArguments[1] as Paint; + results.add({ + 'rRect': rrect, + 'paint.color': paint.color, + 'paint.strokeWidth': paint.strokeWidth, + }); + }); + + barChartPainter + ..drawStackItemBorderStroke( + mockCanvasWrapper, + rodStackItems[0], + 0, + 3, + barRod.width, + RRect.fromLTRBAndCorners( + 0, + 0, + 10, + 100, + bottomRight: const Radius.circular(12), + ), + viewSize, + holder, + ) + ..drawStackItemBorderStroke( + mockCanvasWrapper, + rodStackItems[1], + 1, + 3, + barRod.width, + RRect.fromLTRBAndCorners( + 0, + 0, + 10, + 100, + bottomRight: const Radius.circular(12), + ), + viewSize, + holder, + ) + ..drawStackItemBorderStroke( + mockCanvasWrapper, + rodStackItems[2], + 2, + 3, + barRod.width, + RRect.fromLTRBAndCorners( + 0, + 0, + 10, + 100, + bottomRight: const Radius.circular(12), + ), + viewSize, + holder, + ); + + expect(results.length, 3); + + expect( + results[0]['rRect'], + RRect.fromLTRBAndCorners( + 0, + 70, + 10, + 100, + bottomRight: const Radius.circular(12), + ), + ); + expect( + results[0]['paint.color'], + isSameColorAs(const Color(0x11111111)), + ); + expect(results[0]['paint.strokeWidth'], 1.0); + + expect( + results[1]['rRect'], + RRect.fromLTRBAndCorners( + 0, + 20, + 10, + 70, + ), + ); + expect( + results[1]['paint.color'], + isSameColorAs(const Color(0x22222221)), + ); + expect(results[1]['paint.strokeWidth'], 2.0); + + expect( + results[2]['rRect'], + RRect.fromLTRBAndCorners( + 0, + 0, + 10, + 20, + ), + ); + expect( + results[2]['paint.color'], + isSameColorAs(const Color(0x33333331)), + ); + expect(results[2]['paint.strokeWidth'], 3.0); + }); + }); + + group('handleTouch()', () { + test('test 1', () { + const viewSize = Size(200, 100); + + final barGroups = [ + BarChartGroupData( + x: 0, + barRods: [ + BarChartRodData( + toY: 10, + width: 10, + color: const Color(0x00000000), + borderRadius: const BorderRadius.all(Radius.circular(0.1)), + ), + BarChartRodData( + toY: 8, + width: 11, + color: const Color(0x11111111), + borderRadius: const BorderRadius.all(Radius.circular(0.2)), + ), + BarChartRodData( + toY: 8, + width: 12, + color: const Color(0x22222222), + borderRadius: const BorderRadius.all(Radius.circular(0.3)), + ), + ], + barsSpace: 5, + ), + BarChartGroupData( + x: 1, + barRods: [ + BarChartRodData( + toY: 10, + width: 10, + borderRadius: const BorderRadius.all(Radius.circular(0.4)), + ), + BarChartRodData(toY: 8, width: 10), + ], + barsSpace: 5, + ), + BarChartGroupData( + x: 2, + barRods: [ + BarChartRodData(toY: 10, width: 10), + BarChartRodData(toY: 8, width: 10), + BarChartRodData(toY: 8, width: 10), + BarChartRodData(toY: 8, width: 10), + ], + barsSpace: 5, + ), + ]; + + final (minY, maxY) = BarChartHelper().calculateMaxAxisValues(barGroups); + + final data = BarChartData( + barGroups: barGroups, + titlesData: const FlTitlesData(show: false), + alignment: BarChartAlignment.center, + groupsSpace: 10, + barTouchData: const BarTouchData( + handleBuiltInTouches: true, + touchExtraThreshold: EdgeInsets.all(1), + ), + minY: minY, + maxY: maxY, + ); + + final painter = BarChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + + expect(painter.handleTouch(const Offset(10, 10), viewSize, holder), null); + expect( + painter.handleTouch(const Offset(27.49, 10), viewSize, holder), + null, + ); + + // Group 0 + // 28.5, 0.0, 38.5, 100.0 + // 43.5, 20.0, 54.5, 100.0 + // 59.5, 20.0, 71.5, 100.0 + final result1 = + painter.handleTouch(const Offset(27.5, 10), viewSize, holder); + expect(result1!.touchedBarGroupIndex, 0); + expect(result1.touchedRodDataIndex, 0); + + final result11 = + painter.handleTouch(const Offset(39.5, 10), viewSize, holder); + expect(result11!.touchedBarGroupIndex, 0); + expect(result11.touchedRodDataIndex, 0); + + expect( + painter.handleTouch(const Offset(39.51, 10), viewSize, holder), + null, + ); + + // Group 1 + // 81.5, 0.0, 91.5, 100.0 + // 96.5, 20.0, 106.5, 100.0 + expect( + painter.handleTouch(const Offset(100, 18.99), viewSize, holder), + null, + ); + final result2 = + painter.handleTouch(const Offset(100, 19), viewSize, holder); + expect(result2!.touchedBarGroupIndex, 1); + expect(result2.touchedRodDataIndex, 1); + + // Group 2 + // 116.5, 0.0, 126.5, 100.0 + // 131.5, 20.0, 141.5, 100.0 + // 146.5, 20.0, 156.5, 100.0 + // 161.5, 20.0, 171.5, 100.0 + expect( + painter.handleTouch(const Offset(165, 101.1), viewSize, holder), + null, + ); + final result3 = + painter.handleTouch(const Offset(165, 101), viewSize, holder); + expect(result3!.touchedBarGroupIndex, 2); + expect(result3.touchedRodDataIndex, 3); + }); + + test('test 2', () { + const viewSize = Size(200, 100); + + final barGroups = [ + BarChartGroupData( + x: 0, + barRods: [ + BarChartRodData( + toY: 10, + width: 10, + color: const Color(0x00000000), + borderRadius: const BorderRadius.all(Radius.circular(0.1)), + rodStackItems: [ + BarChartRodStackItem(0, 5, const Color(0xFF0F0F0F)), + ], + ), + ], + barsSpace: 5, + ), + BarChartGroupData( + x: 1, + barRods: [ + BarChartRodData( + toY: -10, + width: 10, + borderRadius: const BorderRadius.all(Radius.circular(0.4)), + ), + ], + barsSpace: 5, + ), + ]; + + final (minY, maxY) = BarChartHelper().calculateMaxAxisValues(barGroups); + + final data = BarChartData( + barGroups: barGroups, + titlesData: const FlTitlesData(show: false), + alignment: BarChartAlignment.center, + groupsSpace: 10, + barTouchData: const BarTouchData( + handleBuiltInTouches: true, + touchExtraThreshold: EdgeInsets.all(1), + ), + minY: minY, + maxY: maxY, + ); + + final painter = BarChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + + expect( + painter.handleTouch(const Offset(134, 48.6), viewSize, holder), + null, + ); + expect( + painter.handleTouch(const Offset(111.2, 31.1), viewSize, holder), + null, + ); + + expect( + painter.handleTouch(const Offset(103.2, 74.8), viewSize, holder), + null, + ); + expect( + painter.handleTouch(const Offset(100.4, 21.2), viewSize, holder), + null, + ); + expect( + painter.handleTouch(const Offset(80.1, 22), viewSize, holder), + null, + ); + + final result1 = + painter.handleTouch(const Offset(89, 38.5), viewSize, holder); + expect(result1!.touchedBarGroupIndex, 0); + expect(result1.touchedRodDataIndex, 0); + expect(result1.touchedStackItemIndex, 0); + + final result2 = + painter.handleTouch(const Offset(88.8, 16.5), viewSize, holder); + expect(result2!.touchedBarGroupIndex, 0); + expect(result2.touchedRodDataIndex, 0); + expect(result2.touchedStackItemIndex, -1); + }); + + test('test 3', () { + const viewSize = Size(200, 100); + + final barGroups = [ + BarChartGroupData( + x: 1, + barRods: [ + BarChartRodData( + toY: 5, + backDrawRodData: BackgroundBarChartRodData( + show: true, + fromY: -5, + toY: 5, + ), + ), + ], + ), + BarChartGroupData( + x: 1, + barRods: [ + BarChartRodData( + toY: -6, + backDrawRodData: BackgroundBarChartRodData( + show: true, + fromY: 5, + toY: -6, + ), + ), + ], + ), + ]; + + final data = BarChartData( + barGroups: barGroups, + titlesData: const FlTitlesData(show: false), + alignment: BarChartAlignment.start, + groupsSpace: 10, + minY: -10, + maxY: 15, + barTouchData: const BarTouchData( + enabled: true, + handleBuiltInTouches: true, + allowTouchBarBackDraw: true, + touchExtraThreshold: EdgeInsets.all(1), + ), + ); + + final painter = BarChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + + final result1 = + painter.handleTouch(const Offset(4, 60), viewSize, holder); + expect(result1!.touchedBarGroupIndex, 0); + expect(result1.touchedRodDataIndex, 0); + + // tap below the positive bar + final result11 = + painter.handleTouch(const Offset(4, 61.1), viewSize, holder); + expect(result11!.touchedBarGroupIndex, 0); + expect(result11.touchedRodDataIndex, 0); + + final result2 = + painter.handleTouch(const Offset(22, 60), viewSize, holder); + expect(result2!.touchedBarGroupIndex, 1); + expect(result2.touchedRodDataIndex, 0); + + final result22 = + painter.handleTouch(const Offset(22, 58.9), viewSize, holder); + expect(result22!.touchedBarGroupIndex, 1); + expect(result22.touchedRodDataIndex, 0); + }); + + test( + 'returns null when chart virtual rect is provided and touch is outside ' + 'of canvas', + () { + const viewSize = Size(50, 50); + + final barGroups = [ + BarChartGroupData( + x: 1, + barRods: [ + BarChartRodData( + toY: 5, + backDrawRodData: BackgroundBarChartRodData( + show: true, + fromY: -5, + toY: 5, + ), + ), + ], + ), + BarChartGroupData( + x: 1, + barRods: [ + BarChartRodData( + toY: -6, + backDrawRodData: BackgroundBarChartRodData( + show: true, + fromY: 5, + toY: -6, + ), + ), + ], + ), + ]; + + final data = BarChartData( + barGroups: barGroups, + titlesData: const FlTitlesData(show: false), + alignment: BarChartAlignment.start, + groupsSpace: 10, + minY: -10, + maxY: 15, + barTouchData: const BarTouchData( + enabled: true, + handleBuiltInTouches: true, + allowTouchBarBackDraw: true, + touchExtraThreshold: EdgeInsets.all(1), + ), + ); + + final painter = BarChartPainter(); + final holder = PaintHolder( + data, + data, + TextScaler.noScaling, + Offset.zero & const Size(50, 50), + ); + + final result1 = + painter.handleTouch(const Offset(4, 60), viewSize, holder); + expect(result1, null); + }, + ); + + test( + 'returns result when chart virtual rect is provided and touch is inside ' + 'of canvas', + () { + const viewSize = Size(50, 50); + + final barGroups = [ + BarChartGroupData( + x: 1, + barRods: [ + BarChartRodData( + toY: 5, + backDrawRodData: BackgroundBarChartRodData( + show: true, + fromY: -5, + toY: 5, + ), + ), + ], + ), + BarChartGroupData( + x: 1, + barRods: [ + BarChartRodData( + toY: -6, + backDrawRodData: BackgroundBarChartRodData( + show: true, + fromY: 5, + toY: -6, + ), + ), + ], + ), + ]; + + final data = BarChartData( + barGroups: barGroups, + titlesData: const FlTitlesData(show: false), + alignment: BarChartAlignment.start, + groupsSpace: 10, + minY: -10, + maxY: 15, + barTouchData: const BarTouchData( + enabled: true, + handleBuiltInTouches: true, + allowTouchBarBackDraw: true, + touchExtraThreshold: EdgeInsets.all(1), + ), + ); + + final painter = BarChartPainter(); + final holder = PaintHolder( + data, + data, + TextScaler.noScaling, + Offset.zero & const Size(50, 50), + ); + + final result1 = + painter.handleTouch(const Offset(4, 30), viewSize, holder); + expect(result1!.touchedBarGroupIndex, 0); + expect(result1.touchedRodDataIndex, 0); + }, + ); + }); + + group('drawExtraLines()', () { + test( + 'should not draw lines when constructor is called with empty ExtraLinesData object', + () { + const viewSize = Size(400, 400); + final data = BarChartData( + barGroups: [ + BarChartGroupData( + x: 1, + barRods: [ + BarChartRodData(fromY: 1, toY: 10), + BarChartRodData(fromY: 2, toY: 10), + ], + showingTooltipIndicators: [ + 1, + 2, + ], + ), + BarChartGroupData( + x: 2, + barRods: [ + BarChartRodData(fromY: 3, toY: 10), + BarChartRodData(fromY: 4, toY: 10), + ], + ), + ], + extraLinesData: const ExtraLinesData(), + ); + + final barChartPainter = BarChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockBuildContext = MockBuildContext(); + final mockCanvasWrapper = MockCanvasWrapper(); + + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + barChartPainter.drawExtraLines( + mockBuildContext, + mockCanvasWrapper, + holder, + ); + + verifyNever( + mockCanvasWrapper.drawDashedLine( + any, + any, + any, + any, + ), + ); + }); + + test('should not draw lines when constructor is not passed extraLinesData', + () { + const viewSize = Size(400, 400); + final data = BarChartData( + barGroups: [ + BarChartGroupData( + x: 1, + barRods: [ + BarChartRodData(fromY: 1, toY: 10), + BarChartRodData(fromY: 2, toY: 10), + ], + showingTooltipIndicators: [ + 1, + 2, + ], + ), + BarChartGroupData( + x: 2, + barRods: [ + BarChartRodData(fromY: 3, toY: 10), + BarChartRodData(fromY: 4, toY: 10), + ], + ), + ], + ); + + final barChartPainter = BarChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockBuildContext = MockBuildContext(); + final mockCanvasWrapper = MockCanvasWrapper(); + + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + barChartPainter.drawExtraLines( + mockBuildContext, + mockCanvasWrapper, + holder, + ); + + verifyNever( + mockCanvasWrapper.drawDashedLine( + any, + any, + any, + any, + ), + ); + }); + + test('bar chart should not paint vertical lines', () { + final utilsMainInstance = Utils(); + const viewSize = Size(400, 400); + + final barGroups = [ + BarChartGroupData( + x: 1, + barRods: [ + BarChartRodData(fromY: 1, toY: 10), + BarChartRodData(fromY: 2, toY: 10), + ], + showingTooltipIndicators: [ + 1, + 2, + ], + ), + BarChartGroupData( + x: 2, + barRods: [ + BarChartRodData(fromY: 3, toY: 10), + BarChartRodData(fromY: 4, toY: 10), + ], + ), + ]; + + final (minY, maxY) = BarChartHelper().calculateMaxAxisValues(barGroups); + + final data = BarChartData( + minY: minY, + maxY: maxY, + barGroups: barGroups, + extraLinesData: ExtraLinesData( + verticalLines: [verticalLine1], + ), + ); + + final barChartPainter = BarChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.getThemeAwareTextStyle(any, any)) + .thenAnswer((realInvocation) => textStyle1); + when(mockUtils.calculateRotationOffset(any, any)) + .thenAnswer((realInvocation) => Offset.zero); + when(mockUtils.convertRadiusToSigma(any)) + .thenAnswer((realInvocation) => 4.0); + when(mockUtils.getEfficientInterval(any, any)) + .thenAnswer((realInvocation) => 1.0); + when(mockUtils.getBestInitialIntervalValue(any, any, any)) + .thenAnswer((realInvocation) => 1.0); + when(mockUtils.normalizeBorderRadius(any, any)) + .thenAnswer((realInvocation) => BorderRadius.zero); + when(mockUtils.normalizeBorderSide(any, any)).thenAnswer( + (realInvocation) => const BorderSide(color: MockData.color0), + ); + + final mockBuildContext = MockBuildContext(); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + barChartPainter.paint( + mockBuildContext, + mockCanvasWrapper, + holder, + ); + + verifyNever( + mockCanvasWrapper.drawDashedLine( + any, + any, + argThat( + const TypeMatcher().having( + (p0) => p0.color, + 'colors match', + isSameColorAs(Colors.red), + ), + ), + holder.data.extraLinesData.verticalLines[0].dashArray, + ), + ); + + Utils.changeInstance(utilsMainInstance); + }); + + test( + 'should not paint horizontal line if Y value is greater or less than Y axis', + () { + final utilsMainInstance = Utils(); + const viewSize = Size(400, 400); + + final horizontalLine1 = HorizontalLine( + y: 10.1, + color: Colors.red, + dashArray: [0, 1], + ); + + final horizontalLine2 = HorizontalLine( + y: -10.1, + color: Colors.red, + dashArray: [0, 1], + ); + + final data = BarChartData( + minY: -10, + maxY: 10, + barGroups: [ + BarChartGroupData( + x: 1, + barRods: [ + BarChartRodData(fromY: 1, toY: 10), + BarChartRodData(fromY: 2, toY: 10), + ], + showingTooltipIndicators: [ + 1, + 2, + ], + ), + BarChartGroupData( + x: 2, + barRods: [ + BarChartRodData(fromY: 3, toY: 10), + BarChartRodData(fromY: 4, toY: 10), + ], + ), + ], + extraLinesData: ExtraLinesData( + horizontalLines: [horizontalLine1, horizontalLine2], + ), + ); + + final barChartPainter = BarChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.getThemeAwareTextStyle(any, any)) + .thenAnswer((realInvocation) => textStyle1); + when(mockUtils.calculateRotationOffset(any, any)) + .thenAnswer((realInvocation) => Offset.zero); + when(mockUtils.convertRadiusToSigma(any)) + .thenAnswer((realInvocation) => 4.0); + when(mockUtils.getEfficientInterval(any, any)) + .thenAnswer((realInvocation) => 1.0); + when(mockUtils.getBestInitialIntervalValue(any, any, any)) + .thenAnswer((realInvocation) => 1.0); + when(mockUtils.normalizeBorderRadius(any, any)) + .thenAnswer((realInvocation) => BorderRadius.zero); + when(mockUtils.normalizeBorderSide(any, any)).thenAnswer( + (realInvocation) => const BorderSide(color: MockData.color0), + ); + + final mockBuildContext = MockBuildContext(); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + barChartPainter.paint( + mockBuildContext, + mockCanvasWrapper, + holder, + ); + + verifyNever( + mockCanvasWrapper.drawDashedLine( + any, + any, + argThat( + const TypeMatcher().having( + (p0) => p0.color, + 'colors match', + isSameColorAs(Colors.red), + ), + ), + holder.data.extraLinesData.horizontalLines[0].dashArray, + ), + ); + + Utils.changeInstance(utilsMainInstance); + }); + + test('should paint horizontal lines', () { + final horizontalLine = HorizontalLine( + y: 2.5, + strokeWidth: 90, + color: Colors.cyanAccent, + dashArray: [100, 20], + ); + final horizontalLine1 = HorizontalLine( + y: 0.2, + strokeWidth: 100, + color: Colors.cyanAccent, + dashArray: [100, 20], + ); + final utilsMainInstance = Utils(); + const viewSize = Size(400, 400); + + final barGroups = [ + BarChartGroupData( + x: 1, + barRods: [ + BarChartRodData(fromY: 1, toY: 10), + BarChartRodData(fromY: 2, toY: 10), + ], + showingTooltipIndicators: [ + 1, + 2, + ], + ), + BarChartGroupData( + x: 2, + barRods: [ + BarChartRodData(fromY: 3, toY: 10), + BarChartRodData(fromY: 4, toY: 10), + ], + ), + ]; + + final (minY, maxY) = BarChartHelper().calculateMaxAxisValues(barGroups); + + final data = BarChartData( + minY: minY, + maxY: maxY, + barGroups: barGroups, + extraLinesData: ExtraLinesData( + horizontalLines: [horizontalLine, horizontalLine1], + ), + ); + + final barChartPainter = BarChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.getThemeAwareTextStyle(any, any)) + .thenAnswer((realInvocation) => textStyle1); + when(mockUtils.calculateRotationOffset(any, any)) + .thenAnswer((realInvocation) => Offset.zero); + when(mockUtils.convertRadiusToSigma(any)) + .thenAnswer((realInvocation) => 4.0); + when(mockUtils.getEfficientInterval(any, any)) + .thenAnswer((realInvocation) => 1.0); + when(mockUtils.getBestInitialIntervalValue(any, any, any)) + .thenAnswer((realInvocation) => 1.0); + when(mockUtils.normalizeBorderRadius(any, any)) + .thenAnswer((realInvocation) => BorderRadius.zero); + when(mockUtils.normalizeBorderSide(any, any)).thenAnswer( + (realInvocation) => const BorderSide(color: MockData.color0), + ); + + final mockBuildContext = MockBuildContext(); + final mockCanvasWrapper = MockCanvasWrapper(); + + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final results = >[]; + + when( + mockCanvasWrapper.drawDashedLine( + any, + any, + captureThat( + const TypeMatcher().having( + (p0) => p0.color, + 'colors match', + isSameColorAs(Colors.cyanAccent), + ), + ), + [100, 20], + ), + ).thenAnswer((inv) { + results.add({ + 'from': inv.positionalArguments[0] as Offset, + 'to': inv.positionalArguments[1] as Offset, + 'paint_color': (inv.positionalArguments[2] as Paint).color, + 'paint_stroke_width': + (inv.positionalArguments[2] as Paint).strokeWidth, + }); + }); + + barChartPainter.paint( + mockBuildContext, + mockCanvasWrapper, + holder, + ); + + expect(results.length, 1); + + expect(results[0]['paint_color'], isSameColorAs(Colors.cyanAccent)); + expect(results[0]['paint_stroke_width'], 90); + + Utils.changeInstance(utilsMainInstance); + }); + + test('should draw extra horizontal lines under chart', () { + const viewSize = Size(100, 100); + final data = BarChartData( + minY: -10, + maxY: 10, + barGroups: [ + BarChartGroupData( + x: 1, + barRods: [ + BarChartRodData(fromY: 1, toY: 10), + BarChartRodData(fromY: 2, toY: 10), + ], + ), + ], + titlesData: const FlTitlesData(show: false), + extraLinesData: ExtraLinesData( + horizontalLines: [ + HorizontalLine( + y: -9.9, + color: Colors.cyanAccent, + dashArray: [12, 22], + ), + HorizontalLine( + y: -.5, + color: Colors.cyanAccent, + dashArray: [12, 22], + ), + ], + extraLinesOnTop: false, + ), + ); + + final barChartPainter = BarChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final mockBuildContext = MockBuildContext(); + + barChartPainter.paint( + mockBuildContext, + mockCanvasWrapper, + holder, + ); + + verify( + mockCanvasWrapper.drawDashedLine( + any, + any, + argThat( + const TypeMatcher().having( + (p0) => p0.color, + 'colors match', + isSameColorAs(Colors.cyanAccent), + ), + ), + holder.data.extraLinesData.horizontalLines[0].dashArray, + ), + ).called(2); + }); + }); +} diff --git a/test/chart/bar_chart/bar_chart_painter_test.mocks.dart b/test/chart/bar_chart/bar_chart_painter_test.mocks.dart new file mode 100644 index 0000000..5ee28d8 --- /dev/null +++ b/test/chart/bar_chart/bar_chart_painter_test.mocks.dart @@ -0,0 +1,924 @@ +// Mocks generated by Mockito 5.4.5 from annotations +// in fl_chart/test/chart/bar_chart/bar_chart_painter_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:typed_data' as _i5; +import 'dart:ui' as _i2; + +import 'package:fl_chart/fl_chart.dart' as _i7; +import 'package:fl_chart/src/utils/canvas_wrapper.dart' as _i6; +import 'package:fl_chart/src/utils/utils.dart' as _i8; +import 'package:flutter/cupertino.dart' as _i3; +import 'package:flutter/foundation.dart' as _i4; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i9; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeRect_0 extends _i1.SmartFake implements _i2.Rect { + _FakeRect_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeCanvas_1 extends _i1.SmartFake implements _i2.Canvas { + _FakeCanvas_1(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeSize_2 extends _i1.SmartFake implements _i2.Size { + _FakeSize_2(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeWidget_3 extends _i1.SmartFake implements _i3.Widget { + _FakeWidget_3(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeInheritedWidget_4 extends _i1.SmartFake + implements _i3.InheritedWidget { + _FakeInheritedWidget_4(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeDiagnosticsNode_5 extends _i1.SmartFake + implements _i3.DiagnosticsNode { + _FakeDiagnosticsNode_5(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({ + _i4.TextTreeConfiguration? parentConfiguration, + _i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info, + }) => super.toString(); +} + +class _FakeOffset_6 extends _i1.SmartFake implements _i2.Offset { + _FakeOffset_6(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeBorderSide_7 extends _i1.SmartFake implements _i3.BorderSide { + _FakeBorderSide_7(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeTextStyle_8 extends _i1.SmartFake implements _i3.TextStyle { + _FakeTextStyle_8(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +/// A class which mocks [Canvas]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCanvas extends _i1.Mock implements _i2.Canvas { + MockCanvas() { + _i1.throwOnMissingStub(this); + } + + @override + void save() => super.noSuchMethod( + Invocation.method(#save, []), + returnValueForMissingStub: null, + ); + + @override + void saveLayer(_i2.Rect? bounds, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#saveLayer, [bounds, paint]), + returnValueForMissingStub: null, + ); + + @override + void restore() => super.noSuchMethod( + Invocation.method(#restore, []), + returnValueForMissingStub: null, + ); + + @override + void restoreToCount(int? count) => super.noSuchMethod( + Invocation.method(#restoreToCount, [count]), + returnValueForMissingStub: null, + ); + + @override + int getSaveCount() => + (super.noSuchMethod(Invocation.method(#getSaveCount, []), returnValue: 0) + as int); + + @override + void translate(double? dx, double? dy) => super.noSuchMethod( + Invocation.method(#translate, [dx, dy]), + returnValueForMissingStub: null, + ); + + @override + void scale(double? sx, [double? sy]) => super.noSuchMethod( + Invocation.method(#scale, [sx, sy]), + returnValueForMissingStub: null, + ); + + @override + void rotate(double? radians) => super.noSuchMethod( + Invocation.method(#rotate, [radians]), + returnValueForMissingStub: null, + ); + + @override + void skew(double? sx, double? sy) => super.noSuchMethod( + Invocation.method(#skew, [sx, sy]), + returnValueForMissingStub: null, + ); + + @override + void transform(_i5.Float64List? matrix4) => super.noSuchMethod( + Invocation.method(#transform, [matrix4]), + returnValueForMissingStub: null, + ); + + @override + _i5.Float64List getTransform() => + (super.noSuchMethod( + Invocation.method(#getTransform, []), + returnValue: _i5.Float64List(0), + ) + as _i5.Float64List); + + @override + void clipRect( + _i2.Rect? rect, { + _i2.ClipOp? clipOp = _i2.ClipOp.intersect, + bool? doAntiAlias = true, + }) => super.noSuchMethod( + Invocation.method( + #clipRect, + [rect], + {#clipOp: clipOp, #doAntiAlias: doAntiAlias}, + ), + returnValueForMissingStub: null, + ); + + @override + void clipRRect(_i2.RRect? rrect, {bool? doAntiAlias = true}) => + super.noSuchMethod( + Invocation.method(#clipRRect, [rrect], {#doAntiAlias: doAntiAlias}), + returnValueForMissingStub: null, + ); + + @override + void clipPath(_i2.Path? path, {bool? doAntiAlias = true}) => + super.noSuchMethod( + Invocation.method(#clipPath, [path], {#doAntiAlias: doAntiAlias}), + returnValueForMissingStub: null, + ); + + @override + _i2.Rect getLocalClipBounds() => + (super.noSuchMethod( + Invocation.method(#getLocalClipBounds, []), + returnValue: _FakeRect_0( + this, + Invocation.method(#getLocalClipBounds, []), + ), + ) + as _i2.Rect); + + @override + _i2.Rect getDestinationClipBounds() => + (super.noSuchMethod( + Invocation.method(#getDestinationClipBounds, []), + returnValue: _FakeRect_0( + this, + Invocation.method(#getDestinationClipBounds, []), + ), + ) + as _i2.Rect); + + @override + void drawColor(_i2.Color? color, _i2.BlendMode? blendMode) => + super.noSuchMethod( + Invocation.method(#drawColor, [color, blendMode]), + returnValueForMissingStub: null, + ); + + @override + void drawLine(_i2.Offset? p1, _i2.Offset? p2, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawLine, [p1, p2, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawPaint(_i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawPaint, [paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRect(_i2.Rect? rect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRect, [rect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRRect(_i2.RRect? rrect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRRect, [rrect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawDRRect(_i2.RRect? outer, _i2.RRect? inner, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawDRRect, [outer, inner, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawOval(_i2.Rect? rect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawOval, [rect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawCircle(_i2.Offset? c, double? radius, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawCircle, [c, radius, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawArc( + _i2.Rect? rect, + double? startAngle, + double? sweepAngle, + bool? useCenter, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawArc, [ + rect, + startAngle, + sweepAngle, + useCenter, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawPath(_i2.Path? path, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawPath, [path, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImage(_i2.Image? image, _i2.Offset? offset, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawImage, [image, offset, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImageRect( + _i2.Image? image, + _i2.Rect? src, + _i2.Rect? dst, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawImageRect, [image, src, dst, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImageNine( + _i2.Image? image, + _i2.Rect? center, + _i2.Rect? dst, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawImageNine, [image, center, dst, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawPicture(_i2.Picture? picture) => super.noSuchMethod( + Invocation.method(#drawPicture, [picture]), + returnValueForMissingStub: null, + ); + + @override + void drawParagraph(_i2.Paragraph? paragraph, _i2.Offset? offset) => + super.noSuchMethod( + Invocation.method(#drawParagraph, [paragraph, offset]), + returnValueForMissingStub: null, + ); + + @override + void drawPoints( + _i2.PointMode? pointMode, + List<_i2.Offset>? points, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawPoints, [pointMode, points, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRawPoints( + _i2.PointMode? pointMode, + _i5.Float32List? points, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawRawPoints, [pointMode, points, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawVertices( + _i2.Vertices? vertices, + _i2.BlendMode? blendMode, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawVertices, [vertices, blendMode, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawAtlas( + _i2.Image? atlas, + List<_i2.RSTransform>? transforms, + List<_i2.Rect>? rects, + List<_i2.Color>? colors, + _i2.BlendMode? blendMode, + _i2.Rect? cullRect, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawAtlas, [ + atlas, + transforms, + rects, + colors, + blendMode, + cullRect, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawRawAtlas( + _i2.Image? atlas, + _i5.Float32List? rstTransforms, + _i5.Float32List? rects, + _i5.Int32List? colors, + _i2.BlendMode? blendMode, + _i2.Rect? cullRect, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawRawAtlas, [ + atlas, + rstTransforms, + rects, + colors, + blendMode, + cullRect, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawShadow( + _i2.Path? path, + _i2.Color? color, + double? elevation, + bool? transparentOccluder, + ) => super.noSuchMethod( + Invocation.method(#drawShadow, [ + path, + color, + elevation, + transparentOccluder, + ]), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [CanvasWrapper]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCanvasWrapper extends _i1.Mock implements _i6.CanvasWrapper { + MockCanvasWrapper() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.Canvas get canvas => + (super.noSuchMethod( + Invocation.getter(#canvas), + returnValue: _FakeCanvas_1(this, Invocation.getter(#canvas)), + ) + as _i2.Canvas); + + @override + _i2.Size get size => + (super.noSuchMethod( + Invocation.getter(#size), + returnValue: _FakeSize_2(this, Invocation.getter(#size)), + ) + as _i2.Size); + + @override + void drawRRect(_i2.RRect? rrect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRRect, [rrect, paint]), + returnValueForMissingStub: null, + ); + + @override + void save() => super.noSuchMethod( + Invocation.method(#save, []), + returnValueForMissingStub: null, + ); + + @override + void restore() => super.noSuchMethod( + Invocation.method(#restore, []), + returnValueForMissingStub: null, + ); + + @override + void clipRect( + _i2.Rect? rect, { + _i2.ClipOp? clipOp = _i2.ClipOp.intersect, + bool? doAntiAlias = true, + }) => super.noSuchMethod( + Invocation.method( + #clipRect, + [rect], + {#clipOp: clipOp, #doAntiAlias: doAntiAlias}, + ), + returnValueForMissingStub: null, + ); + + @override + void translate(double? dx, double? dy) => super.noSuchMethod( + Invocation.method(#translate, [dx, dy]), + returnValueForMissingStub: null, + ); + + @override + void rotate(double? radius) => super.noSuchMethod( + Invocation.method(#rotate, [radius]), + returnValueForMissingStub: null, + ); + + @override + void drawPath(_i2.Path? path, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawPath, [path, paint]), + returnValueForMissingStub: null, + ); + + @override + void saveLayer(_i2.Rect? bounds, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#saveLayer, [bounds, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawPicture(_i2.Picture? picture) => super.noSuchMethod( + Invocation.method(#drawPicture, [picture]), + returnValueForMissingStub: null, + ); + + @override + void drawImage(_i2.Image? image, _i2.Offset? offset, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawImage, [image, offset, paint]), + returnValueForMissingStub: null, + ); + + @override + void clipPath(_i2.Path? path, {bool? doAntiAlias = true}) => + super.noSuchMethod( + Invocation.method(#clipPath, [path], {#doAntiAlias: doAntiAlias}), + returnValueForMissingStub: null, + ); + + @override + void drawRect(_i2.Rect? rect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRect, [rect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawLine(_i2.Offset? p1, _i2.Offset? p2, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawLine, [p1, p2, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawCircle(_i2.Offset? center, double? radius, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawCircle, [center, radius, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawArc( + _i2.Rect? rect, + double? startAngle, + double? sweepAngle, + bool? useCenter, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawArc, [ + rect, + startAngle, + sweepAngle, + useCenter, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawText( + _i3.TextPainter? tp, + _i2.Offset? offset, [ + double? rotateAngle, + ]) => super.noSuchMethod( + Invocation.method(#drawText, [tp, offset, rotateAngle]), + returnValueForMissingStub: null, + ); + + @override + void drawVerticalText(_i3.TextPainter? tp, _i2.Offset? offset) => + super.noSuchMethod( + Invocation.method(#drawVerticalText, [tp, offset]), + returnValueForMissingStub: null, + ); + + @override + void drawDot( + _i7.FlDotPainter? painter, + _i7.FlSpot? spot, + _i2.Offset? offset, + ) => super.noSuchMethod( + Invocation.method(#drawDot, [painter, spot, offset]), + returnValueForMissingStub: null, + ); + + @override + void drawErrorIndicator( + _i7.FlSpotErrorRangePainter? painter, + _i7.FlSpot? origin, + _i2.Offset? offset, + _i2.Rect? errorRelativeRect, + _i7.AxisChartData? axisData, + ) => super.noSuchMethod( + Invocation.method(#drawErrorIndicator, [ + painter, + origin, + offset, + errorRelativeRect, + axisData, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawRotated({ + required _i2.Size? size, + _i2.Offset? rotationOffset = _i2.Offset.zero, + _i2.Offset? drawOffset = _i2.Offset.zero, + required double? angle, + required _i6.DrawCallback? drawCallback, + }) => super.noSuchMethod( + Invocation.method(#drawRotated, [], { + #size: size, + #rotationOffset: rotationOffset, + #drawOffset: drawOffset, + #angle: angle, + #drawCallback: drawCallback, + }), + returnValueForMissingStub: null, + ); + + @override + void drawDashedLine( + _i2.Offset? from, + _i2.Offset? to, + _i2.Paint? painter, + List? dashArray, + ) => super.noSuchMethod( + Invocation.method(#drawDashedLine, [from, to, painter, dashArray]), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [BuildContext]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockBuildContext extends _i1.Mock implements _i3.BuildContext { + MockBuildContext() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Widget get widget => + (super.noSuchMethod( + Invocation.getter(#widget), + returnValue: _FakeWidget_3(this, Invocation.getter(#widget)), + ) + as _i3.Widget); + + @override + bool get mounted => + (super.noSuchMethod(Invocation.getter(#mounted), returnValue: false) + as bool); + + @override + bool get debugDoingBuild => + (super.noSuchMethod( + Invocation.getter(#debugDoingBuild), + returnValue: false, + ) + as bool); + + @override + _i3.InheritedWidget dependOnInheritedElement( + _i3.InheritedElement? ancestor, { + Object? aspect, + }) => + (super.noSuchMethod( + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + returnValue: _FakeInheritedWidget_4( + this, + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + ), + ) + as _i3.InheritedWidget); + + @override + void visitAncestorElements(_i3.ConditionalElementVisitor? visitor) => + super.noSuchMethod( + Invocation.method(#visitAncestorElements, [visitor]), + returnValueForMissingStub: null, + ); + + @override + void visitChildElements(_i3.ElementVisitor? visitor) => super.noSuchMethod( + Invocation.method(#visitChildElements, [visitor]), + returnValueForMissingStub: null, + ); + + @override + void dispatchNotification(_i3.Notification? notification) => + super.noSuchMethod( + Invocation.method(#dispatchNotification, [notification]), + returnValueForMissingStub: null, + ); + + @override + _i3.DiagnosticsNode describeElement( + String? name, { + _i4.DiagnosticsTreeStyle? style = _i4.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method(#describeElement, [name], {#style: style}), + returnValue: _FakeDiagnosticsNode_5( + this, + Invocation.method(#describeElement, [name], {#style: style}), + ), + ) + as _i3.DiagnosticsNode); + + @override + _i3.DiagnosticsNode describeWidget( + String? name, { + _i4.DiagnosticsTreeStyle? style = _i4.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method(#describeWidget, [name], {#style: style}), + returnValue: _FakeDiagnosticsNode_5( + this, + Invocation.method(#describeWidget, [name], {#style: style}), + ), + ) + as _i3.DiagnosticsNode); + + @override + List<_i3.DiagnosticsNode> describeMissingAncestor({ + required Type? expectedAncestorType, + }) => + (super.noSuchMethod( + Invocation.method(#describeMissingAncestor, [], { + #expectedAncestorType: expectedAncestorType, + }), + returnValue: <_i3.DiagnosticsNode>[], + ) + as List<_i3.DiagnosticsNode>); + + @override + _i3.DiagnosticsNode describeOwnershipChain(String? name) => + (super.noSuchMethod( + Invocation.method(#describeOwnershipChain, [name]), + returnValue: _FakeDiagnosticsNode_5( + this, + Invocation.method(#describeOwnershipChain, [name]), + ), + ) + as _i3.DiagnosticsNode); +} + +/// A class which mocks [Utils]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockUtils extends _i1.Mock implements _i8.Utils { + MockUtils() { + _i1.throwOnMissingStub(this); + } + + @override + double radians(double? degrees) => + (super.noSuchMethod( + Invocation.method(#radians, [degrees]), + returnValue: 0.0, + ) + as double); + + @override + double degrees(double? radians) => + (super.noSuchMethod( + Invocation.method(#degrees, [radians]), + returnValue: 0.0, + ) + as double); + + @override + double translateRotatedPosition(double? size, double? degree) => + (super.noSuchMethod( + Invocation.method(#translateRotatedPosition, [size, degree]), + returnValue: 0.0, + ) + as double); + + @override + _i2.Offset calculateRotationOffset(_i2.Size? size, double? degree) => + (super.noSuchMethod( + Invocation.method(#calculateRotationOffset, [size, degree]), + returnValue: _FakeOffset_6( + this, + Invocation.method(#calculateRotationOffset, [size, degree]), + ), + ) + as _i2.Offset); + + @override + _i3.BorderRadius? normalizeBorderRadius( + _i3.BorderRadius? borderRadius, + double? width, + ) => + (super.noSuchMethod( + Invocation.method(#normalizeBorderRadius, [borderRadius, width]), + ) + as _i3.BorderRadius?); + + @override + _i3.BorderSide normalizeBorderSide( + _i3.BorderSide? borderSide, + double? width, + ) => + (super.noSuchMethod( + Invocation.method(#normalizeBorderSide, [borderSide, width]), + returnValue: _FakeBorderSide_7( + this, + Invocation.method(#normalizeBorderSide, [borderSide, width]), + ), + ) + as _i3.BorderSide); + + @override + double getEfficientInterval( + double? axisViewSize, + double? diffInAxis, { + double? pixelPerInterval = 40.0, + }) => + (super.noSuchMethod( + Invocation.method( + #getEfficientInterval, + [axisViewSize, diffInAxis], + {#pixelPerInterval: pixelPerInterval}, + ), + returnValue: 0.0, + ) + as double); + + @override + double roundInterval(double? input) => + (super.noSuchMethod( + Invocation.method(#roundInterval, [input]), + returnValue: 0.0, + ) + as double); + + @override + int getFractionDigits(double? value) => + (super.noSuchMethod( + Invocation.method(#getFractionDigits, [value]), + returnValue: 0, + ) + as int); + + @override + String formatNumber(double? axisMin, double? axisMax, double? axisValue) => + (super.noSuchMethod( + Invocation.method(#formatNumber, [axisMin, axisMax, axisValue]), + returnValue: _i9.dummyValue( + this, + Invocation.method(#formatNumber, [axisMin, axisMax, axisValue]), + ), + ) + as String); + + @override + _i3.TextStyle getThemeAwareTextStyle( + _i3.BuildContext? context, + _i3.TextStyle? providedStyle, + ) => + (super.noSuchMethod( + Invocation.method(#getThemeAwareTextStyle, [ + context, + providedStyle, + ]), + returnValue: _FakeTextStyle_8( + this, + Invocation.method(#getThemeAwareTextStyle, [ + context, + providedStyle, + ]), + ), + ) + as _i3.TextStyle); + + @override + double getBestInitialIntervalValue( + double? min, + double? max, + double? interval, { + double? baseline = 0.0, + }) => + (super.noSuchMethod( + Invocation.method( + #getBestInitialIntervalValue, + [min, max, interval], + {#baseline: baseline}, + ), + returnValue: 0.0, + ) + as double); + + @override + double convertRadiusToSigma(double? radius) => + (super.noSuchMethod( + Invocation.method(#convertRadiusToSigma, [radius]), + returnValue: 0.0, + ) + as double); +} diff --git a/test/chart/bar_chart/bar_chart_renderer_test.dart b/test/chart/bar_chart/bar_chart_renderer_test.dart new file mode 100644 index 0000000..7f7b3df --- /dev/null +++ b/test/chart/bar_chart/bar_chart_renderer_test.dart @@ -0,0 +1,168 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/bar_chart/bar_chart_painter.dart'; +import 'package:fl_chart/src/chart/bar_chart/bar_chart_renderer.dart'; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart'; +import 'package:fl_chart/src/utils/canvas_wrapper.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import '../data_pool.dart'; +import 'bar_chart_renderer_test.mocks.dart'; + +@GenerateMocks([Canvas, PaintingContext, BuildContext, BarChartPainter]) +void main() { + group('BarChartRenderer', () { + final data = BarChartData( + titlesData: const FlTitlesData( + leftTitles: AxisTitles( + sideTitles: SideTitles(reservedSize: 20, showTitles: true), + ), + rightTitles: AxisTitles( + sideTitles: SideTitles(reservedSize: 64, showTitles: true), + ), + topTitles: AxisTitles(), + bottomTitles: AxisTitles(), + ), + ); + + final targetData = BarChartData( + titlesData: const FlTitlesData( + leftTitles: AxisTitles( + sideTitles: SideTitles(reservedSize: 8, showTitles: true), + ), + rightTitles: AxisTitles( + sideTitles: SideTitles(reservedSize: 20, showTitles: true), + ), + topTitles: AxisTitles(), + bottomTitles: AxisTitles(), + ), + ); + + const textScaler = TextScaler.linear(4); + + final mockBuildContext = MockBuildContext(); + final renderBarChart = RenderBarChart( + mockBuildContext, + data, + targetData, + textScaler, + null, + canBeScaled: false, + ); + + final mockPainter = MockBarChartPainter(); + final mockPaintingContext = MockPaintingContext(); + final mockCanvas = MockCanvas(); + const mockSize = Size(44, 44); + when(mockPaintingContext.canvas).thenAnswer((realInvocation) => mockCanvas); + renderBarChart + ..mockTestSize = mockSize + ..painter = mockPainter; + + test('test 1 correct data set', () { + expect(renderBarChart.data == data, true); + expect(renderBarChart.data == targetData, false); + expect(renderBarChart.targetData == targetData, true); + expect(renderBarChart.textScaler == textScaler, true); + expect(renderBarChart.paintHolder.data == data, true); + expect(renderBarChart.paintHolder.targetData == targetData, true); + expect(renderBarChart.paintHolder.textScaler == textScaler, true); + }); + + test('test 2 check paint function', () { + renderBarChart.paint(mockPaintingContext, const Offset(10, 10)); + verify(mockCanvas.save()).called(1); + verify(mockCanvas.translate(10, 10)).called(1); + final result = verify(mockPainter.paint(any, captureAny, captureAny)); + expect(result.callCount, 1); + + final canvasWrapper = result.captured[0] as CanvasWrapper; + expect(canvasWrapper.size, const Size(44, 44)); + expect(canvasWrapper.canvas, mockCanvas); + + final paintHolder = result.captured[1] as PaintHolder; + expect(paintHolder.data, data); + expect(paintHolder.targetData, targetData); + expect(paintHolder.textScaler, textScaler); + + verify(mockCanvas.restore()).called(1); + }); + + test('test 3 check getResponseAtLocation function', () { + final results = >[]; + when(mockPainter.handleTouch(captureAny, captureAny, captureAny)) + .thenAnswer((inv) { + results.add({ + 'local_position': inv.positionalArguments[0] as Offset, + 'size': inv.positionalArguments[1] as Size, + 'paint_holder': inv.positionalArguments[2] as PaintHolder, + }); + return MockData.barTouchedSpot; + }); + + when(mockPainter.getChartCoordinateFromPixel(any, any, any)) + .thenAnswer((_) => const Offset(10, 10)); + + final touchResponse = + renderBarChart.getResponseAtLocation(MockData.offset1); + expect(touchResponse.spot, MockData.barTouchedSpot); + expect(touchResponse.touchChartCoordinate, const Offset(10, 10)); + expect(results[0]['local_position'] as Offset, MockData.offset1); + expect(results[0]['size'] as Size, mockSize); + final paintHolder = results[0]['paint_holder'] as PaintHolder; + expect(paintHolder.data, data); + expect(paintHolder.targetData, targetData); + expect(paintHolder.textScaler, textScaler); + }); + + test('test 4 check setters', () { + renderBarChart + ..data = targetData + ..targetData = data + ..textScaler = const TextScaler.linear(22); + + expect(renderBarChart.data, targetData); + expect(renderBarChart.targetData, data); + expect(renderBarChart.textScaler, const TextScaler.linear(22)); + }); + + test('passes chart virtual rect to paint holder', () { + final rect1 = Offset.zero & const Size(100, 100); + final renderBarChart = RenderBarChart( + mockBuildContext, + data, + targetData, + textScaler, + null, + canBeScaled: false, + ); + + expect(renderBarChart.chartVirtualRect, isNull); + expect(renderBarChart.paintHolder.chartVirtualRect, isNull); + + renderBarChart.chartVirtualRect = rect1; + + expect(renderBarChart.chartVirtualRect, rect1); + expect(renderBarChart.paintHolder.chartVirtualRect, rect1); + }); + + test('uses canBeScaled', () { + final renderBarChart = RenderBarChart( + mockBuildContext, + data, + targetData, + textScaler, + null, + canBeScaled: false, + ); + + expect(renderBarChart.canBeScaled, false); + + renderBarChart.canBeScaled = true; + + expect(renderBarChart.canBeScaled, true); + }); + }); +} diff --git a/test/chart/bar_chart/bar_chart_renderer_test.mocks.dart b/test/chart/bar_chart/bar_chart_renderer_test.mocks.dart new file mode 100644 index 0000000..d31f490 --- /dev/null +++ b/test/chart/bar_chart/bar_chart_renderer_test.mocks.dart @@ -0,0 +1,1098 @@ +// Mocks generated by Mockito 5.4.5 from annotations +// in fl_chart/test/chart/bar_chart/bar_chart_renderer_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:typed_data' as _i7; +import 'dart:ui' as _i2; + +import 'package:fl_chart/fl_chart.dart' as _i13; +import 'package:fl_chart/src/chart/bar_chart/bar_chart_painter.dart' as _i10; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart' + as _i12; +import 'package:fl_chart/src/utils/canvas_wrapper.dart' as _i11; +import 'package:flutter/foundation.dart' as _i5; +import 'package:flutter/gestures.dart' as _i8; +import 'package:flutter/material.dart' as _i6; +import 'package:flutter/rendering.dart' as _i3; +import 'package:flutter/src/rendering/layer.dart' as _i4; +import 'package:flutter/src/widgets/notification_listener.dart' as _i9; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeRect_0 extends _i1.SmartFake implements _i2.Rect { + _FakeRect_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeCanvas_1 extends _i1.SmartFake implements _i2.Canvas { + _FakeCanvas_1(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakePaintingContext_2 extends _i1.SmartFake + implements _i3.PaintingContext { + _FakePaintingContext_2(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeColorFilterLayer_3 extends _i1.SmartFake + implements _i4.ColorFilterLayer { + _FakeColorFilterLayer_3(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeOpacityLayer_4 extends _i1.SmartFake implements _i4.OpacityLayer { + _FakeOpacityLayer_4(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeWidget_5 extends _i1.SmartFake implements _i6.Widget { + _FakeWidget_5(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeInheritedWidget_6 extends _i1.SmartFake + implements _i6.InheritedWidget { + _FakeInheritedWidget_6(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeDiagnosticsNode_7 extends _i1.SmartFake + implements _i5.DiagnosticsNode { + _FakeDiagnosticsNode_7(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({ + _i5.TextTreeConfiguration? parentConfiguration, + _i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info, + }) => super.toString(); +} + +class _FakeOffset_8 extends _i1.SmartFake implements _i2.Offset { + _FakeOffset_8(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +/// A class which mocks [Canvas]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCanvas extends _i1.Mock implements _i2.Canvas { + MockCanvas() { + _i1.throwOnMissingStub(this); + } + + @override + void save() => super.noSuchMethod( + Invocation.method(#save, []), + returnValueForMissingStub: null, + ); + + @override + void saveLayer(_i2.Rect? bounds, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#saveLayer, [bounds, paint]), + returnValueForMissingStub: null, + ); + + @override + void restore() => super.noSuchMethod( + Invocation.method(#restore, []), + returnValueForMissingStub: null, + ); + + @override + void restoreToCount(int? count) => super.noSuchMethod( + Invocation.method(#restoreToCount, [count]), + returnValueForMissingStub: null, + ); + + @override + int getSaveCount() => + (super.noSuchMethod(Invocation.method(#getSaveCount, []), returnValue: 0) + as int); + + @override + void translate(double? dx, double? dy) => super.noSuchMethod( + Invocation.method(#translate, [dx, dy]), + returnValueForMissingStub: null, + ); + + @override + void scale(double? sx, [double? sy]) => super.noSuchMethod( + Invocation.method(#scale, [sx, sy]), + returnValueForMissingStub: null, + ); + + @override + void rotate(double? radians) => super.noSuchMethod( + Invocation.method(#rotate, [radians]), + returnValueForMissingStub: null, + ); + + @override + void skew(double? sx, double? sy) => super.noSuchMethod( + Invocation.method(#skew, [sx, sy]), + returnValueForMissingStub: null, + ); + + @override + void transform(_i7.Float64List? matrix4) => super.noSuchMethod( + Invocation.method(#transform, [matrix4]), + returnValueForMissingStub: null, + ); + + @override + _i7.Float64List getTransform() => + (super.noSuchMethod( + Invocation.method(#getTransform, []), + returnValue: _i7.Float64List(0), + ) + as _i7.Float64List); + + @override + void clipRect( + _i2.Rect? rect, { + _i2.ClipOp? clipOp = _i2.ClipOp.intersect, + bool? doAntiAlias = true, + }) => super.noSuchMethod( + Invocation.method( + #clipRect, + [rect], + {#clipOp: clipOp, #doAntiAlias: doAntiAlias}, + ), + returnValueForMissingStub: null, + ); + + @override + void clipRRect(_i2.RRect? rrect, {bool? doAntiAlias = true}) => + super.noSuchMethod( + Invocation.method(#clipRRect, [rrect], {#doAntiAlias: doAntiAlias}), + returnValueForMissingStub: null, + ); + + @override + void clipPath(_i2.Path? path, {bool? doAntiAlias = true}) => + super.noSuchMethod( + Invocation.method(#clipPath, [path], {#doAntiAlias: doAntiAlias}), + returnValueForMissingStub: null, + ); + + @override + _i2.Rect getLocalClipBounds() => + (super.noSuchMethod( + Invocation.method(#getLocalClipBounds, []), + returnValue: _FakeRect_0( + this, + Invocation.method(#getLocalClipBounds, []), + ), + ) + as _i2.Rect); + + @override + _i2.Rect getDestinationClipBounds() => + (super.noSuchMethod( + Invocation.method(#getDestinationClipBounds, []), + returnValue: _FakeRect_0( + this, + Invocation.method(#getDestinationClipBounds, []), + ), + ) + as _i2.Rect); + + @override + void drawColor(_i2.Color? color, _i2.BlendMode? blendMode) => + super.noSuchMethod( + Invocation.method(#drawColor, [color, blendMode]), + returnValueForMissingStub: null, + ); + + @override + void drawLine(_i2.Offset? p1, _i2.Offset? p2, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawLine, [p1, p2, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawPaint(_i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawPaint, [paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRect(_i2.Rect? rect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRect, [rect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRRect(_i2.RRect? rrect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRRect, [rrect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawDRRect(_i2.RRect? outer, _i2.RRect? inner, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawDRRect, [outer, inner, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawOval(_i2.Rect? rect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawOval, [rect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawCircle(_i2.Offset? c, double? radius, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawCircle, [c, radius, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawArc( + _i2.Rect? rect, + double? startAngle, + double? sweepAngle, + bool? useCenter, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawArc, [ + rect, + startAngle, + sweepAngle, + useCenter, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawPath(_i2.Path? path, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawPath, [path, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImage(_i2.Image? image, _i2.Offset? offset, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawImage, [image, offset, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImageRect( + _i2.Image? image, + _i2.Rect? src, + _i2.Rect? dst, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawImageRect, [image, src, dst, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImageNine( + _i2.Image? image, + _i2.Rect? center, + _i2.Rect? dst, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawImageNine, [image, center, dst, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawPicture(_i2.Picture? picture) => super.noSuchMethod( + Invocation.method(#drawPicture, [picture]), + returnValueForMissingStub: null, + ); + + @override + void drawParagraph(_i2.Paragraph? paragraph, _i2.Offset? offset) => + super.noSuchMethod( + Invocation.method(#drawParagraph, [paragraph, offset]), + returnValueForMissingStub: null, + ); + + @override + void drawPoints( + _i2.PointMode? pointMode, + List<_i2.Offset>? points, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawPoints, [pointMode, points, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRawPoints( + _i2.PointMode? pointMode, + _i7.Float32List? points, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawRawPoints, [pointMode, points, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawVertices( + _i2.Vertices? vertices, + _i2.BlendMode? blendMode, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawVertices, [vertices, blendMode, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawAtlas( + _i2.Image? atlas, + List<_i2.RSTransform>? transforms, + List<_i2.Rect>? rects, + List<_i2.Color>? colors, + _i2.BlendMode? blendMode, + _i2.Rect? cullRect, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawAtlas, [ + atlas, + transforms, + rects, + colors, + blendMode, + cullRect, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawRawAtlas( + _i2.Image? atlas, + _i7.Float32List? rstTransforms, + _i7.Float32List? rects, + _i7.Int32List? colors, + _i2.BlendMode? blendMode, + _i2.Rect? cullRect, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawRawAtlas, [ + atlas, + rstTransforms, + rects, + colors, + blendMode, + cullRect, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawShadow( + _i2.Path? path, + _i2.Color? color, + double? elevation, + bool? transparentOccluder, + ) => super.noSuchMethod( + Invocation.method(#drawShadow, [ + path, + color, + elevation, + transparentOccluder, + ]), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [PaintingContext]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPaintingContext extends _i1.Mock implements _i3.PaintingContext { + MockPaintingContext() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.Rect get estimatedBounds => + (super.noSuchMethod( + Invocation.getter(#estimatedBounds), + returnValue: _FakeRect_0(this, Invocation.getter(#estimatedBounds)), + ) + as _i2.Rect); + + @override + _i2.Canvas get canvas => + (super.noSuchMethod( + Invocation.getter(#canvas), + returnValue: _FakeCanvas_1(this, Invocation.getter(#canvas)), + ) + as _i2.Canvas); + + @override + void paintChild(_i3.RenderObject? child, _i2.Offset? offset) => + super.noSuchMethod( + Invocation.method(#paintChild, [child, offset]), + returnValueForMissingStub: null, + ); + + @override + void appendLayer(_i4.Layer? layer) => super.noSuchMethod( + Invocation.method(#appendLayer, [layer]), + returnValueForMissingStub: null, + ); + + @override + _i2.VoidCallback addCompositionCallback(_i4.CompositionCallback? callback) => + (super.noSuchMethod( + Invocation.method(#addCompositionCallback, [callback]), + returnValue: () {}, + ) + as _i2.VoidCallback); + + @override + void stopRecordingIfNeeded() => super.noSuchMethod( + Invocation.method(#stopRecordingIfNeeded, []), + returnValueForMissingStub: null, + ); + + @override + void setIsComplexHint() => super.noSuchMethod( + Invocation.method(#setIsComplexHint, []), + returnValueForMissingStub: null, + ); + + @override + void setWillChangeHint() => super.noSuchMethod( + Invocation.method(#setWillChangeHint, []), + returnValueForMissingStub: null, + ); + + @override + void addLayer(_i4.Layer? layer) => super.noSuchMethod( + Invocation.method(#addLayer, [layer]), + returnValueForMissingStub: null, + ); + + @override + void pushLayer( + _i4.ContainerLayer? childLayer, + _i3.PaintingContextCallback? painter, + _i2.Offset? offset, { + _i2.Rect? childPaintBounds, + }) => super.noSuchMethod( + Invocation.method( + #pushLayer, + [childLayer, painter, offset], + {#childPaintBounds: childPaintBounds}, + ), + returnValueForMissingStub: null, + ); + + @override + _i3.PaintingContext createChildContext( + _i4.ContainerLayer? childLayer, + _i2.Rect? bounds, + ) => + (super.noSuchMethod( + Invocation.method(#createChildContext, [childLayer, bounds]), + returnValue: _FakePaintingContext_2( + this, + Invocation.method(#createChildContext, [childLayer, bounds]), + ), + ) + as _i3.PaintingContext); + + @override + _i4.ClipRectLayer? pushClipRect( + bool? needsCompositing, + _i2.Offset? offset, + _i2.Rect? clipRect, + _i3.PaintingContextCallback? painter, { + _i2.Clip? clipBehavior = _i2.Clip.hardEdge, + _i4.ClipRectLayer? oldLayer, + }) => + (super.noSuchMethod( + Invocation.method( + #pushClipRect, + [needsCompositing, offset, clipRect, painter], + {#clipBehavior: clipBehavior, #oldLayer: oldLayer}, + ), + ) + as _i4.ClipRectLayer?); + + @override + _i4.ClipRRectLayer? pushClipRRect( + bool? needsCompositing, + _i2.Offset? offset, + _i2.Rect? bounds, + _i2.RRect? clipRRect, + _i3.PaintingContextCallback? painter, { + _i2.Clip? clipBehavior = _i2.Clip.antiAlias, + _i4.ClipRRectLayer? oldLayer, + }) => + (super.noSuchMethod( + Invocation.method( + #pushClipRRect, + [needsCompositing, offset, bounds, clipRRect, painter], + {#clipBehavior: clipBehavior, #oldLayer: oldLayer}, + ), + ) + as _i4.ClipRRectLayer?); + + @override + _i4.ClipPathLayer? pushClipPath( + bool? needsCompositing, + _i2.Offset? offset, + _i2.Rect? bounds, + _i2.Path? clipPath, + _i3.PaintingContextCallback? painter, { + _i2.Clip? clipBehavior = _i2.Clip.antiAlias, + _i4.ClipPathLayer? oldLayer, + }) => + (super.noSuchMethod( + Invocation.method( + #pushClipPath, + [needsCompositing, offset, bounds, clipPath, painter], + {#clipBehavior: clipBehavior, #oldLayer: oldLayer}, + ), + ) + as _i4.ClipPathLayer?); + + @override + _i4.ColorFilterLayer pushColorFilter( + _i2.Offset? offset, + _i2.ColorFilter? colorFilter, + _i3.PaintingContextCallback? painter, { + _i4.ColorFilterLayer? oldLayer, + }) => + (super.noSuchMethod( + Invocation.method( + #pushColorFilter, + [offset, colorFilter, painter], + {#oldLayer: oldLayer}, + ), + returnValue: _FakeColorFilterLayer_3( + this, + Invocation.method( + #pushColorFilter, + [offset, colorFilter, painter], + {#oldLayer: oldLayer}, + ), + ), + ) + as _i4.ColorFilterLayer); + + @override + _i4.TransformLayer? pushTransform( + bool? needsCompositing, + _i2.Offset? offset, + _i8.Matrix4? transform, + _i3.PaintingContextCallback? painter, { + _i4.TransformLayer? oldLayer, + }) => + (super.noSuchMethod( + Invocation.method( + #pushTransform, + [needsCompositing, offset, transform, painter], + {#oldLayer: oldLayer}, + ), + ) + as _i4.TransformLayer?); + + @override + _i4.OpacityLayer pushOpacity( + _i2.Offset? offset, + int? alpha, + _i3.PaintingContextCallback? painter, { + _i4.OpacityLayer? oldLayer, + }) => + (super.noSuchMethod( + Invocation.method( + #pushOpacity, + [offset, alpha, painter], + {#oldLayer: oldLayer}, + ), + returnValue: _FakeOpacityLayer_4( + this, + Invocation.method( + #pushOpacity, + [offset, alpha, painter], + {#oldLayer: oldLayer}, + ), + ), + ) + as _i4.OpacityLayer); + + @override + void clipPathAndPaint( + _i2.Path? path, + _i2.Clip? clipBehavior, + _i2.Rect? bounds, + _i2.VoidCallback? painter, + ) => super.noSuchMethod( + Invocation.method(#clipPathAndPaint, [path, clipBehavior, bounds, painter]), + returnValueForMissingStub: null, + ); + + @override + void clipRRectAndPaint( + _i2.RRect? rrect, + _i2.Clip? clipBehavior, + _i2.Rect? bounds, + _i2.VoidCallback? painter, + ) => super.noSuchMethod( + Invocation.method(#clipRRectAndPaint, [ + rrect, + clipBehavior, + bounds, + painter, + ]), + returnValueForMissingStub: null, + ); + + @override + void clipRectAndPaint( + _i2.Rect? rect, + _i2.Clip? clipBehavior, + _i2.Rect? bounds, + _i2.VoidCallback? painter, + ) => super.noSuchMethod( + Invocation.method(#clipRectAndPaint, [rect, clipBehavior, bounds, painter]), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [BuildContext]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockBuildContext extends _i1.Mock implements _i6.BuildContext { + MockBuildContext() { + _i1.throwOnMissingStub(this); + } + + @override + _i6.Widget get widget => + (super.noSuchMethod( + Invocation.getter(#widget), + returnValue: _FakeWidget_5(this, Invocation.getter(#widget)), + ) + as _i6.Widget); + + @override + bool get mounted => + (super.noSuchMethod(Invocation.getter(#mounted), returnValue: false) + as bool); + + @override + bool get debugDoingBuild => + (super.noSuchMethod( + Invocation.getter(#debugDoingBuild), + returnValue: false, + ) + as bool); + + @override + _i6.InheritedWidget dependOnInheritedElement( + _i6.InheritedElement? ancestor, { + Object? aspect, + }) => + (super.noSuchMethod( + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + returnValue: _FakeInheritedWidget_6( + this, + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + ), + ) + as _i6.InheritedWidget); + + @override + void visitAncestorElements(_i6.ConditionalElementVisitor? visitor) => + super.noSuchMethod( + Invocation.method(#visitAncestorElements, [visitor]), + returnValueForMissingStub: null, + ); + + @override + void visitChildElements(_i6.ElementVisitor? visitor) => super.noSuchMethod( + Invocation.method(#visitChildElements, [visitor]), + returnValueForMissingStub: null, + ); + + @override + void dispatchNotification(_i9.Notification? notification) => + super.noSuchMethod( + Invocation.method(#dispatchNotification, [notification]), + returnValueForMissingStub: null, + ); + + @override + _i5.DiagnosticsNode describeElement( + String? name, { + _i5.DiagnosticsTreeStyle? style = _i5.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method(#describeElement, [name], {#style: style}), + returnValue: _FakeDiagnosticsNode_7( + this, + Invocation.method(#describeElement, [name], {#style: style}), + ), + ) + as _i5.DiagnosticsNode); + + @override + _i5.DiagnosticsNode describeWidget( + String? name, { + _i5.DiagnosticsTreeStyle? style = _i5.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method(#describeWidget, [name], {#style: style}), + returnValue: _FakeDiagnosticsNode_7( + this, + Invocation.method(#describeWidget, [name], {#style: style}), + ), + ) + as _i5.DiagnosticsNode); + + @override + List<_i5.DiagnosticsNode> describeMissingAncestor({ + required Type? expectedAncestorType, + }) => + (super.noSuchMethod( + Invocation.method(#describeMissingAncestor, [], { + #expectedAncestorType: expectedAncestorType, + }), + returnValue: <_i5.DiagnosticsNode>[], + ) + as List<_i5.DiagnosticsNode>); + + @override + _i5.DiagnosticsNode describeOwnershipChain(String? name) => + (super.noSuchMethod( + Invocation.method(#describeOwnershipChain, [name]), + returnValue: _FakeDiagnosticsNode_7( + this, + Invocation.method(#describeOwnershipChain, [name]), + ), + ) + as _i5.DiagnosticsNode); +} + +/// A class which mocks [BarChartPainter]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockBarChartPainter extends _i1.Mock implements _i10.BarChartPainter { + MockBarChartPainter() { + _i1.throwOnMissingStub(this); + } + + @override + void paint( + _i6.BuildContext? context, + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.BarChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#paint, [context, canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + List<_i10.GroupBarsPosition> calculateGroupAndBarsPosition( + _i2.Size? viewSize, + List? groupsX, + List<_i13.BarChartGroupData>? barGroups, + ) => + (super.noSuchMethod( + Invocation.method(#calculateGroupAndBarsPosition, [ + viewSize, + groupsX, + barGroups, + ]), + returnValue: <_i10.GroupBarsPosition>[], + ) + as List<_i10.GroupBarsPosition>); + + @override + void drawBars( + _i11.CanvasWrapper? canvasWrapper, + List<_i10.GroupBarsPosition>? groupBarsPosition, + _i12.PaintHolder<_i13.BarChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawBars, [canvasWrapper, groupBarsPosition, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawErrorIndicatorData( + _i11.CanvasWrapper? canvasWrapper, + List<_i10.GroupBarsPosition>? groupBarsPosition, + _i12.PaintHolder<_i13.BarChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawErrorIndicatorData, [ + canvasWrapper, + groupBarsPosition, + holder, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawTouchTooltip( + _i6.BuildContext? context, + _i11.CanvasWrapper? canvasWrapper, + List<_i10.GroupBarsPosition>? groupPositions, + _i13.BarTouchTooltipData? tooltipData, + _i13.BarChartGroupData? showOnBarGroup, + int? barGroupIndex, + _i13.BarChartRodData? showOnRodData, + int? barRodIndex, + _i12.PaintHolder<_i13.BarChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawTouchTooltip, [ + context, + canvasWrapper, + groupPositions, + tooltipData, + showOnBarGroup, + barGroupIndex, + showOnRodData, + barRodIndex, + holder, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawStackItemBorderStroke( + _i11.CanvasWrapper? canvasWrapper, + _i13.BarChartRodStackItem? stackItem, + int? index, + int? rodStacksSize, + double? barThickSize, + _i2.RRect? barRRect, + _i2.Size? drawSize, + _i12.PaintHolder<_i13.BarChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawStackItemBorderStroke, [ + canvasWrapper, + stackItem, + index, + rodStacksSize, + barThickSize, + barRRect, + drawSize, + holder, + ]), + returnValueForMissingStub: null, + ); + + @override + _i13.BarTouchedSpot? handleTouch( + _i2.Offset? localPosition, + _i2.Size? size, + _i12.PaintHolder<_i13.BarChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#handleTouch, [localPosition, size, holder]), + ) + as _i13.BarTouchedSpot?); + + @override + void drawGrid( + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.BarChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawGrid, [canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawBackground( + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.BarChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawBackground, [canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawRangeAnnotation( + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.BarChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawRangeAnnotation, [canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawExtraLines( + _i6.BuildContext? context, + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.BarChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawExtraLines, [context, canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawHorizontalLines( + _i6.BuildContext? context, + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.BarChartData>? holder, + _i2.Size? viewSize, + ) => super.noSuchMethod( + Invocation.method(#drawHorizontalLines, [ + context, + canvasWrapper, + holder, + viewSize, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawVerticalLines( + _i6.BuildContext? context, + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.BarChartData>? holder, + _i2.Size? viewSize, + ) => super.noSuchMethod( + Invocation.method(#drawVerticalLines, [ + context, + canvasWrapper, + holder, + viewSize, + ]), + returnValueForMissingStub: null, + ); + + @override + double getPixelX( + double? spotX, + _i2.Size? viewSize, + _i12.PaintHolder<_i13.BarChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#getPixelX, [spotX, viewSize, holder]), + returnValue: 0.0, + ) + as double); + + @override + double getPixelY( + double? spotY, + _i2.Size? viewSize, + _i12.PaintHolder<_i13.BarChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#getPixelY, [spotY, viewSize, holder]), + returnValue: 0.0, + ) + as double); + + @override + double getXForPixel( + double? pixelX, + _i2.Size? viewSize, + _i12.PaintHolder<_i13.BarChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#getXForPixel, [pixelX, viewSize, holder]), + returnValue: 0.0, + ) + as double); + + @override + double getYForPixel( + double? pixelY, + _i2.Size? viewSize, + _i12.PaintHolder<_i13.BarChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#getYForPixel, [pixelY, viewSize, holder]), + returnValue: 0.0, + ) + as double); + + @override + _i2.Offset getChartCoordinateFromPixel( + _i2.Offset? pixelOffset, + _i2.Size? viewSize, + _i12.PaintHolder<_i13.BarChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#getChartCoordinateFromPixel, [ + pixelOffset, + viewSize, + holder, + ]), + returnValue: _FakeOffset_8( + this, + Invocation.method(#getChartCoordinateFromPixel, [ + pixelOffset, + viewSize, + holder, + ]), + ), + ) + as _i2.Offset); + + @override + double getTooltipLeft( + double? dx, + double? tooltipWidth, + _i13.FLHorizontalAlignment? tooltipHorizontalAlignment, + double? tooltipHorizontalOffset, + ) => + (super.noSuchMethod( + Invocation.method(#getTooltipLeft, [ + dx, + tooltipWidth, + tooltipHorizontalAlignment, + tooltipHorizontalOffset, + ]), + returnValue: 0.0, + ) + as double); +} diff --git a/test/chart/bar_chart/bar_chart_test.dart b/test/chart/bar_chart/bar_chart_test.dart new file mode 100644 index 0000000..7087721 --- /dev/null +++ b/test/chart/bar_chart/bar_chart_test.dart @@ -0,0 +1,939 @@ +import 'package:fl_chart/src/chart/bar_chart/bar_chart.dart'; +import 'package:fl_chart/src/chart/bar_chart/bar_chart_data.dart'; +import 'package:fl_chart/src/chart/bar_chart/bar_chart_renderer.dart'; +import 'package:fl_chart/src/chart/base/axis_chart/axis_chart_scaffold_widget.dart'; +import 'package:fl_chart/src/chart/base/axis_chart/scale_axis.dart'; +import 'package:fl_chart/src/chart/base/axis_chart/transformation_config.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + Widget createTestWidget({ + required BarChart chart, + }) { + return MaterialApp( + home: chart, + ); + } + + group('BarChart', () { + group('throws AssertionError for', () { + final verticallyScalableAlignments = [ + BarChartAlignment.start, + BarChartAlignment.center, + BarChartAlignment.end, + ]; + for (final alignment in verticallyScalableAlignments) { + testWidgets('FlScaleAxis.horizontal with $alignment', + (WidgetTester tester) async { + expect( + () => tester.pumpWidget( + createTestWidget( + chart: BarChart( + BarChartData( + alignment: alignment, + ), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.horizontal, + ), + ), + ), + ), + throwsAssertionError, + ); + }); + } + + for (final alignment in verticallyScalableAlignments) { + testWidgets('FlScaleAxis.free with $alignment', + (WidgetTester tester) async { + expect( + () => tester.pumpWidget( + createTestWidget( + chart: BarChart( + BarChartData( + alignment: alignment, + ), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.free, + ), + ), + ), + ), + throwsAssertionError, + ); + }); + } + }); + + group('allows passing', () { + for (final alignment in BarChartAlignment.values) { + testWidgets('FlScaleAxis.none with $alignment', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: BarChart( + BarChartData(alignment: alignment), + // This is for test + // ignore: avoid_redundant_argument_values + transformationConfig: const FlTransformationConfig( + // This is for test + // ignore: avoid_redundant_argument_values + scaleAxis: FlScaleAxis.none, + ), + ), + ), + ); + }); + } + + for (final alignment in BarChartAlignment.values) { + testWidgets('FlScaleAxis.vertical with $alignment', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: BarChart( + BarChartData(alignment: alignment), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.vertical, + ), + ), + ), + ); + }); + } + + final scalableAlignments = [ + BarChartAlignment.spaceAround, + BarChartAlignment.spaceBetween, + BarChartAlignment.spaceEvenly, + ]; + + for (final alignment in scalableAlignments) { + testWidgets('FlScaleAxis.free with $alignment', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: BarChart( + BarChartData(alignment: alignment), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.free, + ), + ), + ), + ); + }); + } + + for (final alignment in scalableAlignments) { + testWidgets('FlScaleAxis.horizontal with $alignment', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: BarChart( + BarChartData(alignment: alignment), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.horizontal, + ), + ), + ), + ); + }); + } + }); + + testWidgets('has correct default values', (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: BarChart( + BarChartData(), + ), + ), + ); + + final barChart = tester.widget(find.byType(BarChart)); + expect(barChart.transformationConfig, const FlTransformationConfig()); + }); + + testWidgets('passes interaction parameters to AxisChartScaffoldWidget', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: BarChart( + BarChartData(), + ), + ), + ); + + final axisChartScaffoldWidget = tester.widget( + find.byType(AxisChartScaffoldWidget), + ); + + expect( + axisChartScaffoldWidget.transformationConfig, + const FlTransformationConfig(), + ); + + await tester.pumpAndSettle(); + + final transformationConfig = FlTransformationConfig( + scaleAxis: FlScaleAxis.free, + trackpadScrollCausesScale: true, + maxScale: 10, + minScale: 1.5, + transformationController: TransformationController(), + ); + await tester.pumpWidget( + createTestWidget( + chart: BarChart( + BarChartData(), + transformationConfig: transformationConfig, + ), + ), + ); + + final axisChartScaffoldWidget1 = tester.widget( + find.byType(AxisChartScaffoldWidget), + ); + + expect( + axisChartScaffoldWidget1.transformationConfig, + transformationConfig, + ); + }); + + for (final scaleAxis in FlScaleAxis.scalingEnabledAxis) { + testWidgets('passes canBeScaled true for $scaleAxis', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: BarChart( + BarChartData(), + transformationConfig: FlTransformationConfig( + scaleAxis: scaleAxis, + ), + ), + ), + ); + + final barChartLeaf = tester.widget( + find.byType(BarChartLeaf), + ); + expect(barChartLeaf.canBeScaled, true); + }); + } + + testWidgets('passes canBeScaled false for FlScaleAxis.none', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: BarChart( + BarChartData(), + // This is for test + // ignore: avoid_redundant_argument_values + transformationConfig: const FlTransformationConfig( + // This is for test + // ignore: avoid_redundant_argument_values + scaleAxis: FlScaleAxis.none, + ), + ), + ), + ); + + final barChartLeaf = tester.widget( + find.byType(BarChartLeaf), + ); + expect(barChartLeaf.canBeScaled, false); + }); + + group('touch gesture', () { + testWidgets('does not scale with FlScaleAxis.none', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: BarChart( + BarChartData(), + ), + ), + ); + + final barChartCenterOffset = + tester.getCenter(find.byType(BarChartLeaf)); + final scaleStart1 = barChartCenterOffset; + final scaleStart2 = barChartCenterOffset; + final scaleEnd1 = barChartCenterOffset + const Offset(100, 100); + final scaleEnd2 = barChartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await tester.pump(); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await tester.pump(); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final barChartLeaf = tester.widget( + find.byType(BarChartLeaf), + ); + + expect(barChartLeaf.chartVirtualRect, isNull); + }); + + testWidgets('scales freely with FlScaleAxis.free', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: BarChart( + BarChartData(alignment: BarChartAlignment.spaceEvenly), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.free, + ), + ), + ), + ); + + final barChartCenterOffset = + tester.getCenter(find.byType(BarChartLeaf)); + final scaleStart1 = barChartCenterOffset; + final scaleStart2 = barChartCenterOffset; + final scaleEnd1 = barChartCenterOffset + const Offset(100, 100); + final scaleEnd2 = barChartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await tester.pump(); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await tester.pump(); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final barChartLeaf = tester.widget( + find.byType(BarChartLeaf), + ); + final renderBox = tester.renderObject( + find.byType(BarChartLeaf), + ); + final chartVirtualRect = barChartLeaf.chartVirtualRect!; + + expect(chartVirtualRect.size, greaterThan(renderBox.size)); + expect(chartVirtualRect.left, isNegative); + expect(chartVirtualRect.top, isNegative); + }); + + testWidgets('scales horizontally with FlScaleAxis.horizontal', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: BarChart( + BarChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.horizontal, + ), + ), + ), + ); + + final chartCenterOffset = tester.getCenter(find.byType(BarChartLeaf)); + final scaleStart1 = chartCenterOffset; + final scaleStart2 = chartCenterOffset; + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await tester.pump(); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await tester.pump(); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final barChartLeaf = tester.widget( + find.byType(BarChartLeaf), + ); + final renderBox = tester.renderObject( + find.byType(BarChartLeaf), + ); + final chartVirtualRect = barChartLeaf.chartVirtualRect!; + + expect(chartVirtualRect.size.height, renderBox.size.height); + expect(chartVirtualRect.size.width, greaterThan(renderBox.size.width)); + expect(chartVirtualRect.left, isNegative); + expect(chartVirtualRect.top, 0); + }); + + testWidgets('scales vertically with FlScaleAxis.vertical', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: BarChart( + BarChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.vertical, + ), + ), + ), + ); + + final chartCenterOffset = tester.getCenter(find.byType(BarChartLeaf)); + final scaleStart1 = chartCenterOffset; + final scaleStart2 = chartCenterOffset; + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await tester.pump(); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await tester.pump(); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final barChartLeaf = tester.widget( + find.byType(BarChartLeaf), + ); + final renderBox = tester.renderObject( + find.byType(BarChartLeaf), + ); + final chartVirtualRect = barChartLeaf.chartVirtualRect!; + + expect( + chartVirtualRect.size.height, + greaterThan(renderBox.size.height), + ); + expect(chartVirtualRect.size.width, renderBox.size.width); + expect(chartVirtualRect.left, 0); + expect(chartVirtualRect.top, isNegative); + }); + + group('pans', () { + testWidgets('only horizontally with FlScaleAxis.horizontal', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: BarChart( + BarChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.horizontal, + ), + ), + ), + ); + + final chartCenterOffset = tester.getCenter(find.byType(BarChartLeaf)); + final scaleStart1 = chartCenterOffset; + final scaleStart2 = chartCenterOffset; + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final barChartLeafBeforePan = tester.widget( + find.byType(BarChartLeaf), + ); + final chartVirtualRectBeforePan = + barChartLeafBeforePan.chartVirtualRect!; + expect(chartVirtualRectBeforePan.top, 0); + + const panOffset = Offset(100, 100); + await tester.dragFrom(chartCenterOffset, panOffset); + await tester.pumpAndSettle(); + + final barChartLeafAfterPan = tester.widget( + find.byType(BarChartLeaf), + ); + final chartVirtualRectAfterPan = + barChartLeafAfterPan.chartVirtualRect!; + + expect(chartVirtualRectBeforePan.size, chartVirtualRectAfterPan.size); + expect( + chartVirtualRectAfterPan.left, + greaterThan(chartVirtualRectBeforePan.left), + ); + expect(chartVirtualRectAfterPan.top, 0); + }); + + testWidgets('only vertically with FlScaleAxis.vertical', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: BarChart( + BarChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.vertical, + ), + ), + ), + ); + + final chartCenterOffset = tester.getCenter(find.byType(BarChartLeaf)); + final scaleStart1 = chartCenterOffset; + final scaleStart2 = chartCenterOffset; + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final barChartLeafBeforePan = tester.widget( + find.byType(BarChartLeaf), + ); + final chartVirtualRectBeforePan = + barChartLeafBeforePan.chartVirtualRect!; + expect(chartVirtualRectBeforePan.left, 0); + + const panOffset = Offset(100, 100); + await tester.dragFrom(chartCenterOffset, panOffset); + await tester.pumpAndSettle(); + + final barChartLeafAfterPan = tester.widget( + find.byType(BarChartLeaf), + ); + final chartVirtualRectAfterPan = + barChartLeafAfterPan.chartVirtualRect!; + + expect(chartVirtualRectAfterPan.left, 0); + expect( + chartVirtualRectAfterPan.top, + greaterThan(chartVirtualRectBeforePan.top), + ); + }); + + testWidgets('freely with FlScaleAxis.free', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: BarChart( + BarChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.free, + ), + ), + ), + ); + + final chartCenterOffset = tester.getCenter(find.byType(BarChartLeaf)); + final scaleStart1 = chartCenterOffset; + final scaleStart2 = chartCenterOffset; + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final barChartLeafBeforePan = tester.widget( + find.byType(BarChartLeaf), + ); + final chartVirtualRectBeforePan = + barChartLeafBeforePan.chartVirtualRect!; + expect(chartVirtualRectBeforePan.top, isNegative); + expect(chartVirtualRectBeforePan.left, isNegative); + + const panOffset = Offset(100, 100); + await tester.dragFrom(chartCenterOffset, panOffset); + await tester.pumpAndSettle(); + + final barChartLeafAfterPan = tester.widget( + find.byType(BarChartLeaf), + ); + final chartVirtualRectAfterPan = + barChartLeafAfterPan.chartVirtualRect!; + + expect( + chartVirtualRectAfterPan.left, + greaterThan(chartVirtualRectBeforePan.left), + ); + expect( + chartVirtualRectAfterPan.top, + greaterThan(chartVirtualRectBeforePan.top), + ); + }); + }); + }); + + group('trackpad scroll', () { + group('pans', () { + testWidgets('only horizontally with FlScaleAxis.horizontal', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: BarChart( + BarChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.horizontal, + ), + ), + ), + ); + + final chartCenterOffset = tester.getCenter(find.byType(BarChartLeaf)); + final scaleStart1 = chartCenterOffset; + final scaleStart2 = chartCenterOffset; + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await tester.pump(); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await tester.pump(); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final barChartLeafBeforePan = tester.widget( + find.byType(BarChartLeaf), + ); + + final chartVirtualRectBeforePan = + barChartLeafBeforePan.chartVirtualRect!; + expect(chartVirtualRectBeforePan.top, 0); + + final pointer = TestPointer(1, PointerDeviceKind.trackpad); + const leftAndUp = Offset(-100, -100); + await tester.sendEventToBinding(pointer.hover(chartCenterOffset)); + await tester.pump(); + await tester.sendEventToBinding(pointer.scroll(leftAndUp)); + await tester.pump(); + + final barChartLeafAfterPan = tester.widget( + find.byType(BarChartLeaf), + ); + final chartVirtualRectAfterPan = + barChartLeafAfterPan.chartVirtualRect!; + + expect( + chartVirtualRectAfterPan.left, + greaterThan(chartVirtualRectBeforePan.left), + ); + expect(chartVirtualRectAfterPan.top, 0); + }); + + testWidgets('vertically with FlScaleAxis.vertical', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: BarChart( + BarChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.vertical, + ), + ), + ), + ); + + final chartCenterOffset = tester.getCenter(find.byType(BarChartLeaf)); + final scaleStart1 = chartCenterOffset; + final scaleStart2 = chartCenterOffset; + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await tester.pump(); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await tester.pump(); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final barChartLeafBeforePan = tester.widget( + find.byType(BarChartLeaf), + ); + + final chartVirtualRectBeforePan = + barChartLeafBeforePan.chartVirtualRect!; + expect(chartVirtualRectBeforePan.left, 0); + + final pointer = TestPointer(1, PointerDeviceKind.trackpad); + const leftAndUp = Offset(-100, -100); + await tester.sendEventToBinding(pointer.hover(chartCenterOffset)); + await tester.pump(); + await tester.sendEventToBinding(pointer.scroll(leftAndUp)); + await tester.pump(); + + final barChartLeafAfterPan = tester.widget( + find.byType(BarChartLeaf), + ); + final chartVirtualRectAfterPan = + barChartLeafAfterPan.chartVirtualRect!; + + expect(chartVirtualRectAfterPan.left, 0); + expect( + chartVirtualRectAfterPan.top, + greaterThan(chartVirtualRectBeforePan.top), + ); + }); + + testWidgets('freely with FlScaleAxis.free', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: BarChart( + BarChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.free, + ), + ), + ), + ); + + final chartCenterOffset = tester.getCenter(find.byType(BarChartLeaf)); + final scaleStart1 = chartCenterOffset; + final scaleStart2 = chartCenterOffset; + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await tester.pump(); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await tester.pump(); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final barChartLeafBeforePan = tester.widget( + find.byType(BarChartLeaf), + ); + + final chartVirtualRectBeforePan = + barChartLeafBeforePan.chartVirtualRect!; + + final pointer = TestPointer(1, PointerDeviceKind.trackpad); + const leftAndUp = Offset(-100, -100); + await tester.sendEventToBinding(pointer.hover(chartCenterOffset)); + await tester.pump(); + await tester.sendEventToBinding(pointer.scroll(leftAndUp)); + await tester.pump(); + + final barChartLeafAfterPan = tester.widget( + find.byType(BarChartLeaf), + ); + final chartVirtualRectAfterPan = + barChartLeafAfterPan.chartVirtualRect!; + + expect( + chartVirtualRectAfterPan.left, + greaterThan(chartVirtualRectBeforePan.left), + ); + expect( + chartVirtualRectAfterPan.top, + greaterThan(chartVirtualRectBeforePan.top), + ); + }); + }); + + testWidgets( + 'does not scale with FlScaleAxis.none when ' + 'trackpadScrollCausesScale is true', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: BarChart( + BarChartData(), + transformationConfig: const FlTransformationConfig( + // This is for test + // ignore: avoid_redundant_argument_values + scaleAxis: FlScaleAxis.none, + trackpadScrollCausesScale: true, + ), + ), + ), + ); + + final pointer = TestPointer(1, PointerDeviceKind.trackpad); + final chartCenterOffset = tester.getCenter(find.byType(BarChartLeaf)); + const scrollAmount = Offset(0, -100); + + await tester.sendEventToBinding(pointer.hover(chartCenterOffset)); + await tester.pump(); + await tester.sendEventToBinding(pointer.scroll(scrollAmount)); + await tester.pump(); + + final barChartLeaf = tester.widget( + find.byType(BarChartLeaf), + ); + expect(barChartLeaf.chartVirtualRect, null); + }, + ); + + for (final scaleAxis in FlScaleAxis.scalingEnabledAxis) { + testWidgets( + 'does not scale when trackpadScrollCausesScale is false ' + 'for $scaleAxis', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: BarChart( + BarChartData(), + transformationConfig: FlTransformationConfig( + scaleAxis: scaleAxis, + // This is for test + // ignore: avoid_redundant_argument_values + trackpadScrollCausesScale: false, + ), + ), + ), + ); + + final pointer = TestPointer(1, PointerDeviceKind.trackpad); + final chartCenterOffset = tester.getCenter( + find.byType(BarChartLeaf), + ); + const scrollAmount = Offset(0, -100); + + await tester.sendEventToBinding(pointer.hover(chartCenterOffset)); + await tester.pump(); + await tester.sendEventToBinding(pointer.scroll(scrollAmount)); + await tester.pump(); + + final barChartLeaf = tester.widget( + find.byType(BarChartLeaf), + ); + expect(barChartLeaf.chartVirtualRect, null); + }, + ); + } + + testWidgets('scales horizontally with FlScaleAxis.horizontal', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: BarChart( + BarChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.horizontal, + trackpadScrollCausesScale: true, + ), + ), + ), + ); + + final pointer = TestPointer(1, PointerDeviceKind.trackpad); + final chartCenterOffset = tester.getCenter(find.byType(BarChartLeaf)); + const scrollAmount = Offset(0, -100); + + await tester.sendEventToBinding(pointer.hover(chartCenterOffset)); + await tester.pump(); + await tester.sendEventToBinding(pointer.scroll(scrollAmount)); + await tester.pump(); + + final barChartLeaf = tester.widget( + find.byType(BarChartLeaf), + ); + final renderBox = tester.renderObject( + find.byType(BarChartLeaf), + ); + final chartVirtualRect = barChartLeaf.chartVirtualRect!; + + expect(chartVirtualRect.size.height, renderBox.size.height); + expect(chartVirtualRect.size.width, greaterThan(renderBox.size.width)); + expect(chartVirtualRect.left, isNegative); + expect(chartVirtualRect.top, 0); + }); + + testWidgets('scales vertically with FlScaleAxis.vertical', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: BarChart( + BarChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.vertical, + trackpadScrollCausesScale: true, + ), + ), + ), + ); + + final pointer = TestPointer(1, PointerDeviceKind.trackpad); + final chartCenterOffset = tester.getCenter(find.byType(BarChartLeaf)); + const scrollAmount = Offset(0, -100); + + await tester.sendEventToBinding(pointer.hover(chartCenterOffset)); + await tester.pump(); + await tester.sendEventToBinding(pointer.scroll(scrollAmount)); + await tester.pump(); + + final barChartLeaf = tester.widget( + find.byType(BarChartLeaf), + ); + final renderBox = tester.renderObject( + find.byType(BarChartLeaf), + ); + final chartVirtualRect = barChartLeaf.chartVirtualRect!; + + expect( + chartVirtualRect.size.height, + greaterThan(renderBox.size.height), + ); + expect(chartVirtualRect.size.width, renderBox.size.width); + expect(chartVirtualRect.left, 0); + expect(chartVirtualRect.top, isNegative); + }); + + testWidgets('scales freely with FlScaleAxis.free', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: BarChart( + BarChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.free, + trackpadScrollCausesScale: true, + ), + ), + ), + ); + + final pointer = TestPointer(1, PointerDeviceKind.trackpad); + final chartCenterOffset = tester.getCenter(find.byType(BarChartLeaf)); + const scrollAmount = Offset(0, -100); + + await tester.sendEventToBinding(pointer.hover(chartCenterOffset)); + await tester.pump(); + await tester.sendEventToBinding(pointer.scroll(scrollAmount)); + await tester.pump(); + + final barChartLeaf = + tester.widget(find.byType(BarChartLeaf)); + final renderBox = tester.renderObject( + find.byType(BarChartLeaf), + ); + final chartVirtualRect = barChartLeaf.chartVirtualRect!; + + expect(chartVirtualRect.size, greaterThan(renderBox.size)); + expect(chartVirtualRect.left, isNegative); + expect(chartVirtualRect.top, isNegative); + }); + }); + }); +} diff --git a/test/chart/base/axis_chart/axis_chart_data_test.dart b/test/chart/base/axis_chart/axis_chart_data_test.dart new file mode 100644 index 0000000..1775c89 --- /dev/null +++ b/test/chart/base/axis_chart/axis_chart_data_test.dart @@ -0,0 +1,387 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import '../../data_pool.dart'; +import 'axis_chart_data_test.mocks.dart'; + +@GenerateMocks([Canvas]) +void main() { + group('AxisChartData data equality check', () { + test('AxisTitle equality test', () { + expect(MockData.axisTitles1 == MockData.axisTitles1Clone, true); + expect(MockData.axisTitles1 == MockData.axisTitles2, false); + expect(MockData.axisTitles1 == MockData.axisTitles3, false); + expect(MockData.axisTitles1 == MockData.axisTitles4, false); + expect(MockData.axisTitles1 == MockData.axisTitles5, false); + }); + + test('FlTitlesData equality test', () { + expect(MockData.flTitlesData1 == MockData.flTitlesData1Clone, true); + expect(MockData.flTitlesData1 == MockData.flTitlesData2, false); + expect(MockData.flTitlesData1 == MockData.flTitlesData3, false); + expect(MockData.flTitlesData1 == MockData.flTitlesData4, false); + expect(MockData.flTitlesData1 == MockData.flTitlesData5, false); + expect(MockData.flTitlesData1 == MockData.flTitlesData6, false); + }); + + test('SideTitles equality test', () { + expect(MockData.sideTitles1 == MockData.sideTitles1Clone, true); + expect(MockData.sideTitles1 == MockData.sideTitles2, false); + expect(MockData.sideTitles1 == MockData.sideTitles3, false); + expect(MockData.sideTitles1 == MockData.sideTitles4, false); + expect(MockData.sideTitles1 == MockData.sideTitles5, false); + expect(MockData.sideTitles1 == MockData.sideTitles6, false); + }); + + test('SideTitleFitInsideData equality test', () { + expect( + MockData.sideTitleFitInsideData1 == + MockData.sideTitleFitInsideData1Clone, + true, + ); + expect( + MockData.sideTitleFitInsideData1 == MockData.sideTitleFitInsideData2, + false, + ); + expect( + MockData.sideTitleFitInsideData1 == MockData.sideTitleFitInsideData3, + false, + ); + expect( + MockData.sideTitleFitInsideData1 == MockData.sideTitleFitInsideData4, + false, + ); + expect( + MockData.sideTitleFitInsideData1 == MockData.sideTitleFitInsideData5, + false, + ); + expect( + MockData.sideTitleFitInsideData1 == MockData.sideTitleFitInsideData6, + false, + ); + }); + + test('FlSpot equality test', () { + expect(flSpot1 == flSpot1Clone, true); + + expect(flSpot1 == flSpot2, false); + + expect(flSpot2 == flSpot2Clone, true); + + expect(nullSpot1 == nullSpot2, true); + + expect(nullSpot2 == nullSpot3, true); + + expect(nullSpot1 == nullSpot3, true); + }); + + test('FlGridData equality test', () { + expect(flGridData1 == flGridData1Clone, true); + expect(flGridData1 == flGridData2, false); + expect(flGridData1 == flGridData3, false); + expect(flGridData1 == flGridData4, false); + expect(flGridData1 == flGridData5, false); + }); + + test('FlLine equality test', () { + expect(flLine1 == flLine1Clone, true); + + expect( + flLine1 == + const FlLine( + color: Colors.green, + strokeWidth: 1.001, + dashArray: [1, 2, 3], + ), + false, + ); + + expect( + flLine1 == + const FlLine( + color: Colors.green, + strokeWidth: 1, + dashArray: [ + 1, + ], + ), + false, + ); + + expect( + flLine1 == + const FlLine(color: Colors.green, strokeWidth: 1, dashArray: []), + false, + ); + + expect( + flLine1 == const FlLine(color: Colors.green, strokeWidth: 1), + false, + ); + + expect( + flLine1 == + const FlLine( + color: Colors.white, + strokeWidth: 1, + dashArray: [1, 2, 3], + ), + false, + ); + + expect( + flLine1 == + const FlLine( + color: Colors.green, + strokeWidth: 100, + dashArray: [1, 2, 3], + ), + false, + ); + }); + + test('RangeAnnotations equality test', () { + expect(rangeAnnotations1 == rangeAnnotations1Clone, true); + + expect(rangeAnnotations1 == rangeAnnotations2, false); + + expect( + rangeAnnotations1 == + RangeAnnotations( + horizontalRangeAnnotations: [ + horizontalRangeAnnotation1Clone, + horizontalRangeAnnotation1, + ], + verticalRangeAnnotations: [ + verticalRangeAnnotation1Clone, + verticalRangeAnnotation1, + ], + ), + true, + ); + + expect( + rangeAnnotations1 == + RangeAnnotations( + horizontalRangeAnnotations: [ + horizontalRangeAnnotation1Clone, + ], + verticalRangeAnnotations: [ + verticalRangeAnnotation1Clone, + ], + ), + false, + ); + + expect( + rangeAnnotations1 == + RangeAnnotations( + horizontalRangeAnnotations: [], + verticalRangeAnnotations: [ + verticalRangeAnnotation1, + verticalRangeAnnotation1Clone, + ], + ), + false, + ); + + expect( + rangeAnnotations1 == + RangeAnnotations( + horizontalRangeAnnotations: [ + horizontalRangeAnnotation1, + horizontalRangeAnnotation1Clone, + ], + verticalRangeAnnotations: [ + verticalRangeAnnotation1, + VerticalRangeAnnotation( + color: Colors.green, + x2: 12.01, + x1: 12.1, + ), + ], + ), + false, + ); + }); + + test('HorizontalRangeAnnotation equality test', () { + expect( + horizontalRangeAnnotation1 == horizontalRangeAnnotation1Clone, + true, + ); + + expect( + horizontalRangeAnnotation1 == + HorizontalRangeAnnotation( + color: Colors.green, + y2: 12.1, + y1: 12.1, + ), + false, + ); + + expect( + horizontalRangeAnnotation1 == + HorizontalRangeAnnotation( + color: Colors.green, + y2: 12, + y1: 12.1, + ), + true, + ); + + expect( + horizontalRangeAnnotation1 == + HorizontalRangeAnnotation( + color: Colors.green, + y2: 12.1, + y1: 12, + ), + false, + ); + + expect( + horizontalRangeAnnotation1 == + HorizontalRangeAnnotation( + color: Colors.green.withValues(alpha: 0.5), + y2: 12, + y1: 12.1, + ), + false, + ); + }); + + test('VerticalRangeAnnotation equality test', () { + expect(verticalRangeAnnotation1 == verticalRangeAnnotation1Clone, true); + + expect( + verticalRangeAnnotation1 == + VerticalRangeAnnotation(color: Colors.green, x2: 12.1, x1: 12.1), + false, + ); + + expect( + verticalRangeAnnotation1 == + VerticalRangeAnnotation(color: Colors.green, x2: 12, x1: 12.1), + true, + ); + + expect( + verticalRangeAnnotation1 == + VerticalRangeAnnotation(color: Colors.green, x2: 12.1, x1: 12), + false, + ); + + expect( + verticalRangeAnnotation1 == + VerticalRangeAnnotation( + color: Colors.green.withValues(alpha: 0.5), + x2: 12, + x1: 12.1, + ), + false, + ); + }); + + test('FlSpotErrorRangePainter equality', () { + final FlSpotErrorRangePainter painter1 = FlSimpleErrorPainter(); + final painter2 = FlSimpleErrorPainter(); + final painter3 = FlSimpleErrorPainter( + lineWidth: 1.1, + ); + expect(painter1 == painter2, true); + expect(painter1 != painter3, true); + }); + + test('FlSpotErrorRangePainter render functionality (without texts)', () { + final painter = FlSimpleErrorPainter( + lineWidth: 5.3, + lineColor: Colors.green, + capLength: 10, + ); + final mockCanvas = MockCanvas(); + const offsetInCanvas = Offset(24, 34); + const origin = FlSpot(4, 1); + + painter.draw( + mockCanvas, + offsetInCanvas, + origin, + const Rect.fromLTWH(0, 4, 0, 6), + LineChartData(), + ); + verify( + mockCanvas.drawLine( + captureAny, + captureAny, + captureAny, + ), + ).called(3); + + painter.draw( + mockCanvas, + offsetInCanvas, + origin, + const Rect.fromLTWH(4, 4, 6, 6), + LineChartData(), + ); + final result = verify( + mockCanvas.drawLine( + captureAny, + captureAny, + any, + ), + )..called(6); + expect(result.captured[0], const Offset(24, 38)); + expect(result.captured[1], const Offset(24, 44)); + expect(result.captured[2], const Offset(19, 38)); + expect(result.captured[3], const Offset(29, 38)); + expect(result.captured[4], const Offset(19, 44)); + expect(result.captured[5], const Offset(29, 44)); + expect(result.captured[6], const Offset(28, 34)); + expect(result.captured[7], const Offset(34, 34)); + expect(result.captured[8], const Offset(28, 29)); + expect(result.captured[9], const Offset(28, 39)); + + verifyNever(mockCanvas.drawParagraph(any, any)); + }); + + test('FlSpotErrorRangePainter render functionality (with texts)', () { + final painter = FlSimpleErrorPainter( + showErrorTexts: true, + errorTextDirection: TextDirection.rtl, + errorTextStyle: const TextStyle( + color: Colors.red, + fontSize: 12, + ), + ); + final mockCanvas = MockCanvas(); + + painter.draw( + mockCanvas, + const Offset(24, 34), + const FlSpot( + 4, + 1, + xError: FlErrorRange.symmetric(1), + yError: FlErrorRange.symmetric(1), + ), + const Rect.fromLTWH(4, 4, 6, 6), + LineChartData(), + ); + verify( + mockCanvas.drawLine( + captureAny, + captureAny, + any, + ), + ).called(6); + verify( + mockCanvas.drawParagraph(captureAny, captureAny), + ).called(4); + }); + }); +} diff --git a/test/chart/base/axis_chart/axis_chart_data_test.mocks.dart b/test/chart/base/axis_chart/axis_chart_data_test.mocks.dart new file mode 100644 index 0000000..cc31835 --- /dev/null +++ b/test/chart/base/axis_chart/axis_chart_data_test.mocks.dart @@ -0,0 +1,362 @@ +// Mocks generated by Mockito 5.4.5 from annotations +// in fl_chart/test/chart/base/axis_chart/axis_chart_data_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:typed_data' as _i3; +import 'dart:ui' as _i2; + +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeRect_0 extends _i1.SmartFake implements _i2.Rect { + _FakeRect_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +/// A class which mocks [Canvas]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCanvas extends _i1.Mock implements _i2.Canvas { + MockCanvas() { + _i1.throwOnMissingStub(this); + } + + @override + void save() => super.noSuchMethod( + Invocation.method(#save, []), + returnValueForMissingStub: null, + ); + + @override + void saveLayer(_i2.Rect? bounds, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#saveLayer, [bounds, paint]), + returnValueForMissingStub: null, + ); + + @override + void restore() => super.noSuchMethod( + Invocation.method(#restore, []), + returnValueForMissingStub: null, + ); + + @override + void restoreToCount(int? count) => super.noSuchMethod( + Invocation.method(#restoreToCount, [count]), + returnValueForMissingStub: null, + ); + + @override + int getSaveCount() => + (super.noSuchMethod(Invocation.method(#getSaveCount, []), returnValue: 0) + as int); + + @override + void translate(double? dx, double? dy) => super.noSuchMethod( + Invocation.method(#translate, [dx, dy]), + returnValueForMissingStub: null, + ); + + @override + void scale(double? sx, [double? sy]) => super.noSuchMethod( + Invocation.method(#scale, [sx, sy]), + returnValueForMissingStub: null, + ); + + @override + void rotate(double? radians) => super.noSuchMethod( + Invocation.method(#rotate, [radians]), + returnValueForMissingStub: null, + ); + + @override + void skew(double? sx, double? sy) => super.noSuchMethod( + Invocation.method(#skew, [sx, sy]), + returnValueForMissingStub: null, + ); + + @override + void transform(_i3.Float64List? matrix4) => super.noSuchMethod( + Invocation.method(#transform, [matrix4]), + returnValueForMissingStub: null, + ); + + @override + _i3.Float64List getTransform() => + (super.noSuchMethod( + Invocation.method(#getTransform, []), + returnValue: _i3.Float64List(0), + ) + as _i3.Float64List); + + @override + void clipRect( + _i2.Rect? rect, { + _i2.ClipOp? clipOp = _i2.ClipOp.intersect, + bool? doAntiAlias = true, + }) => super.noSuchMethod( + Invocation.method( + #clipRect, + [rect], + {#clipOp: clipOp, #doAntiAlias: doAntiAlias}, + ), + returnValueForMissingStub: null, + ); + + @override + void clipRRect(_i2.RRect? rrect, {bool? doAntiAlias = true}) => + super.noSuchMethod( + Invocation.method(#clipRRect, [rrect], {#doAntiAlias: doAntiAlias}), + returnValueForMissingStub: null, + ); + + @override + void clipPath(_i2.Path? path, {bool? doAntiAlias = true}) => + super.noSuchMethod( + Invocation.method(#clipPath, [path], {#doAntiAlias: doAntiAlias}), + returnValueForMissingStub: null, + ); + + @override + _i2.Rect getLocalClipBounds() => + (super.noSuchMethod( + Invocation.method(#getLocalClipBounds, []), + returnValue: _FakeRect_0( + this, + Invocation.method(#getLocalClipBounds, []), + ), + ) + as _i2.Rect); + + @override + _i2.Rect getDestinationClipBounds() => + (super.noSuchMethod( + Invocation.method(#getDestinationClipBounds, []), + returnValue: _FakeRect_0( + this, + Invocation.method(#getDestinationClipBounds, []), + ), + ) + as _i2.Rect); + + @override + void drawColor(_i2.Color? color, _i2.BlendMode? blendMode) => + super.noSuchMethod( + Invocation.method(#drawColor, [color, blendMode]), + returnValueForMissingStub: null, + ); + + @override + void drawLine(_i2.Offset? p1, _i2.Offset? p2, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawLine, [p1, p2, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawPaint(_i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawPaint, [paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRect(_i2.Rect? rect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRect, [rect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRRect(_i2.RRect? rrect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRRect, [rrect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawDRRect(_i2.RRect? outer, _i2.RRect? inner, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawDRRect, [outer, inner, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawOval(_i2.Rect? rect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawOval, [rect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawCircle(_i2.Offset? c, double? radius, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawCircle, [c, radius, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawArc( + _i2.Rect? rect, + double? startAngle, + double? sweepAngle, + bool? useCenter, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawArc, [ + rect, + startAngle, + sweepAngle, + useCenter, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawPath(_i2.Path? path, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawPath, [path, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImage(_i2.Image? image, _i2.Offset? offset, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawImage, [image, offset, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImageRect( + _i2.Image? image, + _i2.Rect? src, + _i2.Rect? dst, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawImageRect, [image, src, dst, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImageNine( + _i2.Image? image, + _i2.Rect? center, + _i2.Rect? dst, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawImageNine, [image, center, dst, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawPicture(_i2.Picture? picture) => super.noSuchMethod( + Invocation.method(#drawPicture, [picture]), + returnValueForMissingStub: null, + ); + + @override + void drawParagraph(_i2.Paragraph? paragraph, _i2.Offset? offset) => + super.noSuchMethod( + Invocation.method(#drawParagraph, [paragraph, offset]), + returnValueForMissingStub: null, + ); + + @override + void drawPoints( + _i2.PointMode? pointMode, + List<_i2.Offset>? points, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawPoints, [pointMode, points, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRawPoints( + _i2.PointMode? pointMode, + _i3.Float32List? points, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawRawPoints, [pointMode, points, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawVertices( + _i2.Vertices? vertices, + _i2.BlendMode? blendMode, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawVertices, [vertices, blendMode, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawAtlas( + _i2.Image? atlas, + List<_i2.RSTransform>? transforms, + List<_i2.Rect>? rects, + List<_i2.Color>? colors, + _i2.BlendMode? blendMode, + _i2.Rect? cullRect, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawAtlas, [ + atlas, + transforms, + rects, + colors, + blendMode, + cullRect, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawRawAtlas( + _i2.Image? atlas, + _i3.Float32List? rstTransforms, + _i3.Float32List? rects, + _i3.Int32List? colors, + _i2.BlendMode? blendMode, + _i2.Rect? cullRect, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawRawAtlas, [ + atlas, + rstTransforms, + rects, + colors, + blendMode, + cullRect, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawShadow( + _i2.Path? path, + _i2.Color? color, + double? elevation, + bool? transparentOccluder, + ) => super.noSuchMethod( + Invocation.method(#drawShadow, [ + path, + color, + elevation, + transparentOccluder, + ]), + returnValueForMissingStub: null, + ); +} diff --git a/test/chart/base/axis_chart/axis_chart_extensions_test.dart b/test/chart/base/axis_chart/axis_chart_extensions_test.dart new file mode 100644 index 0000000..424f432 --- /dev/null +++ b/test/chart/base/axis_chart/axis_chart_extensions_test.dart @@ -0,0 +1,113 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/base/axis_chart/axis_chart_extensions.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../../data_pool.dart'; + +void main() { + group('splitByNullSpots()', () { + test('test 1 - null spots start', () { + final spots = [ + FlSpot.nullSpot, + FlSpot.nullSpot, + FlSpot.nullSpot, + FlSpot.nullSpot, + FlSpot.nullSpot, + MockData.flSpot1, + MockData.flSpot2, + MockData.flSpot3, + ]; + final result = spots.splitByNullSpots(); + expect( + result, + [ + [ + MockData.flSpot1, + MockData.flSpot2, + MockData.flSpot3, + ] + ], + ); + }); + test('test 2 - null spots end', () { + final spots = [ + MockData.flSpot1, + MockData.flSpot2, + MockData.flSpot3, + FlSpot.nullSpot, + FlSpot.nullSpot, + FlSpot.nullSpot, + ]; + final result = spots.splitByNullSpots(); + expect( + result, + [ + [ + MockData.flSpot1, + MockData.flSpot2, + MockData.flSpot3, + ] + ], + ); + }); + test('test 3 - null spots around', () { + final spots = [ + FlSpot.nullSpot, + FlSpot.nullSpot, + FlSpot.nullSpot, + FlSpot.nullSpot, + FlSpot.nullSpot, + MockData.flSpot1, + MockData.flSpot2, + MockData.flSpot3, + FlSpot.nullSpot, + FlSpot.nullSpot, + FlSpot.nullSpot, + ]; + final result = spots.splitByNullSpots(); + expect( + result, + [ + [ + MockData.flSpot1, + MockData.flSpot2, + MockData.flSpot3, + ] + ], + ); + }); + test('test 4 - null spots between', () { + final spots = [ + MockData.flSpot1, + MockData.flSpot2, + FlSpot.nullSpot, + MockData.flSpot3, + FlSpot.nullSpot, + MockData.flSpot4, + MockData.flSpot5, + FlSpot.nullSpot, + MockData.flSpot1, + ]; + final result = spots.splitByNullSpots(); + expect( + result, + [ + [ + MockData.flSpot1, + MockData.flSpot2, + ], + [ + MockData.flSpot3, + ], + [ + MockData.flSpot4, + MockData.flSpot5, + ], + [ + MockData.flSpot1, + ] + ], + ); + }); + }); +} diff --git a/test/chart/base/axis_chart/axis_chart_helper_test.dart b/test/chart/base/axis_chart/axis_chart_helper_test.dart new file mode 100644 index 0000000..8f49e84 --- /dev/null +++ b/test/chart/base/axis_chart/axis_chart_helper_test.dart @@ -0,0 +1,222 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/base/axis_chart/axis_chart_helper.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + const tolerance = 0.0001; + group('iterateThroughAxis()', () { + test('test 1', () { + final results = []; + final axisValues = AxisChartHelper().iterateThroughAxis( + min: 0, + max: 0.1, + interval: 0.001, + baseLine: 0, + ); + for (final axisValue in axisValues) { + results.add(axisValue); + } + expect(results.length, 101); + }); + + test('test 2', () { + final results = []; + final axisValues = AxisChartHelper().iterateThroughAxis( + min: 0, + minIncluded: false, + max: 0.1, + maxIncluded: false, + interval: 0.001, + baseLine: 0, + ); + for (final axisValue in axisValues) { + results.add(axisValue); + } + expect(results.length, 99); + expect(results[0], closeTo(0.001, tolerance)); + expect(results[98], closeTo(0.099, tolerance)); + }); + + test('test 3', () { + final results = []; + final axisValues = AxisChartHelper().iterateThroughAxis( + min: 0, + max: 1000, + interval: 200, + baseLine: 0, + ); + for (final axisValue in axisValues) { + results.add(axisValue); + } + expect(results.length, 6); + expect(results[0], 0); + expect(results[1], 200); + expect(results[2], 400); + expect(results[3], 600); + expect(results[4], 800); + expect(results[5], 1000); + }); + + test('test 4', () { + final results = []; + final axisValues = AxisChartHelper().iterateThroughAxis( + min: 0, + max: 10, + interval: 3, + baseLine: 0, + ); + for (final axisValue in axisValues) { + results.add(axisValue); + } + expect(results.length, 5); + expect(results[0], 0); + expect(results[1], 3); + expect(results[2], 6); + expect(results[3], 9); + expect(results[4], 10); + }); + + test('test 5', () { + final results = []; + final axisValues = AxisChartHelper().iterateThroughAxis( + min: 0, + minIncluded: false, + max: 10, + maxIncluded: false, + interval: 3, + baseLine: 0, + ); + for (final axisValue in axisValues) { + results.add(axisValue); + } + expect(results.length, 3); + expect(results[0], 3); + expect(results[1], 6); + expect(results[2], 9); + }); + + test('test 6', () { + final results = []; + final axisValues = AxisChartHelper().iterateThroughAxis( + min: 35, + max: 130, + interval: 50, + baseLine: 0, + ); + for (final axisValue in axisValues) { + results.add(axisValue); + } + expect(results.length, 4); + expect(results[0], 35); + expect(results[1], 50); + expect(results[2], 100); + expect(results[3], 130); + }); + + test('test 7', () { + final results = []; + final axisValues = AxisChartHelper().iterateThroughAxis( + min: 5, + max: 35, + interval: 10, + baseLine: 5, + ); + for (final axisValue in axisValues) { + results.add(axisValue); + } + expect(results.length, 4); + expect(results[0], 5); + expect(results[1], 15); + expect(results[2], 25); + expect(results[3], 35); + }); + }); + + group('calcFitInsideOffset', () { + group('not overflowed', () { + test('vertical axis', () { + const result = Offset.zero; + + final offset = AxisChartHelper().calcFitInsideOffset( + axisSide: AxisSide.left, + childSize: 10, + parentAxisSize: 100, + axisPosition: 20, + distanceFromEdge: 0, + ); + + expect(offset, result); + }); + + test('horizontal axis', () { + const result = Offset.zero; + + final offset = AxisChartHelper().calcFitInsideOffset( + axisSide: AxisSide.bottom, + childSize: 10, + parentAxisSize: 100, + axisPosition: 20, + distanceFromEdge: 0, + ); + + expect(offset, result); + }); + }); + + group('overflowed', () { + test('vertical axis at start', () { + const result = Offset(0, 5); + + final offset = AxisChartHelper().calcFitInsideOffset( + axisSide: AxisSide.left, + childSize: 10, + parentAxisSize: 100, + axisPosition: 0, + distanceFromEdge: 0, + ); + + expect(offset, result); + }); + test('vertical axis at end', () { + const result = Offset(0, -5); + + final offset = AxisChartHelper().calcFitInsideOffset( + axisSide: AxisSide.left, + childSize: 10, + parentAxisSize: 100, + axisPosition: 100, + distanceFromEdge: 0, + ); + + expect(offset, result); + }); + + test('horizontal axis at start', () { + const result = Offset(5, 0); + + final offset = AxisChartHelper().calcFitInsideOffset( + axisSide: AxisSide.bottom, + childSize: 10, + parentAxisSize: 100, + axisPosition: 0, + distanceFromEdge: 0, + ); + + expect(offset, result); + }); + test('horizontal axis at end', () { + const result = Offset(-5, 0); + + final offset = AxisChartHelper().calcFitInsideOffset( + axisSide: AxisSide.bottom, + childSize: 10, + parentAxisSize: 100, + axisPosition: 100, + distanceFromEdge: 0, + ); + + expect(offset, result); + }); + }); + }); +} diff --git a/test/chart/base/axis_chart/axis_chart_scaffold_widget_test.dart b/test/chart/base/axis_chart/axis_chart_scaffold_widget_test.dart new file mode 100644 index 0000000..74cf98b --- /dev/null +++ b/test/chart/base/axis_chart/axis_chart_scaffold_widget_test.dart @@ -0,0 +1,1527 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/base/axis_chart/axis_chart_scaffold_widget.dart'; +import 'package:fl_chart/src/chart/base/axis_chart/side_titles/side_titles_widget.dart'; +import 'package:fl_chart/src/chart/base/custom_interactive_viewer.dart'; +import 'package:fl_chart/src/extensions/size_extension.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + const Rect? isNotScaled = null; + final isScaled = isA(); + + const viewSize = Size(400, 400); + + const dummyChartKey = Key('chart'); + const dummyChart = SizedBox(key: dummyChartKey); + + final lineChartDataBase = LineChartData( + minX: 0, + maxX: 10, + minY: 0, + maxY: 10, + ); + + final lineChartDataWithNoTitles = lineChartDataBase.copyWith( + titlesData: const FlTitlesData( + show: false, + leftTitles: AxisTitles(), + topTitles: AxisTitles(), + rightTitles: AxisTitles(), + bottomTitles: AxisTitles(), + ), + borderData: FlBorderData(show: false), + ); + + final lineChartDataWithAllTitles = lineChartDataBase.copyWith( + borderData: FlBorderData( + show: true, + border: Border.all( + color: Colors.red, + width: 10, + ), + ), + titlesData: FlTitlesData( + leftTitles: AxisTitles( + axisNameWidget: const Icon(Icons.arrow_left), + axisNameSize: 10, + sideTitles: SideTitles( + showTitles: true, + reservedSize: 10, + getTitlesWidget: (double value, TitleMeta meta) { + return Text('L-${value.toInt()}'); + }, + interval: 1, + ), + ), + topTitles: AxisTitles( + axisNameWidget: const Icon(Icons.arrow_drop_up), + axisNameSize: 20, + sideTitles: SideTitles( + showTitles: true, + reservedSize: 20, + getTitlesWidget: (double value, TitleMeta meta) { + return Text('T-${value.toInt()}'); + }, + interval: 1, + ), + ), + rightTitles: AxisTitles( + axisNameWidget: const Icon(Icons.arrow_right), + axisNameSize: 30, + sideTitles: SideTitles( + showTitles: true, + reservedSize: 30, + getTitlesWidget: (double value, TitleMeta meta) { + return Text('R-${value.toInt()}'); + }, + interval: 1, + ), + ), + bottomTitles: AxisTitles( + axisNameWidget: const Icon(Icons.arrow_drop_down), + axisNameSize: 40, + sideTitles: SideTitles( + showTitles: true, + reservedSize: 40, + getTitlesWidget: (double value, TitleMeta meta) { + return Text('B-${value.toInt()}'); + }, + interval: 1, + ), + ), + ), + ); + + final lineChartDataWithOnlyLeftTitles = lineChartDataBase.copyWith( + borderData: FlBorderData( + show: true, + border: const Border( + left: BorderSide( + color: Colors.red, + width: 6, + ), + ), + ), + titlesData: FlTitlesData( + leftTitles: AxisTitles( + axisNameWidget: const Icon(Icons.arrow_left), + axisNameSize: 10, + sideTitles: SideTitles( + showTitles: true, + reservedSize: 10, + getTitlesWidget: (double value, TitleMeta meta) { + return Text('L-${value.toInt()}'); + }, + interval: 1, + ), + ), + topTitles: const AxisTitles(), + rightTitles: const AxisTitles(), + bottomTitles: const AxisTitles(), + ), + ); + + final lineChartDataWithOnlyLeftTitlesWithoutAxisName = + lineChartDataBase.copyWith( + borderData: FlBorderData(show: false), + titlesData: FlTitlesData( + leftTitles: AxisTitles( + axisNameSize: 10, + sideTitles: SideTitles( + showTitles: true, + reservedSize: 10, + getTitlesWidget: (double value, TitleMeta meta) { + return Text('L-${value.toInt()}'); + }, + interval: 1, + ), + ), + topTitles: const AxisTitles(), + rightTitles: const AxisTitles(), + bottomTitles: const AxisTitles(), + ), + ); + + final lineChartDataWithOnlyLeftAxisNameWithoutSideTitles = + lineChartDataBase.copyWith( + borderData: FlBorderData(show: false), + titlesData: FlTitlesData( + leftTitles: AxisTitles( + axisNameSize: 10, + axisNameWidget: const Icon(Icons.arrow_left), + sideTitles: SideTitles( + reservedSize: 10, + getTitlesWidget: (double value, TitleMeta meta) { + return Text('L-${value.toInt()}'); + }, + interval: 1, + ), + ), + topTitles: const AxisTitles(), + rightTitles: const AxisTitles(), + bottomTitles: const AxisTitles(), + ), + ); + + testWidgets( + 'LineChart with no titles', + (WidgetTester tester) async { + Size? chartDrawingSize; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: viewSize.width, + height: viewSize.height, + child: AxisChartScaffoldWidget( + chartBuilder: (context, chartVirtualRect) => LayoutBuilder( + builder: (context, constraints) { + chartDrawingSize = constraints.biggest; + return const ColoredBox( + color: Colors.red, + ); + }, + ), + data: lineChartDataWithNoTitles, + ), + ), + ), + ), + ), + ); + expect(chartDrawingSize, viewSize); + expect(find.byType(Text), findsNothing); + expect(find.byType(Icon), findsNothing); + }, + ); + + testWidgets( + 'LineChart with all titles', + (WidgetTester tester) async { + Size? chartDrawingSize; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: viewSize.width, + height: viewSize.height, + child: AxisChartScaffoldWidget( + chartBuilder: (context, chartVirtualRect) => LayoutBuilder( + builder: (context, constraints) { + chartDrawingSize = constraints.biggest; + return const ColoredBox( + color: Colors.red, + ); + }, + ), + data: lineChartDataWithAllTitles, + ), + ), + ), + ), + ), + ); + + Future checkSide(AxisSide side) async { + final axisChar = switch (side) { + AxisSide.left => 'L', + AxisSide.top => 'T', + AxisSide.right => 'R', + AxisSide.bottom => 'B', + }; + for (var i = 0; i <= 10; i++) { + expect(find.text('$axisChar-$i'), findsOneWidget); + } + } + + expect(chartDrawingSize, const Size(300, 260)); + expect(find.byIcon(Icons.arrow_left), findsOneWidget); + await checkSide(AxisSide.left); + + expect(find.byIcon(Icons.arrow_drop_up), findsOneWidget); + await checkSide(AxisSide.top); + + expect(find.byIcon(Icons.arrow_right), findsOneWidget); + await checkSide(AxisSide.right); + + expect(find.byIcon(Icons.arrow_drop_down), findsOneWidget); + await checkSide(AxisSide.bottom); + + expect(find.byType(Text), findsNWidgets(44)); + expect(find.byType(Icon), findsNWidgets(4)); + }, + ); + + testWidgets( + 'LineChart with only left titles', + (WidgetTester tester) async { + Size? chartDrawingSize; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: viewSize.width, + height: viewSize.height, + child: AxisChartScaffoldWidget( + chartBuilder: (context, chartVirtualRect) => LayoutBuilder( + builder: (context, constraints) { + chartDrawingSize = constraints.biggest; + return const ColoredBox( + color: Colors.red, + ); + }, + ), + data: lineChartDataWithOnlyLeftTitles, + ), + ), + ), + ), + ), + ); + + expect(chartDrawingSize, const Size(374, 400)); + expect(find.byIcon(Icons.arrow_left), findsOneWidget); + for (var i = 0; i <= 10; i++) { + expect(find.text('L-$i'), findsOneWidget); + } + + expect(find.byType(Text), findsNWidgets(11)); + expect(find.byType(Icon), findsNWidgets(1)); + }, + ); + + testWidgets( + 'LineChart with only left titles without axis name', + (WidgetTester tester) async { + Size? chartDrawingSize; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: viewSize.width, + height: viewSize.height, + child: AxisChartScaffoldWidget( + chartBuilder: (context, chartVirtualRect) => LayoutBuilder( + builder: (context, constraints) { + chartDrawingSize = constraints.biggest; + return const ColoredBox( + color: Colors.red, + ); + }, + ), + data: lineChartDataWithOnlyLeftTitlesWithoutAxisName, + ), + ), + ), + ), + ), + ); + + expect(chartDrawingSize, const Size(390, 400)); + for (var i = 0; i <= 10; i++) { + expect(find.text('L-$i'), findsOneWidget); + } + + expect(find.byType(Text), findsNWidgets(11)); + expect(find.byType(Icon), findsNothing); + }, + ); + + testWidgets( + 'LineChart with only left axis name without side titles', + (WidgetTester tester) async { + Size? chartDrawingSize; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: viewSize.width, + height: viewSize.height, + child: AxisChartScaffoldWidget( + chartBuilder: (context, chartVirtualRect) => LayoutBuilder( + builder: (context, constraints) { + chartDrawingSize = constraints.biggest; + return const ColoredBox( + color: Colors.red, + ); + }, + ), + data: lineChartDataWithOnlyLeftAxisNameWithoutSideTitles, + ), + ), + ), + ), + ), + ); + + expect(chartDrawingSize, const Size(390, 400)); + expect(find.byType(Text), findsNothing); + expect(find.byType(Icon), findsOneWidget); + }, + ); + + testWidgets( + 'LineChart with rotationQuarterTurns', + (WidgetTester tester) async { + for (var rotationTurns = 0; rotationTurns <= 8; rotationTurns++) { + Size? chartDrawingSize; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: 400, + height: 200, + child: AxisChartScaffoldWidget( + chartBuilder: (context, chartVirtualRect) => LayoutBuilder( + builder: (context, constraints) { + chartDrawingSize = constraints.biggest; + return const ColoredBox( + color: Colors.red, + ); + }, + ), + data: lineChartDataWithNoTitles.copyWith( + rotationQuarterTurns: rotationTurns, + ), + ), + ), + ), + ), + ), + ); + expect( + chartDrawingSize, + const Size(400, 200).rotateByQuarterTurns(rotationTurns), + ); + final types = find.byType(RotatedBox); + final rotatedBox = tester.widget(types); + expect(rotatedBox.quarterTurns, rotationTurns); + expect(types, findsOne); + } + }, + ); + + group('AxisChartScaffoldWidget', () { + for (final scaleAxis in FlScaleAxis.scalingEnabledAxis) { + testWidgets( + 'wraps chart in interactive viewer when scaling is $scaleAxis', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: viewSize.width, + height: viewSize.height, + child: AxisChartScaffoldWidget( + data: lineChartDataWithAllTitles, + transformationConfig: FlTransformationConfig( + scaleAxis: scaleAxis, + ), + chartBuilder: (context, chartVirtualRect) => dummyChart, + ), + ), + ), + ), + ), + ); + + final interactiveViewer = find.ancestor( + of: find.byKey(dummyChartKey), + matching: find.byType(CustomInteractiveViewer), + ); + expect(interactiveViewer, findsOneWidget); + }, + ); + } + + testWidgets( + 'does not wrap chart in interactive viewer when scaling is disabled', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: viewSize.width, + height: viewSize.height, + child: AxisChartScaffoldWidget( + data: lineChartDataWithAllTitles, + // This is for test + // ignore: avoid_redundant_argument_values + transformationConfig: const FlTransformationConfig( + // This is for test + // ignore: avoid_redundant_argument_values + scaleAxis: FlScaleAxis.none, + ), + chartBuilder: (context, chartVirtualRect) => dummyChart, + ), + ), + ), + ), + ), + ); + + final interactiveViewer = find.ancestor( + of: find.byKey(dummyChartKey), + matching: find.byType(CustomInteractiveViewer), + ); + expect(interactiveViewer, findsNothing); + }, + ); + + testWidgets('passes interaction parameters to interactive viewer', + (WidgetTester tester) async { + Future pumpTestWidget(AxisChartScaffoldWidget widget) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: viewSize.width, + height: viewSize.height, + child: widget, + ), + ), + ), + ), + ); + } + + await pumpTestWidget( + AxisChartScaffoldWidget( + data: lineChartDataWithAllTitles, + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.free, + ), + chartBuilder: (context, chartVirtualRect) => dummyChart, + ), + ); + + final interactiveViewer1 = tester.widget( + find.byType(CustomInteractiveViewer), + ); + + expect(interactiveViewer1.trackpadScrollCausesScale, false); + expect(interactiveViewer1.maxScale, 2.5); + expect(interactiveViewer1.minScale, 1); + expect(interactiveViewer1.clipBehavior, Clip.none); + expect(interactiveViewer1.panEnabled, true); + expect(interactiveViewer1.scaleEnabled, true); + expect( + interactiveViewer1.transformationController, + isA().having( + (controller) => controller.value, + 'value', + Matrix4.identity(), + ), + ); + + final transformationController = TransformationController(); + await pumpTestWidget( + AxisChartScaffoldWidget( + data: lineChartDataWithAllTitles, + transformationConfig: FlTransformationConfig( + scaleAxis: FlScaleAxis.free, + trackpadScrollCausesScale: true, + maxScale: 10, + minScale: 1.5, + panEnabled: false, + scaleEnabled: false, + transformationController: transformationController, + ), + chartBuilder: (context, chartVirtualRect) => dummyChart, + ), + ); + + final interactiveViewer2 = tester.widget( + find.byType(CustomInteractiveViewer), + ); + expect(interactiveViewer2.trackpadScrollCausesScale, true); + expect(interactiveViewer2.maxScale, 10); + expect(interactiveViewer2.minScale, 1.5); + expect(interactiveViewer2.clipBehavior, Clip.none); + expect(interactiveViewer2.panEnabled, false); + expect(interactiveViewer2.scaleEnabled, false); + expect( + interactiveViewer2.transformationController, + transformationController, + ); + }); + + testWidgets('asserts minScale is greater than 1', + (WidgetTester tester) async { + expect( + () => AxisChartScaffoldWidget( + data: lineChartDataWithAllTitles, + transformationConfig: FlTransformationConfig( + scaleAxis: FlScaleAxis.free, + minScale: 0.5, + ), + chartBuilder: (context, chartVirtualRect) => dummyChart, + ), + throwsAssertionError, + ); + }); + + testWidgets('asserts maxScale is greater than or equal to minScale', + (WidgetTester tester) async { + expect( + () => AxisChartScaffoldWidget( + data: lineChartDataWithAllTitles, + transformationConfig: FlTransformationConfig( + scaleAxis: FlScaleAxis.free, + maxScale: 0.5, + ), + chartBuilder: (context, chartVirtualRect) => dummyChart, + ), + throwsAssertionError, + ); + }); + + group('scaling and panning', () { + group('touch gesture', () { + testWidgets('does not scale with FlScaleAxis.none', + (WidgetTester tester) async { + Rect? chartVirtualRect; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: viewSize.width, + height: viewSize.height, + child: AxisChartScaffoldWidget( + chartBuilder: (context, rect) { + chartVirtualRect = rect; + return dummyChart; + }, + data: lineChartDataWithNoTitles, + ), + ), + ), + ), + ), + ); + + final chartCenterOffset = tester.getCenter(find.byKey(dummyChartKey)); + final scaleStart1 = chartCenterOffset; + final scaleStart2 = chartCenterOffset; + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await tester.pump(); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await tester.pump(); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + expect(chartVirtualRect, isNull); + }); + + testWidgets('scales freely with FlScaleAxis.free', + (WidgetTester tester) async { + Rect? chartVirtualRect; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: viewSize.width, + height: viewSize.height, + child: AxisChartScaffoldWidget( + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.free, + ), + chartBuilder: (context, rect) { + chartVirtualRect = rect; + return dummyChart; + }, + data: lineChartDataWithNoTitles, + ), + ), + ), + ), + ), + ); + + final renderBox = tester.renderObject( + find.byKey(dummyChartKey), + ); + final chartCenterOffset = tester.getCenter(find.byKey(dummyChartKey)); + final scaleStart1 = chartCenterOffset + const Offset(10, 10); + final scaleStart2 = chartCenterOffset - const Offset(10, 10); + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await tester.pump(); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await tester.pump(); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + expect(chartVirtualRect!.size, greaterThan(renderBox.size)); + expect(chartVirtualRect!.left, isNegative); + expect(chartVirtualRect!.top, isNegative); + }); + + testWidgets('scales horizontally with FlScaleAxis.horizontal', + (WidgetTester tester) async { + Rect? chartVirtualRect; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: viewSize.width, + height: viewSize.height, + child: AxisChartScaffoldWidget( + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.horizontal, + ), + chartBuilder: (context, rect) { + chartVirtualRect = rect; + return dummyChart; + }, + data: lineChartDataWithNoTitles, + ), + ), + ), + ), + ), + ); + + final renderBox = tester.renderObject( + find.byKey(dummyChartKey), + ); + final chartCenterOffset = tester.getCenter(find.byKey(dummyChartKey)); + final scaleStart1 = chartCenterOffset + const Offset(10, 10); + final scaleStart2 = chartCenterOffset - const Offset(10, 10); + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await tester.pump(); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await tester.pump(); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + expect(chartVirtualRect!.size.height, renderBox.size.height); + expect( + chartVirtualRect!.size.width, + greaterThan(renderBox.size.width), + ); + expect(chartVirtualRect!.left, isNegative); + expect(chartVirtualRect!.top, 0); + }); + + testWidgets('scales vertically with FlScaleAxis.vertical', + (WidgetTester tester) async { + Rect? chartVirtualRect; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: viewSize.width, + height: viewSize.height, + child: AxisChartScaffoldWidget( + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.vertical, + ), + chartBuilder: (context, rect) { + chartVirtualRect = rect; + return dummyChart; + }, + data: lineChartDataWithNoTitles, + ), + ), + ), + ), + ), + ); + + final renderBox = tester.renderObject( + find.byKey(dummyChartKey), + ); + final chartCenterOffset = tester.getCenter(find.byKey(dummyChartKey)); + final scaleStart1 = chartCenterOffset + const Offset(10, 10); + final scaleStart2 = chartCenterOffset - const Offset(10, 10); + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await tester.pump(); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await tester.pump(); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + expect( + chartVirtualRect!.size.height, + greaterThan(renderBox.size.height), + ); + expect(chartVirtualRect!.size.width, renderBox.size.width); + expect(chartVirtualRect!.left, 0); + expect(chartVirtualRect!.top, isNegative); + }); + }); + + group('trackpad scroll', () { + testWidgets( + 'does not scale with FlScaleAxis.none when trackpadScrollCausesScale is true', + (WidgetTester tester) async { + Rect? chartVirtualRect; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: viewSize.width, + height: viewSize.height, + child: AxisChartScaffoldWidget( + transformationConfig: const FlTransformationConfig( + trackpadScrollCausesScale: true, + ), + chartBuilder: (context, rect) { + chartVirtualRect = rect; + return dummyChart; + }, + data: lineChartDataWithNoTitles, + ), + ), + ), + ), + ), + ); + + final pointer = TestPointer(1, PointerDeviceKind.trackpad); + final chartCenterOffset = + tester.getCenter(find.byKey(dummyChartKey)); + const scrollAmount = Offset(0, -100); + + await tester.sendEventToBinding(pointer.hover(chartCenterOffset)); + await tester.pump(); + await tester.sendEventToBinding(pointer.scroll(scrollAmount)); + await tester.pump(); + + expect(chartVirtualRect, isNull); + }, + ); + + for (final scaleAxis in FlScaleAxis.scalingEnabledAxis) { + testWidgets( + 'does not scale when trackpadScrollCausesScale is false ' + 'for $scaleAxis', + (WidgetTester tester) async { + Rect? chartVirtualRect; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: viewSize.width, + height: viewSize.height, + child: AxisChartScaffoldWidget( + transformationConfig: FlTransformationConfig( + scaleAxis: scaleAxis, + ), + chartBuilder: (context, rect) { + chartVirtualRect = rect; + return dummyChart; + }, + data: lineChartDataWithNoTitles, + ), + ), + ), + ), + ), + ); + + final pointer = TestPointer(1, PointerDeviceKind.trackpad); + final chartCenterOffset = tester.getCenter( + find.byKey(dummyChartKey), + ); + const scrollAmount = Offset(0, -100); + + await tester.sendEventToBinding(pointer.hover(chartCenterOffset)); + await tester.pump(); + await tester.sendEventToBinding(pointer.scroll(scrollAmount)); + await tester.pump(); + + expect(chartVirtualRect, isNull); + }, + ); + } + + testWidgets('scales horizontally with FlScaleAxis.horizontal', + (WidgetTester tester) async { + Rect? chartVirtualRect; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: viewSize.width, + height: viewSize.height, + child: AxisChartScaffoldWidget( + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.horizontal, + trackpadScrollCausesScale: true, + ), + chartBuilder: (context, rect) { + chartVirtualRect = rect; + return dummyChart; + }, + data: lineChartDataWithNoTitles, + ), + ), + ), + ), + ), + ); + + final renderBox = tester.renderObject( + find.byKey(dummyChartKey), + ); + final pointer = TestPointer(1, PointerDeviceKind.trackpad); + final chartCenterOffset = tester.getCenter(find.byKey(dummyChartKey)); + const scrollAmount = Offset(0, -100); + + await tester.sendEventToBinding(pointer.hover(chartCenterOffset)); + await tester.pump(); + await tester.sendEventToBinding(pointer.scroll(scrollAmount)); + await tester.pump(); + + expect(chartVirtualRect!.size.height, renderBox.size.height); + expect( + chartVirtualRect!.size.width, + greaterThan(renderBox.size.width), + ); + expect(chartVirtualRect!.left, isNegative); + expect(chartVirtualRect!.top, 0); + }); + + testWidgets('scales vertically with FlScaleAxis.vertical', + (WidgetTester tester) async { + Rect? chartVirtualRect; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: viewSize.width, + height: viewSize.height, + child: AxisChartScaffoldWidget( + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.vertical, + trackpadScrollCausesScale: true, + ), + chartBuilder: (context, rect) { + chartVirtualRect = rect; + return dummyChart; + }, + data: lineChartDataWithNoTitles, + ), + ), + ), + ), + ), + ); + + final renderBox = tester.renderObject( + find.byKey(dummyChartKey), + ); + final pointer = TestPointer(1, PointerDeviceKind.trackpad); + final chartCenterOffset = tester.getCenter(find.byKey(dummyChartKey)); + const scrollAmount = Offset(0, -100); + + await tester.sendEventToBinding(pointer.hover(chartCenterOffset)); + await tester.pump(); + await tester.sendEventToBinding(pointer.scroll(scrollAmount)); + await tester.pump(); + + expect( + chartVirtualRect!.size.height, + greaterThan(renderBox.size.height), + ); + expect(chartVirtualRect!.size.width, renderBox.size.width); + expect(chartVirtualRect!.left, 0); + expect(chartVirtualRect!.top, isNegative); + }); + + testWidgets('scales freely with FlScaleAxis.free', + (WidgetTester tester) async { + Rect? chartVirtualRect; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: viewSize.width, + height: viewSize.height, + child: AxisChartScaffoldWidget( + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.free, + trackpadScrollCausesScale: true, + ), + chartBuilder: (context, rect) { + chartVirtualRect = rect; + return dummyChart; + }, + data: lineChartDataWithNoTitles, + ), + ), + ), + ), + ), + ); + + final renderBox = tester.renderObject( + find.byKey(dummyChartKey), + ); + final pointer = TestPointer(1, PointerDeviceKind.trackpad); + final chartCenterOffset = tester.getCenter(find.byKey(dummyChartKey)); + const scrollAmount = Offset(0, -100); + + await tester.sendEventToBinding(pointer.hover(chartCenterOffset)); + await tester.pump(); + await tester.sendEventToBinding(pointer.scroll(scrollAmount)); + await tester.pump(); + + expect(chartVirtualRect!.size, greaterThan(renderBox.size)); + expect(chartVirtualRect!.left, isNegative); + expect(chartVirtualRect!.top, isNegative); + }); + }); + + group('pans', () { + testWidgets('only horizontally with FlScaleAxis.horizontal', + (WidgetTester tester) async { + Rect? chartVirtualRect; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: viewSize.width, + height: viewSize.height, + child: AxisChartScaffoldWidget( + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.horizontal, + ), + chartBuilder: (context, rect) { + chartVirtualRect = rect; + return const ColoredBox( + color: Colors.red, + ); + }, + data: lineChartDataWithNoTitles, + ), + ), + ), + ), + ), + ); + + final chartCenterOffset = tester.getCenter(find.byType(ColoredBox)); + final scaleStart1 = chartCenterOffset + const Offset(10, 10); + final scaleStart2 = chartCenterOffset - const Offset(10, 10); + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final chartVirtualRectBeforePan = chartVirtualRect; + expect(chartVirtualRectBeforePan!.top, 0); + + const panOffset = Offset(100, 100); + await tester.dragFrom(chartCenterOffset, panOffset); + await tester.pumpAndSettle(); + + expect(chartVirtualRect!.size, chartVirtualRectBeforePan.size); + expect( + chartVirtualRect!.left, + greaterThan(chartVirtualRectBeforePan.left), + ); + expect(chartVirtualRect!.top, 0); + }); + + testWidgets('only vertically with FlScaleAxis.vertical', + (WidgetTester tester) async { + Rect? chartVirtualRect; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: viewSize.width, + height: viewSize.height, + child: AxisChartScaffoldWidget( + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.vertical, + ), + chartBuilder: (context, rect) { + chartVirtualRect = rect; + return const ColoredBox( + color: Colors.red, + ); + }, + data: lineChartDataWithNoTitles, + ), + ), + ), + ), + ), + ); + + final chartCenterOffset = tester.getCenter(find.byType(ColoredBox)); + final scaleStart1 = chartCenterOffset + const Offset(10, 10); + final scaleStart2 = chartCenterOffset - const Offset(10, 10); + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final chartVirtualRectBeforePan = chartVirtualRect; + expect(chartVirtualRectBeforePan!.left, 0); + + const panOffset = Offset(100, 100); + await tester.dragFrom(chartCenterOffset, panOffset); + await tester.pumpAndSettle(); + + expect(chartVirtualRect!.left, 0); + expect( + chartVirtualRect!.top, + greaterThan(chartVirtualRectBeforePan.top), + ); + }); + + testWidgets('freely with FlScaleAxis.free', + (WidgetTester tester) async { + Rect? chartVirtualRect; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: viewSize.width, + height: viewSize.height, + child: AxisChartScaffoldWidget( + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.free, + ), + chartBuilder: (context, rect) { + chartVirtualRect = rect; + return const ColoredBox( + color: Colors.red, + ); + }, + data: lineChartDataWithNoTitles, + ), + ), + ), + ), + ), + ); + + final chartCenterOffset = tester.getCenter(find.byType(ColoredBox)); + final scaleStart1 = chartCenterOffset + const Offset(10, 10); + final scaleStart2 = chartCenterOffset - const Offset(10, 10); + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final chartVirtualRectBeforePan = chartVirtualRect; + expect(chartVirtualRectBeforePan!.left, isNegative); + expect(chartVirtualRectBeforePan.top, isNegative); + + const panOffset = Offset(100, 100); + await tester.dragFrom(chartCenterOffset, panOffset); + await tester.pumpAndSettle(); + + expect( + chartVirtualRect!.left, + greaterThan(chartVirtualRectBeforePan.left), + ); + expect( + chartVirtualRect!.top, + greaterThan(chartVirtualRectBeforePan.top), + ); + }); + }); + }); + + testWidgets('passes chart rect to SideTitlesWidgets', + (WidgetTester tester) async { + Rect? chartVirtualRect; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: viewSize.width, + height: viewSize.height, + child: AxisChartScaffoldWidget( + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.free, + ), + chartBuilder: (context, rect) { + chartVirtualRect = rect; + return const ColoredBox( + color: Colors.red, + ); + }, + data: lineChartDataWithAllTitles, + ), + ), + ), + ), + ), + ); + + final chartCenterOffset = tester.getCenter(find.byType(ColoredBox)); + final scaleStart1 = chartCenterOffset + const Offset(10, 10); + final scaleStart2 = chartCenterOffset - const Offset(10, 10); + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final sideTitlesWidgets = tester.allWidgets.whereType(); + expect(sideTitlesWidgets.length, 4); + for (final sideTitlesWidget in sideTitlesWidgets) { + expect(sideTitlesWidget.chartVirtualRect, chartVirtualRect); + } + }); + + testWidgets( + 'Initializes zoomed chart rect when controller scale != 1.0', + (WidgetTester tester) async { + final controller = TransformationController( + Matrix4.identity()..scale(3.0), + ); + Rect? chartVirtualRect; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: AxisChartScaffoldWidget( + data: lineChartDataWithNoTitles, + transformationConfig: FlTransformationConfig( + transformationController: controller, + scaleAxis: FlScaleAxis.free, + ), + chartBuilder: (context, rect) { + chartVirtualRect = rect; + return dummyChart; + }, + ), + ), + ), + ), + ); + + expect(chartVirtualRect, isNotNull); + }, + ); + + group('didUpdateWidget', () { + const chartScaffoldKey = Key('chartScaffold'); + + final chartVirtualRects = []; + + tearDown(chartVirtualRects.clear); + + Widget createTestWidget({ + TransformationController? controller, + }) { + return MaterialApp( + home: Scaffold( + body: Center( + child: AxisChartScaffoldWidget( + key: chartScaffoldKey, + data: lineChartDataWithNoTitles, + transformationConfig: FlTransformationConfig( + scaleAxis: FlScaleAxis.free, + transformationController: controller, + ), + chartBuilder: (context, rect) { + chartVirtualRects.add(rect); + return dummyChart; + }, + ), + ), + ), + ); + } + + TransformationController? getTransformationController( + WidgetTester tester, + ) { + return tester + .widget( + find.byType(CustomInteractiveViewer), + ) + .transformationController; + } + + testWidgets( + 'oldWidget.controller is null and widget.controller is null: ' + 'keeps old controller', + (WidgetTester tester) async { + final actualChartVirtualRects = [isNotScaled]; + await tester.pumpWidget(createTestWidget()); + expect(chartVirtualRects, actualChartVirtualRects); + + final transformationController = getTransformationController(tester); + transformationController!.value = Matrix4.identity()..scale(2.0); + await tester.pump(); + expect(chartVirtualRects, actualChartVirtualRects..add(isScaled)); + + await tester.pumpWidget(createTestWidget()); + expect(chartVirtualRects, actualChartVirtualRects..add(isScaled)); + + final transformationController2 = getTransformationController(tester); + expect(transformationController2, transformationController); + transformationController2!.value = Matrix4.identity()..scale(3.0); + await tester.pump(); + expect(chartVirtualRects, actualChartVirtualRects..add(isScaled)); + }, + ); + + testWidgets( + 'oldWidget.controller is null and widget.controller is not null: ' + 'disposes old controller and sets up widget.controller with listeners', + (WidgetTester tester) async { + final actualChartVirtualRects = [isNotScaled]; + await tester.pumpWidget(createTestWidget()); + expect(chartVirtualRects, actualChartVirtualRects); + + final transformationController = getTransformationController(tester); + transformationController!.value = Matrix4.identity()..scale(2.0); + await tester.pump(); + expect(chartVirtualRects, actualChartVirtualRects..add(isScaled)); + + final transformationController2 = TransformationController(); + + await tester.pumpWidget( + createTestWidget(controller: transformationController2), + ); + expect(chartVirtualRects, actualChartVirtualRects..add(isNotScaled)); + + expect(transformationController2, isNot(transformationController)); + expect( + () => transformationController.addListener(() {}), + throwsA(isA()), + ); + transformationController2.value = Matrix4.identity()..scale(2.0); + await tester.pump(); + expect(chartVirtualRects, actualChartVirtualRects..add(isScaled)); + }, + ); + + testWidgets( + 'oldWidget.controller is not null and widget.controller is null: ' + 'removes listeners from old controller and sets up new controller ' + 'with listeners', + (WidgetTester tester) async { + final actualChartVirtualRects = [isNotScaled]; + final transformationController = TransformationController(); + await tester.pumpWidget( + createTestWidget(controller: transformationController), + ); + expect(chartVirtualRects, actualChartVirtualRects); + + transformationController.value = Matrix4.identity()..scale(2.0); + await tester.pump(); + expect(chartVirtualRects, actualChartVirtualRects..add(isScaled)); + + await tester.pumpWidget(createTestWidget()); + expect(chartVirtualRects, actualChartVirtualRects..add(isNotScaled)); + + final transformationController2 = getTransformationController(tester); + expect(transformationController2, isNot(transformationController)); + // This is for test + // ignore: invalid_use_of_protected_member + expect(transformationController.hasListeners, false); + transformationController.addListener(() {}); // throws if disposed + transformationController2!.value = Matrix4.identity()..scale(2.0); + await tester.pump(); + expect(chartVirtualRects, actualChartVirtualRects..add(isScaled)); + }, + ); + + testWidgets( + 'oldWidget.controller is not null and widget.controller is not null, ' + 'controllers are different: ' + 'removes listeners from old controller and sets up ' + 'widget.controller with listeners', + (WidgetTester tester) async { + final actualChartVirtualRects = [isNotScaled]; + final transformationController = TransformationController(); + await tester.pumpWidget( + createTestWidget(controller: transformationController), + ); + expect(chartVirtualRects, actualChartVirtualRects); + + transformationController.value = Matrix4.identity()..scale(2.0); + await tester.pump(); + expect(chartVirtualRects, actualChartVirtualRects..add(isScaled)); + + final transformationController2 = TransformationController(); + + await tester.pumpWidget( + createTestWidget(controller: transformationController2), + ); + expect(chartVirtualRects, actualChartVirtualRects..add(isNotScaled)); + + expect(transformationController2, isNot(transformationController)); + // This is for test + // ignore: invalid_use_of_protected_member + expect(transformationController.hasListeners, false); + transformationController.addListener(() {}); // throws if disposed + transformationController2.value = Matrix4.identity()..scale(2.0); + await tester.pump(); + expect(chartVirtualRects, actualChartVirtualRects..add(isScaled)); + }, + ); + + testWidgets( + 'oldWidget.controller is not null and widget.controller is not null, ' + 'controllers are the same: keeps old controller', + (WidgetTester tester) async { + final actualChartVirtualRects = [isNotScaled]; + final transformationController = TransformationController(); + await tester.pumpWidget( + createTestWidget( + controller: transformationController, + ), + ); + expect(chartVirtualRects, actualChartVirtualRects); + + transformationController.value = Matrix4.identity()..scale(2.0); + await tester.pump(); + expect(chartVirtualRects, actualChartVirtualRects..add(isScaled)); + + await tester.pumpWidget( + createTestWidget( + controller: transformationController, + ), + ); + expect(chartVirtualRects, actualChartVirtualRects..add(isScaled)); + + final transformationController2 = getTransformationController(tester); + expect(transformationController2, transformationController); + transformationController.value = Matrix4.identity()..scale(3.0); + await tester.pump(); + expect(chartVirtualRects, actualChartVirtualRects..add(isScaled)); + }, + ); + }); + + testWidgets( + 'sets chartVirtualRect to null, when scaling is updated to 1.0', + (WidgetTester tester) async { + final transformationController = TransformationController(); + final chartVirtualRects = []; + final actualChartVirtualRects = [isNotScaled]; + await tester.pumpWidget( + MaterialApp( + home: AxisChartScaffoldWidget( + data: lineChartDataWithNoTitles, + transformationConfig: FlTransformationConfig( + transformationController: transformationController, + scaleAxis: FlScaleAxis.free, + ), + chartBuilder: (context, rect) { + chartVirtualRects.add(rect); + return dummyChart; + }, + ), + ), + ); + expect(chartVirtualRects, actualChartVirtualRects); + + transformationController.value = Matrix4.identity()..scale(2.0); + await tester.pump(); + expect(chartVirtualRects, actualChartVirtualRects..add(isScaled)); + + transformationController.value = Matrix4.identity()..scale(1.0); + await tester.pump(); + expect(chartVirtualRects, actualChartVirtualRects..add(isNotScaled)); + }, + ); + + testWidgets('does not dispose external controller', + (WidgetTester tester) async { + final controller = TransformationController(); + await tester.pumpWidget( + MaterialApp( + home: AxisChartScaffoldWidget( + data: lineChartDataWithNoTitles, + transformationConfig: FlTransformationConfig( + transformationController: controller, + ), + chartBuilder: (context, rect) { + return dummyChart; + }, + ), + ), + ); + await tester.pumpWidget(Container()); + // This is for test + // ignore: invalid_use_of_protected_member + expect(controller.hasListeners, false); + controller.addListener(() {}); // throws if disposed + }); + }); +} diff --git a/test/chart/base/axis_chart/axis_chart_widgets_test.dart b/test/chart/base/axis_chart/axis_chart_widgets_test.dart new file mode 100644 index 0000000..1a90845 --- /dev/null +++ b/test/chart/base/axis_chart/axis_chart_widgets_test.dart @@ -0,0 +1,259 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TitleMeta getTitleMeta(AxisSide axisSide) => TitleMeta( + min: 0, + max: 10, + parentAxisSize: 100, + axisPosition: 10, + appliedInterval: 10, + sideTitles: const SideTitles(), + formattedValue: '12', + axisSide: axisSide, + rotationQuarterTurns: 0, + ); + + group( + 'SideTitle without FitInside enabled', + () { + testWidgets( + 'SideTitleWidget left', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SideTitleWidget( + meta: getTitleMeta(AxisSide.left), + child: const Text('1s'), + ), + ), + ), + ); + expect(find.byType(Transform), findsAtLeastNWidgets(2)); + expect(find.byType(Container), findsOneWidget); + expect(find.text('1s'), findsOneWidget); + }, + ); + + testWidgets( + 'SideTitleWidget top', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SideTitleWidget( + meta: getTitleMeta(AxisSide.top), + child: const Text('1s'), + ), + ), + ), + ); + expect(find.byType(Transform), findsAtLeastNWidgets(2)); + expect(find.byType(Container), findsOneWidget); + expect(find.text('1s'), findsOneWidget); + }, + ); + + testWidgets( + 'SideTitleWidget right', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SideTitleWidget( + meta: getTitleMeta(AxisSide.right), + child: const Text('1s'), + ), + ), + ), + ); + expect(find.byType(Transform), findsAtLeastNWidgets(2)); + expect(find.byType(Container), findsOneWidget); + expect(find.text('1s'), findsOneWidget); + }, + ); + + testWidgets( + 'SideTitleWidget bottom', + (WidgetTester tester) async { + const widgetKey = Key('SideTitleWidget'); + final sideTitleWidget = SideTitleWidget( + key: widgetKey, + meta: getTitleMeta(AxisSide.bottom), + child: const Text('1s'), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: sideTitleWidget, + ), + ), + ); + + final element = + tester.element(find.byKey(widgetKey)) as StatefulElement; + final state = element.state as State; + expect(state.widget, equals(sideTitleWidget)); + expect(element.renderObject!.attached, isTrue); + + expect(find.byType(Transform), findsAtLeastNWidgets(2)); + expect(find.byType(Container), findsOneWidget); + expect(find.text('1s'), findsOneWidget); + + await tester.pump(); + }, + ); + }, + ); + group( + 'SideTitle with FitInside enabled', + () { + testWidgets( + 'SideTitleWidget left with FitInsideEnabled on Top Side', + (WidgetTester tester) async { + const widgetKey = Key('SideTitleWidget'); + final sideTitleWidget = SideTitleWidget( + key: widgetKey, + meta: getTitleMeta(AxisSide.left), + fitInside: const SideTitleFitInsideData( + enabled: true, + axisPosition: 0, + parentAxisSize: 100, + distanceFromEdge: 0, + ), + child: const Text('A Long Text'), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: sideTitleWidget, + ), + ), + ); + + final element = + tester.element(find.byKey(widgetKey)) as StatefulElement; + final state = element.state as State; + expect(state.widget, equals(sideTitleWidget)); + expect(element.renderObject!.attached, isTrue); + + expect(find.byType(Transform), findsAtLeastNWidgets(2)); + expect(find.byType(Container), findsOneWidget); + expect(find.text('A Long Text'), findsOneWidget); + }, + ); + + testWidgets( + 'SideTitleWidget left with FitInsideEnabled on Bottom Side', + (WidgetTester tester) async { + const widgetKey = Key('SideTitleWidget'); + final sideTitleWidget = SideTitleWidget( + key: widgetKey, + meta: getTitleMeta(AxisSide.left), + fitInside: const SideTitleFitInsideData( + enabled: true, + axisPosition: 100, + parentAxisSize: 100, + distanceFromEdge: 0, + ), + child: const Text('A Long Text'), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: sideTitleWidget, + ), + ), + ); + + final element = + tester.element(find.byKey(widgetKey)) as StatefulElement; + final state = element.state as State; + expect(state.widget, equals(sideTitleWidget)); + expect(element.renderObject!.attached, isTrue); + + expect(find.byType(Transform), findsAtLeastNWidgets(2)); + expect(find.byType(Container), findsOneWidget); + expect(find.text('A Long Text'), findsOneWidget); + }, + ); + + testWidgets( + 'SideTitleWidget bottom with FitInsideEnabled on Left Side', + (WidgetTester tester) async { + const widgetKey = Key('SideTitleWidget'); + final sideTitleWidget = SideTitleWidget( + key: widgetKey, + meta: getTitleMeta(AxisSide.bottom), + fitInside: const SideTitleFitInsideData( + enabled: true, + axisPosition: 0, + parentAxisSize: 100, + distanceFromEdge: 0, + ), + child: const Text('A Long Text'), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: sideTitleWidget, + ), + ), + ); + + final element = + tester.element(find.byKey(widgetKey)) as StatefulElement; + final state = element.state as State; + expect(state.widget, equals(sideTitleWidget)); + expect(element.renderObject!.attached, isTrue); + + expect(find.byType(Transform), findsAtLeastNWidgets(2)); + expect(find.byType(Container), findsOneWidget); + expect(find.text('A Long Text'), findsOneWidget); + }, + ); + + testWidgets( + 'SideTitleWidget bottom with FitInsideEnabled on Right Side', + (WidgetTester tester) async { + const widgetKey = Key('SideTitleWidget'); + final sideTitleWidget = SideTitleWidget( + key: widgetKey, + meta: getTitleMeta(AxisSide.bottom), + fitInside: const SideTitleFitInsideData( + enabled: true, + axisPosition: 100, + parentAxisSize: 100, + distanceFromEdge: 0, + ), + child: const Text('A Long Text'), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: sideTitleWidget, + ), + ), + ); + + final element = + tester.element(find.byKey(widgetKey)) as StatefulElement; + final state = element.state as State; + expect(state.widget, equals(sideTitleWidget)); + expect(element.renderObject!.attached, isTrue); + + expect(find.byType(Transform), findsAtLeastNWidgets(2)); + expect(find.byType(Container), findsOneWidget); + expect(find.text('A Long Text'), findsOneWidget); + }, + ); + }, + ); +} diff --git a/test/chart/base/axis_chart/base_chart_data_test.dart b/test/chart/base/axis_chart/base_chart_data_test.dart new file mode 100644 index 0000000..80f27fe --- /dev/null +++ b/test/chart/base/axis_chart/base_chart_data_test.dart @@ -0,0 +1,29 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import '../../data_pool.dart'; + +void main() { + group('BaseChartData data equality check', () { + test('FlBorderData equality test', () { + expect(borderData1 == borderData1Clone, true); + + expect( + borderData1 == + FlBorderData( + show: false, + border: Border.all(color: Colors.green), + ), + false, + ); + + expect( + borderData1 == + FlBorderData( + show: true, + ), + false, + ); + }); + }); +} diff --git a/test/chart/base/axis_chart/side_titles/side_titles_flex_test.dart b/test/chart/base/axis_chart/side_titles/side_titles_flex_test.dart new file mode 100644 index 0000000..3587dae --- /dev/null +++ b/test/chart/base/axis_chart/side_titles/side_titles_flex_test.dart @@ -0,0 +1,256 @@ +import 'package:fl_chart/src/chart/base/axis_chart/side_titles/side_titles_flex.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('SideTitlesFlex test', () { + testWidgets( + 'Test vertical mode', + (WidgetTester tester) async { + const viewHeight = 400.0; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: 40, + height: viewHeight, + child: SideTitlesFlex( + direction: Axis.vertical, + axisSideMetaData: AxisSideMetaData( + 0, + 10, + viewHeight, + ), + widgetHolders: oneToNineWidgetHolders(viewHeight), + ), + ), + ), + ), + ), + ); + + for (var i = 1; i <= 9; i++) { + expect(find.text(i.toDouble().toString()), findsOneWidget); + } + expect(find.text('10.0'), findsNothing); + expect(find.text('11.0'), findsNothing); + expect(find.text('0.0'), findsNothing); + }, + ); + + testWidgets( + 'Test horizontal mode', + (WidgetTester tester) async { + const viewWidth = 400.0; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: viewWidth, + height: 40, + child: SideTitlesFlex( + direction: Axis.horizontal, + axisSideMetaData: AxisSideMetaData( + 0, + 10, + viewWidth, + ), + widgetHolders: List.generate(9, (index) => index.toDouble()) + .map( + (value) => AxisSideTitleWidgetHolder( + AxisSideTitleMetaData( + value + 1, + (value / 10) * viewWidth, + ), + Text((value + 1).toString()), + ), + ) + .toList(), + ), + ), + ), + ), + ), + ); + + for (var i = 1; i <= 9; i++) { + expect(find.text(i.toDouble().toString()), findsOneWidget); + } + expect(find.text('10.0'), findsNothing); + expect(find.text('11.0'), findsNothing); + expect(find.text('0.0'), findsNothing); + }, + ); + + testWidgets( + 'Test update from horizontal to vertical', + (WidgetTester tester) async { + const valueKey = ValueKey('asdf'); + + const viewSize = 400.0; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: viewSize, + height: 40, + child: SideTitlesFlex( + key: valueKey, + direction: Axis.horizontal, + axisSideMetaData: AxisSideMetaData( + 0, + 10, + viewSize, + ), + widgetHolders: oneToNineWidgetHolders(viewSize), + ), + ), + ), + ), + ), + ); + + for (var i = 1; i <= 9; i++) { + expect(find.text(i.toDouble().toString()), findsOneWidget); + } + expect(find.text('10.0'), findsNothing); + expect(find.text('11.0'), findsNothing); + expect(find.text('0.0'), findsNothing); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: 40, + height: viewSize, + child: SideTitlesFlex( + key: valueKey, + direction: Axis.vertical, + axisSideMetaData: AxisSideMetaData( + 0, + 10, + viewSize, + ), + widgetHolders: oneToNineWidgetHolders(viewSize), + ), + ), + ), + ), + ), + ); + for (var i = 1; i <= 9; i++) { + expect(find.text(i.toDouble().toString()), findsOneWidget); + } + expect(find.text('10.0'), findsNothing); + expect(find.text('11.0'), findsNothing); + expect(find.text('0.0'), findsNothing); + }, + ); + }); + + test('AxisSideTitlesRenderFlex horizontal', () { + const viewSize = 400.0; + final axisSideMetaData = AxisSideMetaData( + 0, + 10, + viewSize, + ); + final sideTitlesMetaData = oneToNineSideTitleMetaData(viewSize); + final renderFlex = AxisSideTitlesRenderFlex( + axisSideMetaData: axisSideMetaData, + axisSideTitlesMetaData: sideTitlesMetaData, + ); + + final builder = DiagnosticPropertiesBuilder(); + renderFlex.debugFillProperties(builder); + expect(builder.properties.length > 1, true); + expect( + (builder.properties.last as EnumProperty).value, + Axis.horizontal, + ); + expect(renderFlex.direction, Axis.horizontal); + expect(renderFlex.axisSideMetaData, axisSideMetaData); + expect(renderFlex.axisSideTitlesMetaData, sideTitlesMetaData); + expect(renderFlex.debugNeedsLayout, false); + expect( + renderFlex.hitTestChildren(BoxHitTestResult(), position: Offset.zero), + false, + ); + expect( + renderFlex + .computeDryLayout(BoxConstraints.tight(const Size(viewSize, 40))), + const Size(viewSize, 40), + ); + expect( + // This is for test + // ignore: invalid_use_of_protected_member + renderFlex.computeDistanceToActualBaseline(TextBaseline.alphabetic), + null, + ); + }); + + test('AxisSideTitlesRenderFlex vertical', () { + const viewSize = 400.0; + final axisSideMetaData = AxisSideMetaData( + 0, + 10, + viewSize, + ); + final sideTitlesMetaData = oneToNineSideTitleMetaData(viewSize); + final renderFlex = AxisSideTitlesRenderFlex( + direction: Axis.vertical, + axisSideMetaData: axisSideMetaData, + axisSideTitlesMetaData: sideTitlesMetaData, + ); + expect( + renderFlex + .computeDryLayout(BoxConstraints.tight(const Size(viewSize, 40))), + const Size(viewSize, 40), + ); + expect( + // This is for test + // ignore: invalid_use_of_protected_member + renderFlex.computeDistanceToActualBaseline(TextBaseline.alphabetic), + null, + ); + }); + + test('AxisSideTitleMetaData equality test', () { + final data1 = AxisSideTitleMetaData(0, 0); + final data1Clone = AxisSideTitleMetaData(0, 0); + final data2 = AxisSideTitleMetaData(2, 0); + expect(data1 == data1Clone, true); + expect(data1 == data2, false); + }); + + test('AxisSideMetaData diff', () { + expect(AxisSideMetaData(5, 10, 100).diff, 5); + expect(AxisSideMetaData(5, 10, 0).diff, 5); + expect(AxisSideMetaData(9, 10, 0).diff, 1); + }); +} + +List oneToNineWidgetHolders(double viewSize) { + return oneToNineSideTitleMetaData(viewSize).asMap().entries.map((e) { + final index = e.key; + final sideTitlesMetaData = e.value; + return AxisSideTitleWidgetHolder( + sideTitlesMetaData, + Text((index + 1).toDouble().toString()), + ); + }).toList(); +} + +List oneToNineSideTitleMetaData(double viewSize) { + return List.generate(9, (index) => index.toDouble()) + .map( + (value) => AxisSideTitleMetaData(value + 1, (value / 10) * viewSize), + ) + .toList(); +} diff --git a/test/chart/base/axis_chart/side_titles/side_titles_test.dart b/test/chart/base/axis_chart/side_titles/side_titles_test.dart new file mode 100644 index 0000000..72a283f --- /dev/null +++ b/test/chart/base/axis_chart/side_titles/side_titles_test.dart @@ -0,0 +1,81 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + final data = [ + const FlSpot(0, 0.5), + const FlSpot(1, 1.3), + const FlSpot(2, 1.9), + ]; + + const viewSize = Size(400, 400); + + testWidgets( + 'Test the effect of minIncluded and maxIncluded in sideTitles', + (WidgetTester tester) async { + // Minimum/maximum included + final mima = [ + [true, true], + [true, false], + [false, true], + [false, false], + ]; + + for (final e in mima) { + final titlesData = FlTitlesData( + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + minIncluded: e[0], + maxIncluded: e[1], + reservedSize: 50, + interval: 1, + ), + ), + rightTitles: const AxisTitles(), + topTitles: const AxisTitles(), + bottomTitles: const AxisTitles(), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: viewSize.width, + height: viewSize.height, + child: LineChart( + LineChartData( + titlesData: titlesData, + lineBarsData: [ + LineChartBarData( + spots: data, + ), + ], + ), + ), + ), + ), + ), + ), + ); + // Number of expected text widgets (titles) on the y-axis + expect( + find.byType(Text), + findsNWidgets((e[0] ? 1 : 0) + (e[1] ? 1 : 0) + 1), + ); + // Always there + expect(find.text('1'), findsOneWidget); + if (e[0]) { + // Minimum included + expect(find.text('0.5'), findsOneWidget); + } + if (e[1]) { + // Maximum included + expect(find.text('1.9'), findsOneWidget); + } + } + }, + ); +} diff --git a/test/chart/base/axis_chart/side_titles/side_titles_widget_test.dart b/test/chart/base/axis_chart/side_titles/side_titles_widget_test.dart new file mode 100644 index 0000000..b6cbc9e --- /dev/null +++ b/test/chart/base/axis_chart/side_titles/side_titles_widget_test.dart @@ -0,0 +1,439 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/bar_chart/bar_chart_helper.dart'; +import 'package:fl_chart/src/chart/base/axis_chart/side_titles/side_titles_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + const viewSize = Size(400, 400); + + final lineChartDataBase = LineChartData( + minX: 0, + maxX: 10, + minY: 0, + maxY: 10, + ); + + final lineChartDataWithNoTitles = lineChartDataBase.copyWith( + titlesData: const FlTitlesData( + show: false, + leftTitles: AxisTitles(), + topTitles: AxisTitles(), + rightTitles: AxisTitles(), + bottomTitles: AxisTitles(), + ), + ); + + final lineChartDataWithAllTitles = lineChartDataBase.copyWith( + titlesData: FlTitlesData( + leftTitles: AxisTitles( + axisNameWidget: const Text('Left Titles'), + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (double value, TitleMeta meta) { + return Text('L-${value.toInt()}'); + }, + interval: 1, + ), + ), + topTitles: AxisTitles( + axisNameWidget: const Text('Top Titles'), + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (double value, TitleMeta meta) { + return Text('T-${value.toInt()}'); + }, + interval: 1, + ), + ), + rightTitles: AxisTitles( + axisNameWidget: const Text('Right Titles'), + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (double value, TitleMeta meta) { + return Text('R-${value.toInt()}'); + }, + interval: 1, + ), + ), + bottomTitles: AxisTitles( + axisNameWidget: const Text('Bottom Titles'), + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (double value, TitleMeta meta) { + return Text('B-${value.toInt()}'); + }, + interval: 1, + ), + ), + ), + ); + + final lineChartDataWithOnlyLeftTitles = lineChartDataBase.copyWith( + titlesData: FlTitlesData( + leftTitles: AxisTitles( + axisNameWidget: const Text('Left Titles'), + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (double value, TitleMeta meta) { + return Text('L-${value.toInt()}'); + }, + interval: 1, + ), + ), + topTitles: const AxisTitles(), + rightTitles: const AxisTitles(), + bottomTitles: const AxisTitles(), + ), + ); + + final lineChartDataWithOnlyLeftTitlesWithoutAxisName = + lineChartDataBase.copyWith( + titlesData: FlTitlesData( + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (double value, TitleMeta meta) { + return Text('L-${value.toInt()}'); + }, + interval: 1, + ), + ), + topTitles: const AxisTitles(), + rightTitles: const AxisTitles(), + bottomTitles: const AxisTitles(), + ), + ); + + final barChartDataWithOnlyBottomTitles = BarChartData( + barGroups: [ + BarChartGroupData( + x: 0, + barRods: [ + BarChartRodData(toY: 10), + ], + ), + BarChartGroupData( + x: 1, + barRods: [ + BarChartRodData(toY: 10), + ], + ), + BarChartGroupData( + x: 2, + barRods: [ + BarChartRodData(toY: 10), + ], + ), + ], + titlesData: FlTitlesData( + leftTitles: const AxisTitles(), + topTitles: const AxisTitles(), + rightTitles: const AxisTitles(), + bottomTitles: AxisTitles( + axisNameWidget: const Icon(Icons.check), + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, meta) { + return TextButton( + onPressed: () {}, + child: Text( + value.toInt().toString(), + ), + ); + }, + ), + ), + ), + ); + + BarChartData createBarChartDataWithOnlyRightTitles() { + final barGroups = [ + BarChartGroupData( + x: 0, + barRods: [ + BarChartRodData(toY: 10), + ], + ), + BarChartGroupData( + x: 1, + barRods: [ + BarChartRodData(toY: 10), + ], + ), + BarChartGroupData( + x: 2, + barRods: [ + BarChartRodData(toY: 10), + ], + ), + ]; + + final (minY, maxY) = BarChartHelper().calculateMaxAxisValues(barGroups); + + return BarChartData( + barGroups: barGroups, + titlesData: FlTitlesData( + leftTitles: const AxisTitles(), + topTitles: const AxisTitles(), + rightTitles: AxisTitles( + axisNameWidget: const Icon(Icons.arrow_right), + sideTitles: SideTitles( + showTitles: true, + interval: 1, + getTitlesWidget: (value, meta) { + return TextButton( + onPressed: () {}, + child: Text( + value.toInt().toString(), + ), + ); + }, + ), + ), + bottomTitles: const AxisTitles(), + ), + minY: minY, + maxY: maxY, + ); + } + + BarChartData createBarChartDataWithEmptyGroups() { + final barGroups = []; + final (minY, maxY) = BarChartHelper().calculateMaxAxisValues(barGroups); + + return BarChartData( + barGroups: [], + titlesData: FlTitlesData( + leftTitles: const AxisTitles(), + topTitles: const AxisTitles(), + rightTitles: AxisTitles( + axisNameWidget: const Icon(Icons.arrow_right), + sideTitles: SideTitles( + showTitles: true, + interval: 1, + getTitlesWidget: (value, meta) { + return TextButton( + onPressed: () {}, + child: Text( + value.toInt().toString(), + ), + ); + }, + ), + ), + bottomTitles: const AxisTitles(), + ), + minY: minY, + maxY: maxY, + ); + } + + testWidgets( + 'LineChart with no titles', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: viewSize.width, + height: viewSize.height, + child: SideTitlesWidget( + side: AxisSide.left, + axisChartData: lineChartDataWithNoTitles, + parentSize: viewSize, + ), + ), + ), + ), + ), + ); + + expect(find.byType(Text), findsNothing); + }, + ); + + testWidgets( + 'LineChart with all titles', + (WidgetTester tester) async { + Future checkSide(AxisSide side) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: viewSize.width, + height: viewSize.height, + child: SideTitlesWidget( + side: side, + axisChartData: lineChartDataWithAllTitles, + parentSize: viewSize, + ), + ), + ), + ), + ), + ); + + final axisName = switch (side) { + AxisSide.left => 'Left', + AxisSide.top => 'Top', + AxisSide.right => 'Right', + AxisSide.bottom => 'Bottom', + }; + expect(find.text('$axisName Titles'), findsOneWidget); + for (var i = 0; i <= 10; i++) { + expect(find.text('${axisName.characters.first}-$i'), findsOneWidget); + } + } + + await checkSide(AxisSide.left); + await checkSide(AxisSide.top); + await checkSide(AxisSide.right); + await checkSide(AxisSide.bottom); + }, + ); + + testWidgets( + 'LineChart with Only left titles', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: viewSize.width, + height: viewSize.height, + child: SideTitlesWidget( + side: AxisSide.left, + axisChartData: lineChartDataWithOnlyLeftTitles, + parentSize: viewSize, + ), + ), + ), + ), + ), + ); + expect(find.text('Left Titles'), findsOneWidget); + for (var i = 0; i <= 10; i++) { + expect(find.text('L-$i'), findsOneWidget); + } + + expect(find.byType(Text), findsNWidgets(12)); + }, + ); + + testWidgets( + 'LineChart with Only left titles without axis name', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: viewSize.width, + height: viewSize.height, + child: SideTitlesWidget( + side: AxisSide.left, + axisChartData: lineChartDataWithOnlyLeftTitlesWithoutAxisName, + parentSize: viewSize, + ), + ), + ), + ), + ), + ); + for (var i = 0; i <= 10; i++) { + expect(find.text('L-$i'), findsOneWidget); + } + + expect(find.byType(Text), findsNWidgets(11)); + }, + ); + + testWidgets( + 'BarChart with Only bottom titles', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: viewSize.width, + height: viewSize.height, + child: SideTitlesWidget( + side: AxisSide.bottom, + axisChartData: barChartDataWithOnlyBottomTitles, + parentSize: viewSize, + ), + ), + ), + ), + ), + ); + + expect(find.byIcon(Icons.check), findsOneWidget); + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsOneWidget); + expect(find.text('2'), findsOneWidget); + expect(find.byType(TextButton), findsNWidgets(3)); + }, + ); + + testWidgets( + 'BarChart with Only right titles', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: viewSize.width, + height: viewSize.height, + child: SideTitlesWidget( + side: AxisSide.right, + axisChartData: createBarChartDataWithOnlyRightTitles(), + parentSize: viewSize, + ), + ), + ), + ), + ), + ); + + expect(find.byIcon(Icons.arrow_right), findsOneWidget); + for (var i = 0; i <= 10; i++) { + expect(find.text('$i'), findsOneWidget); + } + expect(find.byType(TextButton), findsNWidgets(11)); + }, + ); + + testWidgets( + 'BarChart with empty bars', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: viewSize.width, + height: viewSize.height, + child: SideTitlesWidget( + side: AxisSide.right, + axisChartData: createBarChartDataWithEmptyGroups(), + parentSize: viewSize, + ), + ), + ), + ), + ), + ); + + expect(find.byIcon(Icons.arrow_right), findsOneWidget); + expect(find.byType(Text), findsOneWidget); + expect(find.byType(TextButton), findsOneWidget); + }, + ); +} diff --git a/test/chart/base/axis_chart/transformation_config_test.dart b/test/chart/base/axis_chart/transformation_config_test.dart new file mode 100644 index 0000000..1aebad9 --- /dev/null +++ b/test/chart/base/axis_chart/transformation_config_test.dart @@ -0,0 +1,31 @@ +import 'package:fl_chart/src/chart/base/axis_chart/scale_axis.dart'; +import 'package:fl_chart/src/chart/base/axis_chart/transformation_config.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('FlTransformationConfig', () { + test('throws assertion error when minScale is less than 1', () { + expect( + () => FlTransformationConfig(minScale: 0.99), + throwsAssertionError, + ); + }); + + test('throws assertion error when maxScale is less than minScale', () { + expect( + () => FlTransformationConfig(minScale: 1.1, maxScale: 1), + throwsAssertionError, + ); + }); + + test('has correct default values', () { + const config = FlTransformationConfig(); + + expect(config.minScale, 1); + expect(config.maxScale, 2.5); + expect(config.trackpadScrollCausesScale, false); + expect(config.scaleAxis, FlScaleAxis.none); + expect(config.transformationController, isNull); + }); + }); +} diff --git a/test/chart/base/line_test.dart b/test/chart/base/line_test.dart new file mode 100644 index 0000000..78ceb65 --- /dev/null +++ b/test/chart/base/line_test.dart @@ -0,0 +1,40 @@ +import 'package:fl_chart/src/utils/utils.dart'; +import 'package:flutter_test/flutter_test.dart'; +import '../data_pool.dart'; + +void main() { + const tolerance = 0.001; + + test('test magnitude()', () { + expect(line1.magnitude(), closeTo(14.142, tolerance)); + expect(line2.magnitude(), closeTo(22.360, tolerance)); + expect(line3.magnitude(), closeTo(18.027, tolerance)); + expect(line4.magnitude(), closeTo(32.310, tolerance)); + expect(line5.magnitude(), closeTo(5.830, tolerance)); + }); + + test('test angle()', () { + expect(line1.direction(), closeTo(Utils().radians(45), tolerance)); + expect(line2.direction(), closeTo(Utils().radians(63.434), tolerance)); + expect(line3.direction(), closeTo(Utils().radians(-3.179), tolerance)); + expect(line4.direction(), closeTo(Utils().radians(68.198), tolerance)); + expect(line5.direction(), closeTo(Utils().radians(59), tolerance)); + }); + + test('test normalize()', () { + expect(line1.normalize().dx, closeTo(0.707, tolerance)); + expect(line1.normalize().dy, closeTo(0.707, tolerance)); + + expect(line2.normalize().dx, closeTo(0.447, tolerance)); + expect(line2.normalize().dy, closeTo(0.894, tolerance)); + + expect(line3.normalize().dx, closeTo(-0.998, tolerance)); + expect(line3.normalize().dy, closeTo(0.0554, tolerance)); + + expect(line4.normalize().dx, closeTo(-0.371, tolerance)); + expect(line4.normalize().dy, closeTo(-0.928, tolerance)); + + expect(line5.normalize().dx, closeTo(0.514, tolerance)); + expect(line5.normalize().dy, closeTo(0.857, tolerance)); + }); +} diff --git a/test/chart/base/render_base_chart_test.dart b/test/chart/base/render_base_chart_test.dart new file mode 100644 index 0000000..4890e40 --- /dev/null +++ b/test/chart/base/render_base_chart_test.dart @@ -0,0 +1,192 @@ +import 'package:fl_chart/src/chart/base/base_chart/base_chart_data.dart'; +import 'package:fl_chart/src/chart/base/base_chart/fl_touch_event.dart'; +import 'package:fl_chart/src/chart/base/base_chart/render_base_chart.dart'; +import 'package:fl_chart/src/chart/line_chart/line_chart_data.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'render_base_chart_test.mocks.dart'; + +@GenerateMocks([ + BuildContext, + PanGestureRecognizer, + TapGestureRecognizer, + LongPressGestureRecognizer, +]) +void main() { + group('RenderBaseChart', () { + late BuildContext mockContext; + late PanGestureRecognizer panGestureRecognizer; + late TapGestureRecognizer tapGestureRecognizer; + late LongPressGestureRecognizer longPressGestureRecognizer; + late TestTouchData data; + void touchCallback(_, __) {} + + setUp(() { + mockContext = MockBuildContext(); + panGestureRecognizer = MockPanGestureRecognizer(); + tapGestureRecognizer = MockTapGestureRecognizer(); + longPressGestureRecognizer = MockLongPressGestureRecognizer(); + data = TestTouchData( + false, + touchCallback, + null, + null, + ); + }); + + group('handleEvent', () { + test('respects canBeScaled for pan gestures for PointerDownEvent', () { + const pointerDownEvent = PointerDownEvent(); + final scalableChart = TestRenderBaseChart( + mockContext, + data, + canBeScaled: true, + panGestureRecognizerOverride: panGestureRecognizer, + tapGestureRecognizerOverride: tapGestureRecognizer, + longPressGestureRecognizerOverride: longPressGestureRecognizer, + ); + + final nonScalableChart = TestRenderBaseChart( + mockContext, + data, + canBeScaled: false, + panGestureRecognizerOverride: panGestureRecognizer, + tapGestureRecognizerOverride: tapGestureRecognizer, + longPressGestureRecognizerOverride: longPressGestureRecognizer, + ); + + final hitTestEntry = BoxHitTestEntry( + scalableChart, + Offset.zero, + ); + + scalableChart.handleEvent(pointerDownEvent, hitTestEntry); + verifyNever(panGestureRecognizer.addPointer(pointerDownEvent)); + verify(longPressGestureRecognizer.addPointer(pointerDownEvent)) + .called(1); + verify(tapGestureRecognizer.addPointer(pointerDownEvent)).called(1); + + nonScalableChart.handleEvent(pointerDownEvent, hitTestEntry); + verify(panGestureRecognizer.addPointer(pointerDownEvent)).called(1); + verify(longPressGestureRecognizer.addPointer(pointerDownEvent)) + .called(1); + verify(tapGestureRecognizer.addPointer(pointerDownEvent)).called(1); + }); + + test( + 'does not add pointers for PointerDownEvent when no ' + 'touchCallback provided', + () { + const pointerDownEvent = PointerDownEvent(); + final chart = TestRenderBaseChart( + mockContext, + TestTouchData( + false, + null, + null, + null, + ), + canBeScaled: true, + panGestureRecognizerOverride: panGestureRecognizer, + tapGestureRecognizerOverride: tapGestureRecognizer, + longPressGestureRecognizerOverride: longPressGestureRecognizer, + ); + + final hitTestEntry = BoxHitTestEntry( + chart, + Offset.zero, + ); + chart.handleEvent(pointerDownEvent, hitTestEntry); + + verifyNever(panGestureRecognizer.addPointer(pointerDownEvent)); + verifyNever(tapGestureRecognizer.addPointer(pointerDownEvent)); + verifyNever(longPressGestureRecognizer.addPointer(pointerDownEvent)); + }, + ); + + test('calls touchCallback for PointerHoverEvent', () { + late FlTouchEvent testEvent; + late LineTouchResponse? testResponse; + void callback(FlTouchEvent event, LineTouchResponse? response) { + testEvent = event; + testResponse = response; + } + + const pointerHoverEvent = PointerHoverEvent(); + final chart = TestRenderBaseChart( + mockContext, + TestTouchData( + false, + callback, + null, + null, + ), + canBeScaled: false, + panGestureRecognizerOverride: panGestureRecognizer, + tapGestureRecognizerOverride: tapGestureRecognizer, + longPressGestureRecognizerOverride: longPressGestureRecognizer, + ); + + final hitTestEntry = BoxHitTestEntry( + chart, + Offset.zero, + ); + chart.handleEvent(pointerHoverEvent, hitTestEntry); + + expect(testEvent, isA()); + expect(testResponse, isA()); + }); + }); + }); +} + +// Modify TestRenderBaseChart to track gesture recognizer calls +class TestRenderBaseChart extends RenderBaseChart { + TestRenderBaseChart( + BuildContext context, + FlTouchData? touchData, { + required bool canBeScaled, + required this.panGestureRecognizerOverride, + required this.tapGestureRecognizerOverride, + required this.longPressGestureRecognizerOverride, + }) : super(touchData, context, canBeScaled: canBeScaled); + + int panGestureAddPointerCallCount = 0; + int longPressGestureAddPointerCallCount = 0; + int tapGestureAddPointerCallCount = 0; + + final PanGestureRecognizer panGestureRecognizerOverride; + final TapGestureRecognizer tapGestureRecognizerOverride; + final LongPressGestureRecognizer longPressGestureRecognizerOverride; + + @override + void initGestureRecognizers() { + super.initGestureRecognizers(); + panGestureRecognizer = panGestureRecognizerOverride; + tapGestureRecognizer = tapGestureRecognizerOverride; + longPressGestureRecognizer = longPressGestureRecognizerOverride; + } + + @override + LineTouchResponse getResponseAtLocation(Offset localPosition) { + return LineTouchResponse( + touchLocation: Offset.zero, + touchChartCoordinate: Offset.zero, + lineBarSpots: [], + ); + } +} + +class TestTouchData extends FlTouchData { + TestTouchData( + super.enabled, + super.touchCallback, + super.mouseCursorResolver, + super.longPressDuration, + ); +} diff --git a/test/chart/base/render_base_chart_test.mocks.dart b/test/chart/base/render_base_chart_test.mocks.dart new file mode 100644 index 0000000..d7a7d9e --- /dev/null +++ b/test/chart/base/render_base_chart_test.mocks.dart @@ -0,0 +1,1558 @@ +// Mocks generated by Mockito 5.4.5 from annotations +// in fl_chart/test/chart/base/render_base_chart_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:ui' as _i12; + +import 'package:flutter/foundation.dart' as _i3; +import 'package:flutter/rendering.dart' as _i10; +import 'package:flutter/src/gestures/drag_details.dart' as _i9; +import 'package:flutter/src/gestures/events.dart' as _i11; +import 'package:flutter/src/gestures/long_press.dart' as _i14; +import 'package:flutter/src/gestures/monodrag.dart' as _i7; +import 'package:flutter/src/gestures/recognizer.dart' as _i5; +import 'package:flutter/src/gestures/tap.dart' as _i13; +import 'package:flutter/src/gestures/velocity_tracker.dart' as _i4; +import 'package:flutter/src/widgets/framework.dart' as _i2; +import 'package:flutter/src/widgets/notification_listener.dart' as _i6; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i8; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeWidget_0 extends _i1.SmartFake implements _i2.Widget { + _FakeWidget_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeInheritedWidget_1 extends _i1.SmartFake + implements _i2.InheritedWidget { + _FakeInheritedWidget_1(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeDiagnosticsNode_2 extends _i1.SmartFake + implements _i3.DiagnosticsNode { + _FakeDiagnosticsNode_2(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({ + _i3.TextTreeConfiguration? parentConfiguration, + _i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info, + }) => super.toString(); +} + +class _FakeVelocityTracker_3 extends _i1.SmartFake + implements _i4.VelocityTracker { + _FakeVelocityTracker_3(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeOffsetPair_4 extends _i1.SmartFake implements _i5.OffsetPair { + _FakeOffsetPair_4(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +/// A class which mocks [BuildContext]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockBuildContext extends _i1.Mock implements _i2.BuildContext { + MockBuildContext() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.Widget get widget => + (super.noSuchMethod( + Invocation.getter(#widget), + returnValue: _FakeWidget_0(this, Invocation.getter(#widget)), + ) + as _i2.Widget); + + @override + bool get mounted => + (super.noSuchMethod(Invocation.getter(#mounted), returnValue: false) + as bool); + + @override + bool get debugDoingBuild => + (super.noSuchMethod( + Invocation.getter(#debugDoingBuild), + returnValue: false, + ) + as bool); + + @override + _i2.InheritedWidget dependOnInheritedElement( + _i2.InheritedElement? ancestor, { + Object? aspect, + }) => + (super.noSuchMethod( + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + returnValue: _FakeInheritedWidget_1( + this, + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + ), + ) + as _i2.InheritedWidget); + + @override + void visitAncestorElements(_i2.ConditionalElementVisitor? visitor) => + super.noSuchMethod( + Invocation.method(#visitAncestorElements, [visitor]), + returnValueForMissingStub: null, + ); + + @override + void visitChildElements(_i2.ElementVisitor? visitor) => super.noSuchMethod( + Invocation.method(#visitChildElements, [visitor]), + returnValueForMissingStub: null, + ); + + @override + void dispatchNotification(_i6.Notification? notification) => + super.noSuchMethod( + Invocation.method(#dispatchNotification, [notification]), + returnValueForMissingStub: null, + ); + + @override + _i3.DiagnosticsNode describeElement( + String? name, { + _i3.DiagnosticsTreeStyle? style = _i3.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method(#describeElement, [name], {#style: style}), + returnValue: _FakeDiagnosticsNode_2( + this, + Invocation.method(#describeElement, [name], {#style: style}), + ), + ) + as _i3.DiagnosticsNode); + + @override + _i3.DiagnosticsNode describeWidget( + String? name, { + _i3.DiagnosticsTreeStyle? style = _i3.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method(#describeWidget, [name], {#style: style}), + returnValue: _FakeDiagnosticsNode_2( + this, + Invocation.method(#describeWidget, [name], {#style: style}), + ), + ) + as _i3.DiagnosticsNode); + + @override + List<_i3.DiagnosticsNode> describeMissingAncestor({ + required Type? expectedAncestorType, + }) => + (super.noSuchMethod( + Invocation.method(#describeMissingAncestor, [], { + #expectedAncestorType: expectedAncestorType, + }), + returnValue: <_i3.DiagnosticsNode>[], + ) + as List<_i3.DiagnosticsNode>); + + @override + _i3.DiagnosticsNode describeOwnershipChain(String? name) => + (super.noSuchMethod( + Invocation.method(#describeOwnershipChain, [name]), + returnValue: _FakeDiagnosticsNode_2( + this, + Invocation.method(#describeOwnershipChain, [name]), + ), + ) + as _i3.DiagnosticsNode); +} + +/// A class which mocks [PanGestureRecognizer]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPanGestureRecognizer extends _i1.Mock + implements _i7.PanGestureRecognizer { + MockPanGestureRecognizer() { + _i1.throwOnMissingStub(this); + } + + @override + String get debugDescription => + (super.noSuchMethod( + Invocation.getter(#debugDescription), + returnValue: _i8.dummyValue( + this, + Invocation.getter(#debugDescription), + ), + ) + as String); + + @override + _i5.DragStartBehavior get dragStartBehavior => + (super.noSuchMethod( + Invocation.getter(#dragStartBehavior), + returnValue: _i5.DragStartBehavior.down, + ) + as _i5.DragStartBehavior); + + @override + set dragStartBehavior(_i5.DragStartBehavior? _dragStartBehavior) => + super.noSuchMethod( + Invocation.setter(#dragStartBehavior, _dragStartBehavior), + returnValueForMissingStub: null, + ); + + @override + _i5.MultitouchDragStrategy get multitouchDragStrategy => + (super.noSuchMethod( + Invocation.getter(#multitouchDragStrategy), + returnValue: _i5.MultitouchDragStrategy.latestPointer, + ) + as _i5.MultitouchDragStrategy); + + @override + set multitouchDragStrategy( + _i5.MultitouchDragStrategy? _multitouchDragStrategy, + ) => super.noSuchMethod( + Invocation.setter(#multitouchDragStrategy, _multitouchDragStrategy), + returnValueForMissingStub: null, + ); + + @override + set onDown(_i9.GestureDragDownCallback? _onDown) => super.noSuchMethod( + Invocation.setter(#onDown, _onDown), + returnValueForMissingStub: null, + ); + + @override + set onStart(_i9.GestureDragStartCallback? _onStart) => super.noSuchMethod( + Invocation.setter(#onStart, _onStart), + returnValueForMissingStub: null, + ); + + @override + set onUpdate(_i9.GestureDragUpdateCallback? _onUpdate) => super.noSuchMethod( + Invocation.setter(#onUpdate, _onUpdate), + returnValueForMissingStub: null, + ); + + @override + set onEnd(_i7.GestureDragEndCallback? _onEnd) => super.noSuchMethod( + Invocation.setter(#onEnd, _onEnd), + returnValueForMissingStub: null, + ); + + @override + set onCancel(_i7.GestureDragCancelCallback? _onCancel) => super.noSuchMethod( + Invocation.setter(#onCancel, _onCancel), + returnValueForMissingStub: null, + ); + + @override + set minFlingDistance(double? _minFlingDistance) => super.noSuchMethod( + Invocation.setter(#minFlingDistance, _minFlingDistance), + returnValueForMissingStub: null, + ); + + @override + set minFlingVelocity(double? _minFlingVelocity) => super.noSuchMethod( + Invocation.setter(#minFlingVelocity, _minFlingVelocity), + returnValueForMissingStub: null, + ); + + @override + set maxFlingVelocity(double? _maxFlingVelocity) => super.noSuchMethod( + Invocation.setter(#maxFlingVelocity, _maxFlingVelocity), + returnValueForMissingStub: null, + ); + + @override + bool get onlyAcceptDragOnThreshold => + (super.noSuchMethod( + Invocation.getter(#onlyAcceptDragOnThreshold), + returnValue: false, + ) + as bool); + + @override + set onlyAcceptDragOnThreshold(bool? _onlyAcceptDragOnThreshold) => + super.noSuchMethod( + Invocation.setter( + #onlyAcceptDragOnThreshold, + _onlyAcceptDragOnThreshold, + ), + returnValueForMissingStub: null, + ); + + @override + _i7.GestureVelocityTrackerBuilder get velocityTrackerBuilder => + (super.noSuchMethod( + Invocation.getter(#velocityTrackerBuilder), + returnValue: + (_i10.PointerEvent event) => _FakeVelocityTracker_3( + this, + Invocation.getter(#velocityTrackerBuilder), + ), + ) + as _i7.GestureVelocityTrackerBuilder); + + @override + set velocityTrackerBuilder( + _i7.GestureVelocityTrackerBuilder? _velocityTrackerBuilder, + ) => super.noSuchMethod( + Invocation.setter(#velocityTrackerBuilder, _velocityTrackerBuilder), + returnValueForMissingStub: null, + ); + + @override + _i5.OffsetPair get lastPosition => + (super.noSuchMethod( + Invocation.getter(#lastPosition), + returnValue: _FakeOffsetPair_4( + this, + Invocation.getter(#lastPosition), + ), + ) + as _i5.OffsetPair); + + @override + double get globalDistanceMoved => + (super.noSuchMethod( + Invocation.getter(#globalDistanceMoved), + returnValue: 0.0, + ) + as double); + + @override + set team(_i5.GestureArenaTeam? value) => super.noSuchMethod( + Invocation.setter(#team, value), + returnValueForMissingStub: null, + ); + + @override + set gestureSettings(_i11.DeviceGestureSettings? _gestureSettings) => + super.noSuchMethod( + Invocation.setter(#gestureSettings, _gestureSettings), + returnValueForMissingStub: null, + ); + + @override + set supportedDevices(Set<_i12.PointerDeviceKind>? _supportedDevices) => + super.noSuchMethod( + Invocation.setter(#supportedDevices, _supportedDevices), + returnValueForMissingStub: null, + ); + + @override + _i5.AllowedButtonsFilter get allowedButtonsFilter => + (super.noSuchMethod( + Invocation.getter(#allowedButtonsFilter), + returnValue: (int buttons) => false, + ) + as _i5.AllowedButtonsFilter); + + @override + bool isFlingGesture( + _i4.VelocityEstimate? estimate, + _i12.PointerDeviceKind? kind, + ) => + (super.noSuchMethod( + Invocation.method(#isFlingGesture, [estimate, kind]), + returnValue: false, + ) + as bool); + + @override + _i9.DragEndDetails? considerFling( + _i4.VelocityEstimate? estimate, + _i12.PointerDeviceKind? kind, + ) => + (super.noSuchMethod(Invocation.method(#considerFling, [estimate, kind])) + as _i9.DragEndDetails?); + + @override + bool hasSufficientGlobalDistanceToAccept( + _i12.PointerDeviceKind? pointerDeviceKind, + double? deviceTouchSlop, + ) => + (super.noSuchMethod( + Invocation.method(#hasSufficientGlobalDistanceToAccept, [ + pointerDeviceKind, + deviceTouchSlop, + ]), + returnValue: false, + ) + as bool); + + @override + bool isPointerAllowed(_i10.PointerEvent? event) => + (super.noSuchMethod( + Invocation.method(#isPointerAllowed, [event]), + returnValue: false, + ) + as bool); + + @override + void addAllowedPointer(_i10.PointerDownEvent? event) => super.noSuchMethod( + Invocation.method(#addAllowedPointer, [event]), + returnValueForMissingStub: null, + ); + + @override + void addAllowedPointerPanZoom(_i11.PointerPanZoomStartEvent? event) => + super.noSuchMethod( + Invocation.method(#addAllowedPointerPanZoom, [event]), + returnValueForMissingStub: null, + ); + + @override + void handleEvent(_i10.PointerEvent? event) => super.noSuchMethod( + Invocation.method(#handleEvent, [event]), + returnValueForMissingStub: null, + ); + + @override + void acceptGesture(int? pointer) => super.noSuchMethod( + Invocation.method(#acceptGesture, [pointer]), + returnValueForMissingStub: null, + ); + + @override + void rejectGesture(int? pointer) => super.noSuchMethod( + Invocation.method(#rejectGesture, [pointer]), + returnValueForMissingStub: null, + ); + + @override + void didStopTrackingLastPointer(int? pointer) => super.noSuchMethod( + Invocation.method(#didStopTrackingLastPointer, [pointer]), + returnValueForMissingStub: null, + ); + + @override + void dispose() => super.noSuchMethod( + Invocation.method(#dispose, []), + returnValueForMissingStub: null, + ); + + @override + void debugFillProperties(_i3.DiagnosticPropertiesBuilder? properties) => + super.noSuchMethod( + Invocation.method(#debugFillProperties, [properties]), + returnValueForMissingStub: null, + ); + + @override + void handleNonAllowedPointer(_i10.PointerDownEvent? event) => + super.noSuchMethod( + Invocation.method(#handleNonAllowedPointer, [event]), + returnValueForMissingStub: null, + ); + + @override + void resolve(_i5.GestureDisposition? disposition) => super.noSuchMethod( + Invocation.method(#resolve, [disposition]), + returnValueForMissingStub: null, + ); + + @override + void resolvePointer(int? pointer, _i5.GestureDisposition? disposition) => + super.noSuchMethod( + Invocation.method(#resolvePointer, [pointer, disposition]), + returnValueForMissingStub: null, + ); + + @override + void startTrackingPointer(int? pointer, [_i10.Matrix4? transform]) => + super.noSuchMethod( + Invocation.method(#startTrackingPointer, [pointer, transform]), + returnValueForMissingStub: null, + ); + + @override + void stopTrackingPointer(int? pointer) => super.noSuchMethod( + Invocation.method(#stopTrackingPointer, [pointer]), + returnValueForMissingStub: null, + ); + + @override + void stopTrackingIfPointerNoLongerDown(_i10.PointerEvent? event) => + super.noSuchMethod( + Invocation.method(#stopTrackingIfPointerNoLongerDown, [event]), + returnValueForMissingStub: null, + ); + + @override + void addPointerPanZoom(_i11.PointerPanZoomStartEvent? event) => + super.noSuchMethod( + Invocation.method(#addPointerPanZoom, [event]), + returnValueForMissingStub: null, + ); + + @override + void addPointer(_i10.PointerDownEvent? event) => super.noSuchMethod( + Invocation.method(#addPointer, [event]), + returnValueForMissingStub: null, + ); + + @override + void handleNonAllowedPointerPanZoom(_i11.PointerPanZoomStartEvent? event) => + super.noSuchMethod( + Invocation.method(#handleNonAllowedPointerPanZoom, [event]), + returnValueForMissingStub: null, + ); + + @override + bool isPointerPanZoomAllowed(_i11.PointerPanZoomStartEvent? event) => + (super.noSuchMethod( + Invocation.method(#isPointerPanZoomAllowed, [event]), + returnValue: false, + ) + as bool); + + @override + _i12.PointerDeviceKind getKindForPointer(int? pointer) => + (super.noSuchMethod( + Invocation.method(#getKindForPointer, [pointer]), + returnValue: _i12.PointerDeviceKind.touch, + ) + as _i12.PointerDeviceKind); + + @override + T? invokeCallback( + String? name, + _i5.RecognizerCallback? callback, { + String Function()? debugReport, + }) => + (super.noSuchMethod( + Invocation.method( + #invokeCallback, + [name, callback], + {#debugReport: debugReport}, + ), + ) + as T?); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); + + @override + String toStringShallow({ + String? joiner = ', ', + _i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.debug, + }) => + (super.noSuchMethod( + Invocation.method(#toStringShallow, [], { + #joiner: joiner, + #minLevel: minLevel, + }), + returnValue: _i8.dummyValue( + this, + Invocation.method(#toStringShallow, [], { + #joiner: joiner, + #minLevel: minLevel, + }), + ), + ) + as String); + + @override + String toStringDeep({ + String? prefixLineOne = '', + String? prefixOtherLines, + _i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.debug, + int? wrapWidth = 65, + }) => + (super.noSuchMethod( + Invocation.method(#toStringDeep, [], { + #prefixLineOne: prefixLineOne, + #prefixOtherLines: prefixOtherLines, + #minLevel: minLevel, + #wrapWidth: wrapWidth, + }), + returnValue: _i8.dummyValue( + this, + Invocation.method(#toStringDeep, [], { + #prefixLineOne: prefixLineOne, + #prefixOtherLines: prefixOtherLines, + #minLevel: minLevel, + #wrapWidth: wrapWidth, + }), + ), + ) + as String); + + @override + String toStringShort() => + (super.noSuchMethod( + Invocation.method(#toStringShort, []), + returnValue: _i8.dummyValue( + this, + Invocation.method(#toStringShort, []), + ), + ) + as String); + + @override + _i3.DiagnosticsNode toDiagnosticsNode({ + String? name, + _i3.DiagnosticsTreeStyle? style, + }) => + (super.noSuchMethod( + Invocation.method(#toDiagnosticsNode, [], { + #name: name, + #style: style, + }), + returnValue: _FakeDiagnosticsNode_2( + this, + Invocation.method(#toDiagnosticsNode, [], { + #name: name, + #style: style, + }), + ), + ) + as _i3.DiagnosticsNode); + + @override + List<_i3.DiagnosticsNode> debugDescribeChildren() => + (super.noSuchMethod( + Invocation.method(#debugDescribeChildren, []), + returnValue: <_i3.DiagnosticsNode>[], + ) + as List<_i3.DiagnosticsNode>); +} + +/// A class which mocks [TapGestureRecognizer]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTapGestureRecognizer extends _i1.Mock + implements _i13.TapGestureRecognizer { + MockTapGestureRecognizer() { + _i1.throwOnMissingStub(this); + } + + @override + set onTapDown(_i13.GestureTapDownCallback? _onTapDown) => super.noSuchMethod( + Invocation.setter(#onTapDown, _onTapDown), + returnValueForMissingStub: null, + ); + + @override + set onTapUp(_i13.GestureTapUpCallback? _onTapUp) => super.noSuchMethod( + Invocation.setter(#onTapUp, _onTapUp), + returnValueForMissingStub: null, + ); + + @override + set onTap(_i13.GestureTapCallback? _onTap) => super.noSuchMethod( + Invocation.setter(#onTap, _onTap), + returnValueForMissingStub: null, + ); + + @override + set onTapCancel(_i13.GestureTapCancelCallback? _onTapCancel) => + super.noSuchMethod( + Invocation.setter(#onTapCancel, _onTapCancel), + returnValueForMissingStub: null, + ); + + @override + set onSecondaryTap(_i13.GestureTapCallback? _onSecondaryTap) => + super.noSuchMethod( + Invocation.setter(#onSecondaryTap, _onSecondaryTap), + returnValueForMissingStub: null, + ); + + @override + set onSecondaryTapDown(_i13.GestureTapDownCallback? _onSecondaryTapDown) => + super.noSuchMethod( + Invocation.setter(#onSecondaryTapDown, _onSecondaryTapDown), + returnValueForMissingStub: null, + ); + + @override + set onSecondaryTapUp(_i13.GestureTapUpCallback? _onSecondaryTapUp) => + super.noSuchMethod( + Invocation.setter(#onSecondaryTapUp, _onSecondaryTapUp), + returnValueForMissingStub: null, + ); + + @override + set onSecondaryTapCancel( + _i13.GestureTapCancelCallback? _onSecondaryTapCancel, + ) => super.noSuchMethod( + Invocation.setter(#onSecondaryTapCancel, _onSecondaryTapCancel), + returnValueForMissingStub: null, + ); + + @override + set onTertiaryTapDown(_i13.GestureTapDownCallback? _onTertiaryTapDown) => + super.noSuchMethod( + Invocation.setter(#onTertiaryTapDown, _onTertiaryTapDown), + returnValueForMissingStub: null, + ); + + @override + set onTertiaryTapUp(_i13.GestureTapUpCallback? _onTertiaryTapUp) => + super.noSuchMethod( + Invocation.setter(#onTertiaryTapUp, _onTertiaryTapUp), + returnValueForMissingStub: null, + ); + + @override + set onTertiaryTapCancel( + _i13.GestureTapCancelCallback? _onTertiaryTapCancel, + ) => super.noSuchMethod( + Invocation.setter(#onTertiaryTapCancel, _onTertiaryTapCancel), + returnValueForMissingStub: null, + ); + + @override + String get debugDescription => + (super.noSuchMethod( + Invocation.getter(#debugDescription), + returnValue: _i8.dummyValue( + this, + Invocation.getter(#debugDescription), + ), + ) + as String); + + @override + _i5.GestureRecognizerState get state => + (super.noSuchMethod( + Invocation.getter(#state), + returnValue: _i5.GestureRecognizerState.ready, + ) + as _i5.GestureRecognizerState); + + @override + set team(_i5.GestureArenaTeam? value) => super.noSuchMethod( + Invocation.setter(#team, value), + returnValueForMissingStub: null, + ); + + @override + set gestureSettings(_i11.DeviceGestureSettings? _gestureSettings) => + super.noSuchMethod( + Invocation.setter(#gestureSettings, _gestureSettings), + returnValueForMissingStub: null, + ); + + @override + set supportedDevices(Set<_i12.PointerDeviceKind>? _supportedDevices) => + super.noSuchMethod( + Invocation.setter(#supportedDevices, _supportedDevices), + returnValueForMissingStub: null, + ); + + @override + _i5.AllowedButtonsFilter get allowedButtonsFilter => + (super.noSuchMethod( + Invocation.getter(#allowedButtonsFilter), + returnValue: (int buttons) => false, + ) + as _i5.AllowedButtonsFilter); + + @override + bool isPointerAllowed(_i10.PointerDownEvent? event) => + (super.noSuchMethod( + Invocation.method(#isPointerAllowed, [event]), + returnValue: false, + ) + as bool); + + @override + void handleTapDown({required _i10.PointerDownEvent? down}) => + super.noSuchMethod( + Invocation.method(#handleTapDown, [], {#down: down}), + returnValueForMissingStub: null, + ); + + @override + void handleTapUp({ + required _i10.PointerDownEvent? down, + required _i10.PointerUpEvent? up, + }) => super.noSuchMethod( + Invocation.method(#handleTapUp, [], {#down: down, #up: up}), + returnValueForMissingStub: null, + ); + + @override + void handleTapCancel({ + required _i10.PointerDownEvent? down, + _i10.PointerCancelEvent? cancel, + required String? reason, + }) => super.noSuchMethod( + Invocation.method(#handleTapCancel, [], { + #down: down, + #cancel: cancel, + #reason: reason, + }), + returnValueForMissingStub: null, + ); + + @override + void addAllowedPointer(_i10.PointerDownEvent? event) => super.noSuchMethod( + Invocation.method(#addAllowedPointer, [event]), + returnValueForMissingStub: null, + ); + + @override + void startTrackingPointer(int? pointer, [_i10.Matrix4? transform]) => + super.noSuchMethod( + Invocation.method(#startTrackingPointer, [pointer, transform]), + returnValueForMissingStub: null, + ); + + @override + void handlePrimaryPointer(_i10.PointerEvent? event) => super.noSuchMethod( + Invocation.method(#handlePrimaryPointer, [event]), + returnValueForMissingStub: null, + ); + + @override + void resolve(_i5.GestureDisposition? disposition) => super.noSuchMethod( + Invocation.method(#resolve, [disposition]), + returnValueForMissingStub: null, + ); + + @override + void didExceedDeadline() => super.noSuchMethod( + Invocation.method(#didExceedDeadline, []), + returnValueForMissingStub: null, + ); + + @override + void acceptGesture(int? pointer) => super.noSuchMethod( + Invocation.method(#acceptGesture, [pointer]), + returnValueForMissingStub: null, + ); + + @override + void rejectGesture(int? pointer) => super.noSuchMethod( + Invocation.method(#rejectGesture, [pointer]), + returnValueForMissingStub: null, + ); + + @override + void debugFillProperties(_i3.DiagnosticPropertiesBuilder? properties) => + super.noSuchMethod( + Invocation.method(#debugFillProperties, [properties]), + returnValueForMissingStub: null, + ); + + @override + void handleNonAllowedPointer(_i10.PointerDownEvent? event) => + super.noSuchMethod( + Invocation.method(#handleNonAllowedPointer, [event]), + returnValueForMissingStub: null, + ); + + @override + void handleEvent(_i10.PointerEvent? event) => super.noSuchMethod( + Invocation.method(#handleEvent, [event]), + returnValueForMissingStub: null, + ); + + @override + void didExceedDeadlineWithEvent(_i10.PointerDownEvent? event) => + super.noSuchMethod( + Invocation.method(#didExceedDeadlineWithEvent, [event]), + returnValueForMissingStub: null, + ); + + @override + void didStopTrackingLastPointer(int? pointer) => super.noSuchMethod( + Invocation.method(#didStopTrackingLastPointer, [pointer]), + returnValueForMissingStub: null, + ); + + @override + void dispose() => super.noSuchMethod( + Invocation.method(#dispose, []), + returnValueForMissingStub: null, + ); + + @override + void resolvePointer(int? pointer, _i5.GestureDisposition? disposition) => + super.noSuchMethod( + Invocation.method(#resolvePointer, [pointer, disposition]), + returnValueForMissingStub: null, + ); + + @override + void stopTrackingPointer(int? pointer) => super.noSuchMethod( + Invocation.method(#stopTrackingPointer, [pointer]), + returnValueForMissingStub: null, + ); + + @override + void stopTrackingIfPointerNoLongerDown(_i10.PointerEvent? event) => + super.noSuchMethod( + Invocation.method(#stopTrackingIfPointerNoLongerDown, [event]), + returnValueForMissingStub: null, + ); + + @override + void addPointerPanZoom(_i11.PointerPanZoomStartEvent? event) => + super.noSuchMethod( + Invocation.method(#addPointerPanZoom, [event]), + returnValueForMissingStub: null, + ); + + @override + void addAllowedPointerPanZoom(_i11.PointerPanZoomStartEvent? event) => + super.noSuchMethod( + Invocation.method(#addAllowedPointerPanZoom, [event]), + returnValueForMissingStub: null, + ); + + @override + void addPointer(_i10.PointerDownEvent? event) => super.noSuchMethod( + Invocation.method(#addPointer, [event]), + returnValueForMissingStub: null, + ); + + @override + void handleNonAllowedPointerPanZoom(_i11.PointerPanZoomStartEvent? event) => + super.noSuchMethod( + Invocation.method(#handleNonAllowedPointerPanZoom, [event]), + returnValueForMissingStub: null, + ); + + @override + bool isPointerPanZoomAllowed(_i11.PointerPanZoomStartEvent? event) => + (super.noSuchMethod( + Invocation.method(#isPointerPanZoomAllowed, [event]), + returnValue: false, + ) + as bool); + + @override + _i12.PointerDeviceKind getKindForPointer(int? pointer) => + (super.noSuchMethod( + Invocation.method(#getKindForPointer, [pointer]), + returnValue: _i12.PointerDeviceKind.touch, + ) + as _i12.PointerDeviceKind); + + @override + T? invokeCallback( + String? name, + _i5.RecognizerCallback? callback, { + String Function()? debugReport, + }) => + (super.noSuchMethod( + Invocation.method( + #invokeCallback, + [name, callback], + {#debugReport: debugReport}, + ), + ) + as T?); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); + + @override + String toStringShallow({ + String? joiner = ', ', + _i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.debug, + }) => + (super.noSuchMethod( + Invocation.method(#toStringShallow, [], { + #joiner: joiner, + #minLevel: minLevel, + }), + returnValue: _i8.dummyValue( + this, + Invocation.method(#toStringShallow, [], { + #joiner: joiner, + #minLevel: minLevel, + }), + ), + ) + as String); + + @override + String toStringDeep({ + String? prefixLineOne = '', + String? prefixOtherLines, + _i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.debug, + int? wrapWidth = 65, + }) => + (super.noSuchMethod( + Invocation.method(#toStringDeep, [], { + #prefixLineOne: prefixLineOne, + #prefixOtherLines: prefixOtherLines, + #minLevel: minLevel, + #wrapWidth: wrapWidth, + }), + returnValue: _i8.dummyValue( + this, + Invocation.method(#toStringDeep, [], { + #prefixLineOne: prefixLineOne, + #prefixOtherLines: prefixOtherLines, + #minLevel: minLevel, + #wrapWidth: wrapWidth, + }), + ), + ) + as String); + + @override + String toStringShort() => + (super.noSuchMethod( + Invocation.method(#toStringShort, []), + returnValue: _i8.dummyValue( + this, + Invocation.method(#toStringShort, []), + ), + ) + as String); + + @override + _i3.DiagnosticsNode toDiagnosticsNode({ + String? name, + _i3.DiagnosticsTreeStyle? style, + }) => + (super.noSuchMethod( + Invocation.method(#toDiagnosticsNode, [], { + #name: name, + #style: style, + }), + returnValue: _FakeDiagnosticsNode_2( + this, + Invocation.method(#toDiagnosticsNode, [], { + #name: name, + #style: style, + }), + ), + ) + as _i3.DiagnosticsNode); + + @override + List<_i3.DiagnosticsNode> debugDescribeChildren() => + (super.noSuchMethod( + Invocation.method(#debugDescribeChildren, []), + returnValue: <_i3.DiagnosticsNode>[], + ) + as List<_i3.DiagnosticsNode>); +} + +/// A class which mocks [LongPressGestureRecognizer]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockLongPressGestureRecognizer extends _i1.Mock + implements _i14.LongPressGestureRecognizer { + MockLongPressGestureRecognizer() { + _i1.throwOnMissingStub(this); + } + + @override + set onLongPressDown(_i14.GestureLongPressDownCallback? _onLongPressDown) => + super.noSuchMethod( + Invocation.setter(#onLongPressDown, _onLongPressDown), + returnValueForMissingStub: null, + ); + + @override + set onLongPressCancel( + _i14.GestureLongPressCancelCallback? _onLongPressCancel, + ) => super.noSuchMethod( + Invocation.setter(#onLongPressCancel, _onLongPressCancel), + returnValueForMissingStub: null, + ); + + @override + set onLongPress(_i14.GestureLongPressCallback? _onLongPress) => + super.noSuchMethod( + Invocation.setter(#onLongPress, _onLongPress), + returnValueForMissingStub: null, + ); + + @override + set onLongPressStart(_i14.GestureLongPressStartCallback? _onLongPressStart) => + super.noSuchMethod( + Invocation.setter(#onLongPressStart, _onLongPressStart), + returnValueForMissingStub: null, + ); + + @override + set onLongPressMoveUpdate( + _i14.GestureLongPressMoveUpdateCallback? _onLongPressMoveUpdate, + ) => super.noSuchMethod( + Invocation.setter(#onLongPressMoveUpdate, _onLongPressMoveUpdate), + returnValueForMissingStub: null, + ); + + @override + set onLongPressUp(_i14.GestureLongPressUpCallback? _onLongPressUp) => + super.noSuchMethod( + Invocation.setter(#onLongPressUp, _onLongPressUp), + returnValueForMissingStub: null, + ); + + @override + set onLongPressEnd(_i14.GestureLongPressEndCallback? _onLongPressEnd) => + super.noSuchMethod( + Invocation.setter(#onLongPressEnd, _onLongPressEnd), + returnValueForMissingStub: null, + ); + + @override + set onSecondaryLongPressDown( + _i14.GestureLongPressDownCallback? _onSecondaryLongPressDown, + ) => super.noSuchMethod( + Invocation.setter(#onSecondaryLongPressDown, _onSecondaryLongPressDown), + returnValueForMissingStub: null, + ); + + @override + set onSecondaryLongPressCancel( + _i14.GestureLongPressCancelCallback? _onSecondaryLongPressCancel, + ) => super.noSuchMethod( + Invocation.setter(#onSecondaryLongPressCancel, _onSecondaryLongPressCancel), + returnValueForMissingStub: null, + ); + + @override + set onSecondaryLongPress( + _i14.GestureLongPressCallback? _onSecondaryLongPress, + ) => super.noSuchMethod( + Invocation.setter(#onSecondaryLongPress, _onSecondaryLongPress), + returnValueForMissingStub: null, + ); + + @override + set onSecondaryLongPressStart( + _i14.GestureLongPressStartCallback? _onSecondaryLongPressStart, + ) => super.noSuchMethod( + Invocation.setter(#onSecondaryLongPressStart, _onSecondaryLongPressStart), + returnValueForMissingStub: null, + ); + + @override + set onSecondaryLongPressMoveUpdate( + _i14.GestureLongPressMoveUpdateCallback? _onSecondaryLongPressMoveUpdate, + ) => super.noSuchMethod( + Invocation.setter( + #onSecondaryLongPressMoveUpdate, + _onSecondaryLongPressMoveUpdate, + ), + returnValueForMissingStub: null, + ); + + @override + set onSecondaryLongPressUp( + _i14.GestureLongPressUpCallback? _onSecondaryLongPressUp, + ) => super.noSuchMethod( + Invocation.setter(#onSecondaryLongPressUp, _onSecondaryLongPressUp), + returnValueForMissingStub: null, + ); + + @override + set onSecondaryLongPressEnd( + _i14.GestureLongPressEndCallback? _onSecondaryLongPressEnd, + ) => super.noSuchMethod( + Invocation.setter(#onSecondaryLongPressEnd, _onSecondaryLongPressEnd), + returnValueForMissingStub: null, + ); + + @override + set onTertiaryLongPressDown( + _i14.GestureLongPressDownCallback? _onTertiaryLongPressDown, + ) => super.noSuchMethod( + Invocation.setter(#onTertiaryLongPressDown, _onTertiaryLongPressDown), + returnValueForMissingStub: null, + ); + + @override + set onTertiaryLongPressCancel( + _i14.GestureLongPressCancelCallback? _onTertiaryLongPressCancel, + ) => super.noSuchMethod( + Invocation.setter(#onTertiaryLongPressCancel, _onTertiaryLongPressCancel), + returnValueForMissingStub: null, + ); + + @override + set onTertiaryLongPress( + _i14.GestureLongPressCallback? _onTertiaryLongPress, + ) => super.noSuchMethod( + Invocation.setter(#onTertiaryLongPress, _onTertiaryLongPress), + returnValueForMissingStub: null, + ); + + @override + set onTertiaryLongPressStart( + _i14.GestureLongPressStartCallback? _onTertiaryLongPressStart, + ) => super.noSuchMethod( + Invocation.setter(#onTertiaryLongPressStart, _onTertiaryLongPressStart), + returnValueForMissingStub: null, + ); + + @override + set onTertiaryLongPressMoveUpdate( + _i14.GestureLongPressMoveUpdateCallback? _onTertiaryLongPressMoveUpdate, + ) => super.noSuchMethod( + Invocation.setter( + #onTertiaryLongPressMoveUpdate, + _onTertiaryLongPressMoveUpdate, + ), + returnValueForMissingStub: null, + ); + + @override + set onTertiaryLongPressUp( + _i14.GestureLongPressUpCallback? _onTertiaryLongPressUp, + ) => super.noSuchMethod( + Invocation.setter(#onTertiaryLongPressUp, _onTertiaryLongPressUp), + returnValueForMissingStub: null, + ); + + @override + set onTertiaryLongPressEnd( + _i14.GestureLongPressEndCallback? _onTertiaryLongPressEnd, + ) => super.noSuchMethod( + Invocation.setter(#onTertiaryLongPressEnd, _onTertiaryLongPressEnd), + returnValueForMissingStub: null, + ); + + @override + String get debugDescription => + (super.noSuchMethod( + Invocation.getter(#debugDescription), + returnValue: _i8.dummyValue( + this, + Invocation.getter(#debugDescription), + ), + ) + as String); + + @override + _i5.GestureRecognizerState get state => + (super.noSuchMethod( + Invocation.getter(#state), + returnValue: _i5.GestureRecognizerState.ready, + ) + as _i5.GestureRecognizerState); + + @override + set team(_i5.GestureArenaTeam? value) => super.noSuchMethod( + Invocation.setter(#team, value), + returnValueForMissingStub: null, + ); + + @override + set gestureSettings(_i11.DeviceGestureSettings? _gestureSettings) => + super.noSuchMethod( + Invocation.setter(#gestureSettings, _gestureSettings), + returnValueForMissingStub: null, + ); + + @override + set supportedDevices(Set<_i12.PointerDeviceKind>? _supportedDevices) => + super.noSuchMethod( + Invocation.setter(#supportedDevices, _supportedDevices), + returnValueForMissingStub: null, + ); + + @override + _i5.AllowedButtonsFilter get allowedButtonsFilter => + (super.noSuchMethod( + Invocation.getter(#allowedButtonsFilter), + returnValue: (int buttons) => false, + ) + as _i5.AllowedButtonsFilter); + + @override + bool isPointerAllowed(_i10.PointerDownEvent? event) => + (super.noSuchMethod( + Invocation.method(#isPointerAllowed, [event]), + returnValue: false, + ) + as bool); + + @override + void didExceedDeadline() => super.noSuchMethod( + Invocation.method(#didExceedDeadline, []), + returnValueForMissingStub: null, + ); + + @override + void handlePrimaryPointer(_i10.PointerEvent? event) => super.noSuchMethod( + Invocation.method(#handlePrimaryPointer, [event]), + returnValueForMissingStub: null, + ); + + @override + void resolve(_i5.GestureDisposition? disposition) => super.noSuchMethod( + Invocation.method(#resolve, [disposition]), + returnValueForMissingStub: null, + ); + + @override + void acceptGesture(int? pointer) => super.noSuchMethod( + Invocation.method(#acceptGesture, [pointer]), + returnValueForMissingStub: null, + ); + + @override + void addAllowedPointer(_i10.PointerDownEvent? event) => super.noSuchMethod( + Invocation.method(#addAllowedPointer, [event]), + returnValueForMissingStub: null, + ); + + @override + void handleNonAllowedPointer(_i10.PointerDownEvent? event) => + super.noSuchMethod( + Invocation.method(#handleNonAllowedPointer, [event]), + returnValueForMissingStub: null, + ); + + @override + void handleEvent(_i10.PointerEvent? event) => super.noSuchMethod( + Invocation.method(#handleEvent, [event]), + returnValueForMissingStub: null, + ); + + @override + void didExceedDeadlineWithEvent(_i10.PointerDownEvent? event) => + super.noSuchMethod( + Invocation.method(#didExceedDeadlineWithEvent, [event]), + returnValueForMissingStub: null, + ); + + @override + void rejectGesture(int? pointer) => super.noSuchMethod( + Invocation.method(#rejectGesture, [pointer]), + returnValueForMissingStub: null, + ); + + @override + void didStopTrackingLastPointer(int? pointer) => super.noSuchMethod( + Invocation.method(#didStopTrackingLastPointer, [pointer]), + returnValueForMissingStub: null, + ); + + @override + void dispose() => super.noSuchMethod( + Invocation.method(#dispose, []), + returnValueForMissingStub: null, + ); + + @override + void debugFillProperties(_i3.DiagnosticPropertiesBuilder? properties) => + super.noSuchMethod( + Invocation.method(#debugFillProperties, [properties]), + returnValueForMissingStub: null, + ); + + @override + void resolvePointer(int? pointer, _i5.GestureDisposition? disposition) => + super.noSuchMethod( + Invocation.method(#resolvePointer, [pointer, disposition]), + returnValueForMissingStub: null, + ); + + @override + void startTrackingPointer(int? pointer, [_i10.Matrix4? transform]) => + super.noSuchMethod( + Invocation.method(#startTrackingPointer, [pointer, transform]), + returnValueForMissingStub: null, + ); + + @override + void stopTrackingPointer(int? pointer) => super.noSuchMethod( + Invocation.method(#stopTrackingPointer, [pointer]), + returnValueForMissingStub: null, + ); + + @override + void stopTrackingIfPointerNoLongerDown(_i10.PointerEvent? event) => + super.noSuchMethod( + Invocation.method(#stopTrackingIfPointerNoLongerDown, [event]), + returnValueForMissingStub: null, + ); + + @override + void addPointerPanZoom(_i11.PointerPanZoomStartEvent? event) => + super.noSuchMethod( + Invocation.method(#addPointerPanZoom, [event]), + returnValueForMissingStub: null, + ); + + @override + void addAllowedPointerPanZoom(_i11.PointerPanZoomStartEvent? event) => + super.noSuchMethod( + Invocation.method(#addAllowedPointerPanZoom, [event]), + returnValueForMissingStub: null, + ); + + @override + void addPointer(_i10.PointerDownEvent? event) => super.noSuchMethod( + Invocation.method(#addPointer, [event]), + returnValueForMissingStub: null, + ); + + @override + void handleNonAllowedPointerPanZoom(_i11.PointerPanZoomStartEvent? event) => + super.noSuchMethod( + Invocation.method(#handleNonAllowedPointerPanZoom, [event]), + returnValueForMissingStub: null, + ); + + @override + bool isPointerPanZoomAllowed(_i11.PointerPanZoomStartEvent? event) => + (super.noSuchMethod( + Invocation.method(#isPointerPanZoomAllowed, [event]), + returnValue: false, + ) + as bool); + + @override + _i12.PointerDeviceKind getKindForPointer(int? pointer) => + (super.noSuchMethod( + Invocation.method(#getKindForPointer, [pointer]), + returnValue: _i12.PointerDeviceKind.touch, + ) + as _i12.PointerDeviceKind); + + @override + T? invokeCallback( + String? name, + _i5.RecognizerCallback? callback, { + String Function()? debugReport, + }) => + (super.noSuchMethod( + Invocation.method( + #invokeCallback, + [name, callback], + {#debugReport: debugReport}, + ), + ) + as T?); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); + + @override + String toStringShallow({ + String? joiner = ', ', + _i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.debug, + }) => + (super.noSuchMethod( + Invocation.method(#toStringShallow, [], { + #joiner: joiner, + #minLevel: minLevel, + }), + returnValue: _i8.dummyValue( + this, + Invocation.method(#toStringShallow, [], { + #joiner: joiner, + #minLevel: minLevel, + }), + ), + ) + as String); + + @override + String toStringDeep({ + String? prefixLineOne = '', + String? prefixOtherLines, + _i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.debug, + int? wrapWidth = 65, + }) => + (super.noSuchMethod( + Invocation.method(#toStringDeep, [], { + #prefixLineOne: prefixLineOne, + #prefixOtherLines: prefixOtherLines, + #minLevel: minLevel, + #wrapWidth: wrapWidth, + }), + returnValue: _i8.dummyValue( + this, + Invocation.method(#toStringDeep, [], { + #prefixLineOne: prefixLineOne, + #prefixOtherLines: prefixOtherLines, + #minLevel: minLevel, + #wrapWidth: wrapWidth, + }), + ), + ) + as String); + + @override + String toStringShort() => + (super.noSuchMethod( + Invocation.method(#toStringShort, []), + returnValue: _i8.dummyValue( + this, + Invocation.method(#toStringShort, []), + ), + ) + as String); + + @override + _i3.DiagnosticsNode toDiagnosticsNode({ + String? name, + _i3.DiagnosticsTreeStyle? style, + }) => + (super.noSuchMethod( + Invocation.method(#toDiagnosticsNode, [], { + #name: name, + #style: style, + }), + returnValue: _FakeDiagnosticsNode_2( + this, + Invocation.method(#toDiagnosticsNode, [], { + #name: name, + #style: style, + }), + ), + ) + as _i3.DiagnosticsNode); + + @override + List<_i3.DiagnosticsNode> debugDescribeChildren() => + (super.noSuchMethod( + Invocation.method(#debugDescribeChildren, []), + returnValue: <_i3.DiagnosticsNode>[], + ) + as List<_i3.DiagnosticsNode>); +} diff --git a/test/chart/candlestick_chart/candlestick_chart_data_test.dart b/test/chart/candlestick_chart/candlestick_chart_data_test.dart new file mode 100644 index 0000000..8ad006d --- /dev/null +++ b/test/chart/candlestick_chart/candlestick_chart_data_test.dart @@ -0,0 +1,410 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../data_pool.dart'; + +void main() { + group('Candlestick data equality check', () { + test('CandlestickChartData equality test', () { + expect(candleStickChartData1 == candleStickChartData1Clone, true); + expect( + candleStickChartData1 == + candleStickChartData1Clone.copyWith(showingTooltipIndicators: []), + false, + ); + expect( + candleStickChartData1 == + candleStickChartData1Clone.copyWith( + borderData: FlBorderData( + show: true, + border: Border.all(color: Colors.red), + ), + ), + false, + ); + expect( + candleStickChartData1 == + candleStickChartData1Clone.copyWith( + borderData: FlBorderData( + show: true, + border: Border.all(color: Colors.green), + ), + ), + true, + ); + expect( + candleStickChartData1 == candleStickChartData1Clone.copyWith(maxX: 444), + false, + ); + expect( + candleStickChartData1 == + candleStickChartData1Clone.copyWith( + candlestickSpots: [ + CandlestickSpot( + x: 0, + open: 10, + high: 100, + low: 0, + close: 20, + ), + CandlestickSpot( + x: 10, + open: 30, + high: 110, + low: 10, + close: 20, + ), + CandlestickSpot( + x: 20, + open: 30, + high: 120, + low: 20, + close: 40, + ), + CandlestickSpot( + x: 30, + open: 40, + high: 130, + low: 30, + close: 50, + ), + ], + ), + true, + ); + expect( + candleStickChartData1 == + candleStickChartData1Clone.copyWith( + candlestickSpots: [ + CandlestickSpot(x: 5, open: 10, high: 100, low: 0, close: 20), + CandlestickSpot(x: 10, open: 20, high: 110, low: 10, close: 30), + CandlestickSpot(x: 20, open: 30, high: 120, low: 20, close: 40), + CandlestickSpot(x: 30, open: 40, high: 130, low: 30, close: 50), + ], + ), + false, + ); + expect( + candleStickChartData1 == + candleStickChartData1Clone.copyWith( + clipData: const FlClipData.all(), + ), + false, + ); + expect( + candleStickChartData1 == + candleStickChartData1Clone.copyWith( + gridData: const FlGridData( + verticalInterval: 12, + horizontalInterval: 22, + drawVerticalLine: false, + checkToShowVerticalLine: checkToShowLine, + getDrawingHorizontalLine: getDrawingLine, + ), + ), + true, + ); + expect( + candleStickChartData1 == + candleStickChartData1Clone.copyWith( + gridData: const FlGridData( + getDrawingHorizontalLine: gridGetDrawingLine, + getDrawingVerticalLine: gridGetDrawingLine, + checkToShowHorizontalLine: gridCheckToShowLine, + checkToShowVerticalLine: gridCheckToShowLine, + drawVerticalLine: false, + horizontalInterval: 33, + verticalInterval: 1, + ), + ), + false, + ); + expect( + candleStickChartData1 == + candleStickChartData1Clone.copyWith( + gridData: FlGridData( + show: false, + getDrawingHorizontalLine: (value) => const FlLine( + color: Colors.green, + strokeWidth: 12, + dashArray: [1, 2], + ), + getDrawingVerticalLine: (value) => const FlLine( + color: Colors.yellow, + strokeWidth: 33, + dashArray: [0, 1], + ), + checkToShowHorizontalLine: (value) => false, + checkToShowVerticalLine: (value) => true, + drawVerticalLine: false, + horizontalInterval: 32, + verticalInterval: 1, + ), + ), + false, + ); + expect( + candleStickChartData1 == + candleStickChartData1Clone.copyWith( + titlesData: MockData.flTitlesData1, + ), + true, + ); + expect( + candleStickChartData1 == + candleStickChartData1Clone.copyWith( + titlesData: const FlTitlesData( + leftTitles: AxisTitles( + axisNameSize: 332, + axisNameWidget: Text('title 1'), + ), + rightTitles: AxisTitles( + axisNameSize: 1326, + axisNameWidget: Text('title 3'), + sideTitles: SideTitles(reservedSize: 500, showTitles: true), + ), + topTitles: AxisTitles( + axisNameSize: 34, + axisNameWidget: Text('title 4'), + ), + bottomTitles: AxisTitles( + axisNameSize: 22, + axisNameWidget: Text('title 2'), + ), + ), + ), + false, + ); + expect( + candleStickChartData1 == + candleStickChartData1Clone.copyWith( + titlesData: const FlTitlesData( + leftTitles: AxisTitles( + axisNameSize: 33, + axisNameWidget: Text('title 1'), + ), + rightTitles: AxisTitles( + axisNameSize: 1326, + axisNameWidget: Text('title 3'), + sideTitles: SideTitles(reservedSize: 500, showTitles: true), + ), + topTitles: AxisTitles( + axisNameSize: 34, + axisNameWidget: Text('title 4'), + ), + bottomTitles: AxisTitles( + axisNameSize: 22, + axisNameWidget: Text('title 2'), + sideTitles: SideTitles(showTitles: true), + ), + ), + ), + false, + ); + expect( + candleStickChartData1 == + candleStickChartData1Clone.copyWith( + titlesData: const FlTitlesData( + leftTitles: AxisTitles( + axisNameSize: 33, + axisNameWidget: Text('title 1'), + ), + rightTitles: AxisTitles( + axisNameSize: 1326, + axisNameWidget: Text('title 1'), + sideTitles: SideTitles(reservedSize: 500, showTitles: true), + ), + topTitles: AxisTitles( + axisNameSize: 34, + axisNameWidget: Text('title 4'), + ), + bottomTitles: AxisTitles( + axisNameSize: 22, + axisNameWidget: Text('title 2'), + ), + ), + ), + false, + ); + expect( + candleStickChartData1 == + candleStickChartData1Clone.copyWith( + titlesData: const FlTitlesData( + leftTitles: AxisTitles( + axisNameSize: 33, + axisNameWidget: Text('title 1'), + ), + rightTitles: AxisTitles( + axisNameSize: 13262, + axisNameWidget: Text('title 3'), + sideTitles: SideTitles(reservedSize: 500, showTitles: true), + ), + topTitles: AxisTitles( + axisNameSize: 34, + axisNameWidget: Text('title 4'), + ), + bottomTitles: AxisTitles( + axisNameSize: 22, + axisNameWidget: Text('title 2'), + ), + ), + ), + false, + ); + + expect( + candleStickChartData1 == + candleStickChartData1Clone.copyWith(showingTooltipIndicators: []), + false, + ); + + expect( + candleStickChartData1 == + candleStickChartData1Clone + .copyWith(showingTooltipIndicators: [2, 1, 0]), + false, + ); + + expect( + candleStickChartData1 == + candleStickChartData1Clone.copyWith( + candlestickPainter: DefaultCandlestickPainter(), + ), + false, + ); + }); + + test('CandlestickSpot equality test', () { + expect(candlestickSpot1 == candlestickSpot1Clone, true); + expect(candlestickSpot1 == candlestickSpot2, false); + expect(candlestickSpot2 == candlestickSpot2.copyWith(), true); + expect(candlestickSpot2 == candlestickSpot3, false); + expect(candlestickSpot3 == candlestickSpot4, false); + expect( + candlestickSpot3 == + candlestickSpot3.copyWith( + show: false, + ), + false, + ); + expect( + candlestickSpot3 == + candlestickSpot3.copyWith( + show: true, + ), + true, + ); + }); + + test('CandlestickTouchData equality test', () { + final sample = CandlestickTouchData( + touchTooltipData: CandlestickTouchTooltipData( + maxContentWidth: 2, + getTooltipColor: candlestickChartGetTooltipRedColor, + tooltipPadding: const EdgeInsets.all(11), + ), + handleBuiltInTouches: false, + touchSpotThreshold: 23, + enabled: false, + ); + final sampleClone = CandlestickTouchData( + touchTooltipData: CandlestickTouchTooltipData( + maxContentWidth: 2, + getTooltipColor: candlestickChartGetTooltipRedColor, + tooltipPadding: const EdgeInsets.all(11), + ), + handleBuiltInTouches: false, + touchSpotThreshold: 23, + enabled: false, + ); + expect(sample == sampleClone, true); + + expect( + sample == + sampleClone.copyWith( + touchCallback: (event, response) {}, + ), + false, + ); + expect( + sample == + sampleClone.copyWith( + enabled: true, + ), + false, + ); + expect( + sample == + sampleClone.copyWith( + touchSpotThreshold: 22, + ), + false, + ); + expect( + sample == + sampleClone.copyWith( + handleBuiltInTouches: true, + ), + false, + ); + expect( + sample == + sampleClone.copyWith( + longPressDuration: Duration.zero, + ), + false, + ); + }); + + test('CandlestickTouchTooltipData equality test', () { + expect( + candlestickTouchTooltipData1 == candlestickTouchTooltipData1Clone, + true, + ); + expect( + candlestickTouchTooltipData1 == candlestickTouchTooltipData2, + false, + ); + expect( + candlestickTouchTooltipData1 == candlestickTouchTooltipData3, + false, + ); + }); + + test('CandlestickTooltipItem equality test', () { + final sample1 = CandlestickTooltipItem( + 'aa', + textStyle: const TextStyle(color: Colors.red), + bottomMargin: 23, + ); + final sample2 = CandlestickTooltipItem( + 'aa', + textStyle: const TextStyle(color: Colors.red), + bottomMargin: 23, + ); + expect(sample1 == sample2, true); + + var changed = CandlestickTooltipItem( + 'a3a', + textStyle: const TextStyle(color: Colors.red), + bottomMargin: 23, + ); + expect(sample1 == changed, false); + + changed = CandlestickTooltipItem( + 'aa', + textStyle: const TextStyle(color: Colors.green), + bottomMargin: 23, + ); + expect(sample1 == changed, false); + + changed = CandlestickTooltipItem( + 'aa', + textStyle: const TextStyle(color: Colors.red), + bottomMargin: 0, + ); + expect(sample1 == changed, false); + }); + }); +} diff --git a/test/chart/candlestick_chart/candlestick_chart_helper_test.dart b/test/chart/candlestick_chart/candlestick_chart_helper_test.dart new file mode 100644 index 0000000..b421d4b --- /dev/null +++ b/test/chart/candlestick_chart/candlestick_chart_helper_test.dart @@ -0,0 +1,93 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/candlestick_chart/candlestick_chart_helper.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../data_pool.dart'; + +void main() { + group('Check CandlestickChartHelper.calculateMaxAxisValues', () { + test('Test validity 1', () { + final candlestickSpots = [ + candlestickSpot1, + candlestickSpot2, + candlestickSpot3, + candlestickSpot4, + ]; + final (minX, maxX, minY, maxY) = + CandlestickChartHelper.calculateMaxAxisValues(candlestickSpots); + expect(minX, 0); + expect(maxX, 30); + expect(minY, 0); + expect(maxY, 130); + }); + + test('Test validity 2', () { + final scatterSpots = [ + CandlestickSpot( + x: 0, + open: 100, + close: 200, + high: 400, + low: 4, + ), + CandlestickSpot( + x: 1, + open: 500, + close: 200, + high: 800, + low: 40, + ), + ]; + final (minX, maxX, minY, maxY) = + CandlestickChartHelper.calculateMaxAxisValues(scatterSpots); + expect(minX, 0); + expect(maxX, 1); + expect(minY, 4); + expect(maxY, 800); + }); + + test('Test validity 3', () { + final candlestickSpots = []; + final (minX, maxX, minY, maxY) = + CandlestickChartHelper.calculateMaxAxisValues(candlestickSpots); + expect(minX, 0); + expect(maxX, 0); + expect(minY, 0); + expect(maxY, 0); + }); + + test('Test validity 4', () { + final candlestickSpots = [ + candlestickSpot1, + candlestickSpot2, + candlestickSpot3, + candlestickSpot4, + candlestickSpot5, + ]; + final (minX, maxX, minY, maxY) = + CandlestickChartHelper.calculateMaxAxisValues(candlestickSpots); + expect(minX, -50); + expect(maxX, 30); + expect(minY, -30); + expect(maxY, 130); + }); + + test('Test equality', () { + final candlestickSpots = [ + candlestickSpot1, + candlestickSpot2, + candlestickSpot3, + ]; + final candlestickSpotsClone = [ + candlestickSpot1Clone, + candlestickSpot2Clone, + candlestickSpot3, + ]; + final result1 = + CandlestickChartHelper.calculateMaxAxisValues(candlestickSpots); + final result2 = + CandlestickChartHelper.calculateMaxAxisValues(candlestickSpotsClone); + expect(result1, result2); + }); + }); +} diff --git a/test/chart/candlestick_chart/candlestick_chart_painter_test.dart b/test/chart/candlestick_chart/candlestick_chart_painter_test.dart new file mode 100644 index 0000000..6753a97 --- /dev/null +++ b/test/chart/candlestick_chart/candlestick_chart_painter_test.dart @@ -0,0 +1,1070 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart'; +import 'package:fl_chart/src/chart/candlestick_chart/candlestick_chart_painter.dart'; +import 'package:fl_chart/src/utils/canvas_wrapper.dart'; +import 'package:fl_chart/src/utils/utils.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import '../../helper_methods.dart'; +import '../data_pool.dart'; +import 'candlestick_chart_painter_test.mocks.dart'; + +@GenerateMocks([Canvas, CanvasWrapper, BuildContext, Utils]) +void main() { + group('paint()', () { + test('test 1 - simple paint call', () { + final utilsMainInstance = Utils(); + const viewSize = Size(400, 400); + final data = CandlestickChartData( + candlestickSpots: [ + candlestickSpot1, + candlestickSpot2, + candlestickSpot3, + ], + clipData: const FlClipData.all(), + ); + + final candlestickPainter = CandlestickChartPainter(); + final holder = PaintHolder( + data, + data, + TextScaler.noScaling, + ); + + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.getEfficientInterval(any, any)) + .thenAnswer((realInvocation) => 1.0); + when(mockUtils.getBestInitialIntervalValue(any, any, any)) + .thenAnswer((realInvocation) => 1.0); + final mockBuildContext = MockBuildContext(); + final mockCanvas = MockCanvas(); + final canvasWrapper = CanvasWrapper(mockCanvas, viewSize); + candlestickPainter.paint( + mockBuildContext, + canvasWrapper, + holder, + ); + + verify(mockCanvas.clipRect(any)).called(1); + verify(mockCanvas.drawLine(any, any, any)).called(6); + Utils.changeInstance(utilsMainInstance); + }); + }); + + group('drawAxisSpotIndicator()', () { + test('test 1 - draw both lines', () { + final utilsMainInstance = Utils(); + const viewSize = Size(400, 400); + final data = CandlestickChartData( + minX: 0, + maxX: 100, + minY: 0, + maxY: 100, + gridData: const FlGridData(show: false), + touchedPointIndicator: AxisSpotIndicator( + x: 50, + y: 50, + painter: AxisLinesIndicatorPainter( + verticalLineProvider: (x) => VerticalLine( + x: x, + color: MockData.color2, + strokeWidth: 8, + ), + horizontalLineProvider: (y) => HorizontalLine( + y: y, + color: MockData.color1, + strokeWidth: 4, + dashArray: [0, 1, 0], + ), + ), + ), + ); + + final candlestickPainter = CandlestickChartPainter(); + final holder = PaintHolder( + data, + data, + TextScaler.noScaling, + ); + + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.getEfficientInterval(any, any)) + .thenAnswer((realInvocation) => 1.0); + when(mockUtils.getBestInitialIntervalValue(any, any, any)) + .thenAnswer((realInvocation) => 1.0); + final mockBuildContext = MockBuildContext(); + final mockCanvas = MockCanvas(); + final canvasWrapper = CanvasWrapper(mockCanvas, viewSize); + final drawCalls = <(Path, Color, double)>[]; + when(mockCanvas.drawPath(any, any)).thenAnswer((invocation) { + drawCalls.add( + ( + invocation.positionalArguments[0] as Path, + (invocation.positionalArguments[1] as Paint).color, + (invocation.positionalArguments[1] as Paint).strokeWidth, + ), + ); + }); + + candlestickPainter.paint( + mockBuildContext, + canvasWrapper, + holder, + ); + + expect(drawCalls.length, 2); + + // Horizontal line + expect(drawCalls[0].$1.getBounds().left, 0); + expect(drawCalls[0].$1.getBounds().right, 400); + expect(drawCalls[0].$1.getBounds().top, 200); + expect(drawCalls[0].$1.getBounds().bottom, 200); + expect(drawCalls[0].$2.toARGB32(), MockData.color1.toARGB32()); + expect(drawCalls[0].$3, 4); + + /// Vertical line + expect(drawCalls[1].$1.getBounds().left, 200); + expect(drawCalls[1].$1.getBounds().right, 200); + expect(drawCalls[1].$1.getBounds().top, 0); + expect(drawCalls[1].$1.getBounds().bottom, 400); + expect(drawCalls[1].$2.toARGB32(), MockData.color2.toARGB32()); + expect(drawCalls[1].$3, 8); + + Utils.changeInstance(utilsMainInstance); + }); + + test('test 1 - draw only horizontal line', () { + final utilsMainInstance = Utils(); + const viewSize = Size(400, 400); + final data = CandlestickChartData( + minX: 0, + maxX: 100, + minY: 0, + maxY: 100, + gridData: const FlGridData(show: false), + touchedPointIndicator: AxisSpotIndicator( + y: 50, + painter: AxisLinesIndicatorPainter( + verticalLineProvider: null, + horizontalLineProvider: (y) => HorizontalLine( + y: y, + color: MockData.color1, + strokeWidth: 4, + ), + ), + ), + ); + + final candlestickPainter = CandlestickChartPainter(); + final holder = PaintHolder( + data, + data, + TextScaler.noScaling, + ); + + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.getEfficientInterval(any, any)) + .thenAnswer((realInvocation) => 1.0); + when(mockUtils.getBestInitialIntervalValue(any, any, any)) + .thenAnswer((realInvocation) => 1.0); + final mockBuildContext = MockBuildContext(); + final mockCanvas = MockCanvas(); + final canvasWrapper = CanvasWrapper(mockCanvas, viewSize); + final drawCalls = <(Path, Color, double)>[]; + when(mockCanvas.drawPath(any, any)).thenAnswer((invocation) { + drawCalls.add( + ( + invocation.positionalArguments[0] as Path, + (invocation.positionalArguments[1] as Paint).color, + (invocation.positionalArguments[1] as Paint).strokeWidth, + ), + ); + }); + + candlestickPainter.paint( + mockBuildContext, + canvasWrapper, + holder, + ); + + expect(drawCalls.length, 1); + + // Horizontal line + expect(drawCalls[0].$1.getBounds().left, 0); + expect(drawCalls[0].$1.getBounds().right, 400); + expect(drawCalls[0].$1.getBounds().top, 200); + expect(drawCalls[0].$1.getBounds().bottom, 200); + expect(drawCalls[0].$2.toARGB32(), MockData.color1.toARGB32()); + expect(drawCalls[0].$3, 4); + + Utils.changeInstance(utilsMainInstance); + }); + + test('test 1 - draw no line', () { + final utilsMainInstance = Utils(); + const viewSize = Size(400, 400); + final data = CandlestickChartData( + minX: 0, + maxX: 100, + minY: 0, + maxY: 100, + ); + + final candlestickPainter = CandlestickChartPainter(); + final holder = PaintHolder( + data, + data, + TextScaler.noScaling, + ); + + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.getEfficientInterval(any, any)) + .thenAnswer((realInvocation) => 1.0); + when(mockUtils.getBestInitialIntervalValue(any, any, any)) + .thenAnswer((realInvocation) => 1.0); + final mockBuildContext = MockBuildContext(); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + final drawCalls = <(Offset, Offset, Color, double)>[]; + when(mockCanvasWrapper.drawLine(any, any, any)).thenAnswer((invocation) { + drawCalls.add( + ( + invocation.positionalArguments[0] as Offset, + invocation.positionalArguments[1] as Offset, + (invocation.positionalArguments[2] as Paint).color, + (invocation.positionalArguments[2] as Paint).strokeWidth, + ), + ); + }); + + candlestickPainter.paint( + mockBuildContext, + mockCanvasWrapper, + holder, + ); + + expect(drawCalls.length, 0); + + Utils.changeInstance(utilsMainInstance); + }); + }); + + group('drawCandlesticks()', () { + test('test 1 - check drawing candlesticks', () { + final utilsMainInstance = Utils(); + const viewSize = Size(400, 400); + final data = CandlestickChartData( + candlestickSpots: [ + candlestickSpot1, + candlestickSpot2, + candlestickSpot3, + ], + candlestickPainter: DefaultCandlestickPainter( + candlestickStyleProvider: (CandlestickSpot spot, int index) { + final generalColor = + spot.isUp ? const Color(0xFF4CAF50) : const Color(0xFFEF5350); + return CandlestickStyle( + lineColor: generalColor, + lineWidth: (1 + index).toDouble(), + bodyStrokeColor: generalColor, + bodyStrokeWidth: (1 + index).toDouble(), + bodyFillColor: generalColor, + bodyWidth: (4 + index).toDouble(), + bodyRadius: index.toDouble(), + ); + }, + ), + ); + + final candlestickPainter = CandlestickChartPainter(); + final holder = PaintHolder( + data, + data, + TextScaler.noScaling, + ); + + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.getEfficientInterval(any, any)) + .thenAnswer((realInvocation) => 1.0); + when(mockUtils.getBestInitialIntervalValue(any, any, any)) + .thenAnswer((realInvocation) => 1.0); + final mockBuildContext = MockBuildContext(); + final mockCanvas = MockCanvas(); + final canvasWrapper = CanvasWrapper(mockCanvas, viewSize); + final lineDrawCalls = <(Offset, Offset, Color, double)>[]; + when(mockCanvas.drawLine(any, any, any)).thenAnswer((invocation) { + lineDrawCalls.add( + ( + invocation.positionalArguments[0] as Offset, + invocation.positionalArguments[1] as Offset, + (invocation.positionalArguments[2] as Paint).color, + (invocation.positionalArguments[2] as Paint).strokeWidth, + ), + ); + }); + + final rrectDrawCalls = <(RRect, Color, double)>[]; + when(mockCanvas.drawRRect(any, any)).thenAnswer((invocation) { + rrectDrawCalls.add( + ( + invocation.positionalArguments[0] as RRect, + (invocation.positionalArguments[1] as Paint).color, + (invocation.positionalArguments[1] as Paint).strokeWidth, + ), + ); + }); + + candlestickPainter.paint( + mockBuildContext, + canvasWrapper, + holder, + ); + + expect(lineDrawCalls.length, 6); + expect(rrectDrawCalls.length, 6); + + final expectedLines = [ + (const Offset(0, 400), const Offset(0, 366.7)), + (const Offset(0, 66.7), const Offset(0, 333.3)), + (const Offset(200, 366.7), const Offset(200, 333.3)), + (const Offset(200, 33.3), const Offset(200, 300)), + (const Offset(400, 333.3), const Offset(400, 300)), + (const Offset(400, 0), const Offset(400, 266.7)), + ]; + + final expectedLineColors = [ + const Color(0xFF4CAF50), + const Color(0xFF4CAF50), + const Color(0xFFEF5350), + const Color(0xFFEF5350), + const Color(0xFF4CAF50), + const Color(0xFF4CAF50), + ]; + + final expectedLineWidths = [ + 1.0, + 1.0, + 2.0, + 2.0, + 3.0, + 3.0, + ]; + + final expectedRRectCalls = <({ + double width, + double height, + double radius, + Color color, + double strokeWidth, + })>[ + ( + width: 4, + height: 33.3, + radius: 0, + color: const Color(0xFF4CAF50), + strokeWidth: 1, + ), + ( + width: 4, + height: 33.3, + radius: 0, + color: const Color(0xFF4CAF50), + strokeWidth: 1, + ), + ( + width: 5, + height: 33.3, + radius: 1, + color: const Color(0xFFEF5350), + strokeWidth: 2, + ), + ( + width: 5, + height: 33.3, + radius: 1, + color: const Color(0xFFEF5350), + strokeWidth: 2, + ), + ( + width: 6, + height: 33.3, + radius: 2, + color: const Color(0xFF4CAF50), + strokeWidth: 3, + ), + ( + width: 7, + height: 33.3, + radius: 2, + color: const Color(0xFF4CAF50), + strokeWidth: 3, + ), + ]; + + for (var i = 0; i < lineDrawCalls.length; i += 2) { + // bottom line + expect( + HelperMethods.equalsOffsets(lineDrawCalls[i].$1, expectedLines[i].$1), + true, + ); + expect( + HelperMethods.equalsOffsets(lineDrawCalls[i].$2, expectedLines[i].$2), + true, + ); + expect( + lineDrawCalls[i].$3.toARGB32(), + expectedLineColors[i].toARGB32(), + ); + expect(lineDrawCalls[i].$4, expectedLineWidths[i]); + + // top line + expect( + HelperMethods.equalsOffsets( + lineDrawCalls[i + 1].$1, + expectedLines[i + 1].$1, + ), + true, + ); + expect( + HelperMethods.equalsOffsets( + lineDrawCalls[i + 1].$2, + expectedLines[i + 1].$2, + ), + true, + ); + expect( + lineDrawCalls[i + 1].$3.toARGB32(), + expectedLineColors[i + 1].toARGB32(), + ); + expect(lineDrawCalls[i + 1].$4, expectedLineWidths[i + 1]); + + // body + expect( + rrectDrawCalls[i].$1.blRadiusX, + closeTo(expectedRRectCalls[i].radius, 0.1), + ); + expect( + rrectDrawCalls[i].$1.width, + closeTo(expectedRRectCalls[i].width, 0.1), + ); + expect( + rrectDrawCalls[i].$1.height, + closeTo(expectedRRectCalls[i].height, 0.1), + ); + expect( + rrectDrawCalls[i].$2.toARGB32(), + expectedRRectCalls[i].color.toARGB32(), + ); + + // body stroke + expect(rrectDrawCalls[i + 1].$3, expectedRRectCalls[i + 1].strokeWidth); + expect( + rrectDrawCalls[i + 1].$1.blRadiusY, + expectedRRectCalls[i].radius, + ); + } + + Utils.changeInstance(utilsMainInstance); + }); + }); + + group('drawTouchTooltips()', () { + test('test 1', () { + const viewSize = Size(100, 100); + + final data = CandlestickChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + candlestickSpots: [ + candlestickSpot1, + candlestickSpot2, + candlestickSpot3, + candlestickSpot4, + ], + showingTooltipIndicators: [0, 3], + titlesData: const FlTitlesData(show: false), + ); + + final candlestickChartPainter = CandlestickChartPainter(); + final holder = PaintHolder( + data, + data, + TextScaler.noScaling, + ); + final mockCanvasWrapper = MockCanvasWrapper(); + final mockBuildContext = MockBuildContext(); + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.getThemeAwareTextStyle(any, any)) + .thenReturn(const TextStyle(color: Color(0x00ffffff))); + when(mockUtils.calculateRotationOffset(any, any)).thenReturn(Offset.zero); + when(mockCanvasWrapper.size).thenReturn(viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + candlestickChartPainter.drawTouchTooltips( + mockBuildContext, + mockCanvasWrapper, + holder, + ); + + verify( + mockCanvasWrapper.drawRotated( + size: anyNamed('size'), + rotationOffset: anyNamed('rotationOffset'), + drawOffset: anyNamed('drawOffset'), + angle: anyNamed('angle'), + drawCallback: anyNamed('drawCallback'), + ), + ).called(2); + }); + }); + + group('drawTouchTooltip()', () { + test('test 1', () { + const viewSize = Size(100, 100); + + final data = CandlestickChartData( + minY: 0, + maxY: 1000, + minX: 0, + maxX: 1000, + candlestickSpots: [ + candlestickSpot1, + candlestickSpot2, + candlestickSpot3, + candlestickSpot4, + ], + showingTooltipIndicators: [0, 2, 3], + titlesData: const FlTitlesData(show: false), + candlestickTouchData: CandlestickTouchData( + touchTooltipData: CandlestickTouchTooltipData( + rotateAngle: 18, + getTooltipColor: (touchedSpot) => const Color(0xFF00FF00), + tooltipBorderRadius: const BorderRadius.only( + topLeft: Radius.circular(85), + topRight: Radius.circular(8), + ), + tooltipPadding: const EdgeInsets.all(12), + getTooltipItems: (_, __, ___) { + return CandlestickTooltipItem( + 'faketext', + textStyle: textStyle1, + textAlign: TextAlign.left, + textDirection: TextDirection.rtl, + children: [ + textSpan2, + textSpan1, + ], + ); + }, + ), + ), + ); + + final candlestickChartPainter = CandlestickChartPainter(); + final holder = PaintHolder( + data, + data, + TextScaler.noScaling, + ); + final mockCanvasWrapper = MockCanvasWrapper(); + final mockBuildContext = MockBuildContext(); + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.getThemeAwareTextStyle(any, any)).thenReturn(textStyle2); + when(mockUtils.calculateRotationOffset(any, any)).thenReturn(Offset.zero); + when(mockCanvasWrapper.size).thenReturn(viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + candlestickChartPainter.drawTouchTooltip( + mockBuildContext, + mockCanvasWrapper, + data.candlestickTouchData.touchTooltipData, + candlestickSpot1, + 0, + holder, + ); + + final verificationResult = verify( + mockCanvasWrapper.drawRotated( + size: anyNamed('size'), + drawOffset: anyNamed('drawOffset'), + angle: 18, + drawCallback: captureAnyNamed('drawCallback'), + ), + ); + + final passedDrawCallback = + verificationResult.captured.first as DrawCallback; + passedDrawCallback(); + + verificationResult.called(1); + + final captured2 = verifyInOrder([ + mockCanvasWrapper.drawRRect(captureAny, captureAny), + mockCanvasWrapper.drawText(captureAny, any), + ]).captured; + + final rRect = captured2[0][0] as RRect; + final bgPaint = captured2[0][1] as Paint; + final textPainter = captured2[1][0] as TextPainter; + + expect(rRect.blRadiusX, 0); + expect(rRect.blRadiusY, 0); + expect(rRect.tlRadiusY, 85); + expect(rRect.trRadiusX, 8); + + expect(bgPaint.color, const Color(0xFF00FF00)); + expect( + textPainter.text, + const TextSpan( + style: textStyle2, + text: 'faketext', + children: [ + textSpan2, + textSpan1, + ], + ), + ); + }); + + test('test 2', () { + const viewSize = Size(100, 100); + + final data = CandlestickChartData( + minY: 0, + maxY: 1000, + minX: 0, + maxX: 1000, + candlestickSpots: [ + candlestickSpot1, + candlestickSpot2, + candlestickSpot3, + candlestickSpot4, + ], + showingTooltipIndicators: [0, 2, 3], + titlesData: const FlTitlesData(show: false), + candlestickTouchData: CandlestickTouchData( + touchTooltipData: CandlestickTouchTooltipData( + rotateAngle: 18, + getTooltipColor: (touchedSpot) => const Color(0xFFFFFF00), + tooltipBorderRadius: BorderRadius.circular(22), + fitInsideHorizontally: false, + fitInsideVertically: true, + tooltipPadding: const EdgeInsets.all(12), + tooltipHorizontalAlignment: FLHorizontalAlignment.left, + getTooltipItems: (_, __, ___) => CandlestickTooltipItem( + 'faketext', + textStyle: textStyle2, + textAlign: TextAlign.right, + textDirection: TextDirection.ltr, + children: [ + textSpan1, + textSpan2, + ], + ), + ), + ), + ); + + final candlestickChartPainter = CandlestickChartPainter(); + final holder = PaintHolder( + data, + data, + TextScaler.noScaling, + ); + final mockCanvasWrapper = MockCanvasWrapper(); + final mockBuildContext = MockBuildContext(); + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.getThemeAwareTextStyle(any, any)).thenReturn(textStyle1); + when(mockUtils.calculateRotationOffset(any, any)).thenReturn(Offset.zero); + when(mockCanvasWrapper.size).thenReturn(viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + candlestickChartPainter.drawTouchTooltip( + mockBuildContext, + mockCanvasWrapper, + data.candlestickTouchData.touchTooltipData, + candlestickSpot1, + 0, + holder, + ); + + final verificationResult = verify( + mockCanvasWrapper.drawRotated( + size: anyNamed('size'), + drawOffset: anyNamed('drawOffset'), + angle: 18, + drawCallback: captureAnyNamed('drawCallback'), + ), + ); + + final passedDrawCallback = + verificationResult.captured.first as DrawCallback; + passedDrawCallback(); + + verificationResult.called(1); + + final captured2 = verifyInOrder([ + mockCanvasWrapper.drawRRect(captureAny, captureAny), + mockCanvasWrapper.drawText(captureAny, any), + ]).captured; + + final rRect = captured2[0][0] as RRect; + final bgPaint = captured2[0][1] as Paint; + final textPainter = captured2[1][0] as TextPainter; + + expect(rRect.blRadiusX, 22); + expect(rRect.tlRadiusY, 22); + + expect(rRect.left, -144); + + expect(bgPaint.color, const Color(0xFFFFFF00)); + expect( + textPainter.text, + const TextSpan( + style: textStyle1, + text: 'faketext', + children: [ + textSpan1, + textSpan2, + ], + ), + ); + }); + + test('test 3', () { + const viewSize = Size(100, 100); + + final data = CandlestickChartData( + minY: 0, + maxY: 1000, + minX: 0, + maxX: 1000, + candlestickSpots: [ + candlestickSpot1, + candlestickSpot2, + candlestickSpot3, + candlestickSpot4, + ], + showingTooltipIndicators: [0, 2, 3], + titlesData: const FlTitlesData(show: false), + candlestickTouchData: CandlestickTouchData( + touchTooltipData: CandlestickTouchTooltipData( + rotateAngle: 18, + getTooltipColor: (touchedSpot) => const Color(0xFFFFFF00), + tooltipBorderRadius: BorderRadius.circular(22), + fitInsideHorizontally: false, + fitInsideVertically: true, + tooltipPadding: const EdgeInsets.all(12), + tooltipHorizontalAlignment: FLHorizontalAlignment.right, + getTooltipItems: (_, __, ___) => CandlestickTooltipItem( + 'faketext', + textStyle: textStyle2, + textAlign: TextAlign.right, + textDirection: TextDirection.ltr, + children: [ + textSpan1, + textSpan2, + ], + ), + ), + ), + ); + + final candlestickChartPainter = CandlestickChartPainter(); + final holder = PaintHolder( + data, + data, + TextScaler.noScaling, + ); + final mockCanvasWrapper = MockCanvasWrapper(); + final mockBuildContext = MockBuildContext(); + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.getThemeAwareTextStyle(any, any)).thenReturn(textStyle1); + when(mockUtils.calculateRotationOffset(any, any)).thenReturn(Offset.zero); + when(mockCanvasWrapper.size).thenReturn(viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + candlestickChartPainter.drawTouchTooltip( + mockBuildContext, + mockCanvasWrapper, + data.candlestickTouchData.touchTooltipData, + candlestickSpot1, + 0, + holder, + ); + + final verificationResult = verify( + mockCanvasWrapper.drawRotated( + size: anyNamed('size'), + drawOffset: anyNamed('drawOffset'), + angle: 18, + drawCallback: captureAnyNamed('drawCallback'), + ), + ); + + final passedDrawCallback = + verificationResult.captured.first as DrawCallback; + passedDrawCallback(); + + verificationResult.called(1); + + final captured2 = verifyInOrder([ + mockCanvasWrapper.drawRRect(captureAny, captureAny), + mockCanvasWrapper.drawText(captureAny, any), + ]).captured; + + final rRect = captured2[0][0] as RRect; + final bgPaint = captured2[0][1] as Paint; + final textPainter = captured2[1][0] as TextPainter; + + expect(rRect.blRadiusX, 22); + expect(rRect.tlRadiusY, 22); + + expect(rRect.left, 0); + + expect(bgPaint.color, const Color(0xFFFFFF00)); + expect( + textPainter.text, + const TextSpan( + style: textStyle1, + text: 'faketext', + children: [ + textSpan1, + textSpan2, + ], + ), + ); + }); + + test('test 4', () { + const viewSize = Size(100, 100); + + final data = CandlestickChartData( + candlestickSpots: [ + candlestickSpot1, + candlestickSpot2, + candlestickSpot3, + candlestickSpot4, + ], + showingTooltipIndicators: [0, 1, 2, 3], + titlesData: const FlTitlesData(show: false), + candlestickTouchData: CandlestickTouchData( + touchTooltipData: CandlestickTouchTooltipData( + rotateAngle: 18, + getTooltipColor: (touchedSpot) => const Color(0xFFFFFF00), + tooltipBorderRadius: BorderRadius.circular(22), + fitInsideHorizontally: true, + fitInsideVertically: true, + tooltipPadding: const EdgeInsets.all(12), + tooltipHorizontalAlignment: FLHorizontalAlignment.right, + tooltipBorder: const BorderSide( + color: Color(0xFF00FF00), + width: 2, + ), + getTooltipItems: (_, __, ___) => CandlestickTooltipItem( + 'faketext', + textStyle: textStyle2, + textAlign: TextAlign.right, + textDirection: TextDirection.ltr, + children: [ + textSpan1, + textSpan2, + ], + ), + ), + ), + ); + + final candlestickChartPainter = CandlestickChartPainter(); + final holder = PaintHolder( + data, + data, + TextScaler.noScaling, + ); + final mockCanvasWrapper = MockCanvasWrapper(); + final mockBuildContext = MockBuildContext(); + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.getThemeAwareTextStyle(any, any)).thenReturn(textStyle1); + when(mockUtils.calculateRotationOffset(any, any)).thenReturn(Offset.zero); + when(mockCanvasWrapper.size).thenReturn(viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + candlestickChartPainter.drawTouchTooltip( + mockBuildContext, + mockCanvasWrapper, + data.candlestickTouchData.touchTooltipData, + candlestickSpot1, + 0, + holder, + ); + + final verificationResult = verify( + mockCanvasWrapper.drawRotated( + size: anyNamed('size'), + drawOffset: anyNamed('drawOffset'), + angle: 18, + drawCallback: captureAnyNamed('drawCallback'), + ), + ); + + final passedDrawCallback = + verificationResult.captured.first as DrawCallback; + passedDrawCallback(); + + verificationResult.called(1); + + final captured2 = verifyInOrder([ + mockCanvasWrapper.drawRRect(captureAny, captureAny), + mockCanvasWrapper.drawText(captureAny, any), + ]).captured; + + final rRect = captured2[0][0] as RRect; + final bgPaint = captured2[0][1] as Paint; + final textPainter = captured2[1][0] as TextPainter; + + expect(rRect.blRadiusX, 22); + expect(rRect.tlRadiusY, 22); + + expect(rRect.left, -44); + + expect(bgPaint.color, const Color(0xFFFFFF00)); + expect( + textPainter.text, + const TextSpan( + style: textStyle1, + text: 'faketext', + children: [ + textSpan1, + textSpan2, + ], + ), + ); + }); + }); + + group('handleTouch()', () { + test('test 1', () { + const viewSize = Size(100, 100); + final spots = [ + candlestickSpot1, + candlestickSpot2, + candlestickSpot3, + candlestickSpot4, + ]; + + final data = CandlestickChartData( + minY: 0, + maxY: 150, + minX: 0, + maxX: 30, + titlesData: const FlTitlesData(show: false), + candlestickSpots: spots, + candlestickTouchData: CandlestickTouchData( + touchSpotThreshold: 5, + ), + ); + + final candlestickChartPainter = CandlestickChartPainter(); + final holder = PaintHolder( + data, + data, + TextScaler.noScaling, + ); + expect( + candlestickChartPainter + .handleTouch( + const Offset(0, 1), + viewSize, + holder, + )! + .spot, + spots[0], + ); + + expect( + candlestickChartPainter + .handleTouch( + const Offset(0, 100), + viewSize, + holder, + )! + .spot, + spots[0], + ); + + expect( + candlestickChartPainter + .handleTouch( + const Offset(5, 100), + viewSize, + holder, + )! + .spot, + spots[0], + ); + + expect( + candlestickChartPainter.handleTouch( + const Offset(6, 100), + viewSize, + holder, + ), + null, + ); + + expect( + candlestickChartPainter + .handleTouch( + const Offset((1 / 3) * 100, 100), + viewSize, + holder, + )! + .spot, + spots[1], + ); + + expect( + candlestickChartPainter + .handleTouch( + const Offset((1 / 3) * 100, 0), + viewSize, + holder, + )! + .spot, + spots[1], + ); + + expect( + candlestickChartPainter + .handleTouch( + const Offset((2 / 3) * 100, 0), + viewSize, + holder, + )! + .spot, + spots[2], + ); + + expect( + candlestickChartPainter.handleTouch( + const Offset(((2 / 3) * 100) + 6, 0), + viewSize, + holder, + ), + null, + ); + }); + }); +} diff --git a/test/chart/candlestick_chart/candlestick_chart_painter_test.mocks.dart b/test/chart/candlestick_chart/candlestick_chart_painter_test.mocks.dart new file mode 100644 index 0000000..7601c24 --- /dev/null +++ b/test/chart/candlestick_chart/candlestick_chart_painter_test.mocks.dart @@ -0,0 +1,924 @@ +// Mocks generated by Mockito 5.4.5 from annotations +// in fl_chart/test/chart/candlestick_chart/candlestick_chart_painter_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:typed_data' as _i5; +import 'dart:ui' as _i2; + +import 'package:fl_chart/fl_chart.dart' as _i7; +import 'package:fl_chart/src/utils/canvas_wrapper.dart' as _i6; +import 'package:fl_chart/src/utils/utils.dart' as _i8; +import 'package:flutter/cupertino.dart' as _i3; +import 'package:flutter/foundation.dart' as _i4; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i9; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeRect_0 extends _i1.SmartFake implements _i2.Rect { + _FakeRect_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeCanvas_1 extends _i1.SmartFake implements _i2.Canvas { + _FakeCanvas_1(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeSize_2 extends _i1.SmartFake implements _i2.Size { + _FakeSize_2(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeWidget_3 extends _i1.SmartFake implements _i3.Widget { + _FakeWidget_3(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeInheritedWidget_4 extends _i1.SmartFake + implements _i3.InheritedWidget { + _FakeInheritedWidget_4(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeDiagnosticsNode_5 extends _i1.SmartFake + implements _i3.DiagnosticsNode { + _FakeDiagnosticsNode_5(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({ + _i4.TextTreeConfiguration? parentConfiguration, + _i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info, + }) => super.toString(); +} + +class _FakeOffset_6 extends _i1.SmartFake implements _i2.Offset { + _FakeOffset_6(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeBorderSide_7 extends _i1.SmartFake implements _i3.BorderSide { + _FakeBorderSide_7(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeTextStyle_8 extends _i1.SmartFake implements _i3.TextStyle { + _FakeTextStyle_8(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +/// A class which mocks [Canvas]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCanvas extends _i1.Mock implements _i2.Canvas { + MockCanvas() { + _i1.throwOnMissingStub(this); + } + + @override + void save() => super.noSuchMethod( + Invocation.method(#save, []), + returnValueForMissingStub: null, + ); + + @override + void saveLayer(_i2.Rect? bounds, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#saveLayer, [bounds, paint]), + returnValueForMissingStub: null, + ); + + @override + void restore() => super.noSuchMethod( + Invocation.method(#restore, []), + returnValueForMissingStub: null, + ); + + @override + void restoreToCount(int? count) => super.noSuchMethod( + Invocation.method(#restoreToCount, [count]), + returnValueForMissingStub: null, + ); + + @override + int getSaveCount() => + (super.noSuchMethod(Invocation.method(#getSaveCount, []), returnValue: 0) + as int); + + @override + void translate(double? dx, double? dy) => super.noSuchMethod( + Invocation.method(#translate, [dx, dy]), + returnValueForMissingStub: null, + ); + + @override + void scale(double? sx, [double? sy]) => super.noSuchMethod( + Invocation.method(#scale, [sx, sy]), + returnValueForMissingStub: null, + ); + + @override + void rotate(double? radians) => super.noSuchMethod( + Invocation.method(#rotate, [radians]), + returnValueForMissingStub: null, + ); + + @override + void skew(double? sx, double? sy) => super.noSuchMethod( + Invocation.method(#skew, [sx, sy]), + returnValueForMissingStub: null, + ); + + @override + void transform(_i5.Float64List? matrix4) => super.noSuchMethod( + Invocation.method(#transform, [matrix4]), + returnValueForMissingStub: null, + ); + + @override + _i5.Float64List getTransform() => + (super.noSuchMethod( + Invocation.method(#getTransform, []), + returnValue: _i5.Float64List(0), + ) + as _i5.Float64List); + + @override + void clipRect( + _i2.Rect? rect, { + _i2.ClipOp? clipOp = _i2.ClipOp.intersect, + bool? doAntiAlias = true, + }) => super.noSuchMethod( + Invocation.method( + #clipRect, + [rect], + {#clipOp: clipOp, #doAntiAlias: doAntiAlias}, + ), + returnValueForMissingStub: null, + ); + + @override + void clipRRect(_i2.RRect? rrect, {bool? doAntiAlias = true}) => + super.noSuchMethod( + Invocation.method(#clipRRect, [rrect], {#doAntiAlias: doAntiAlias}), + returnValueForMissingStub: null, + ); + + @override + void clipPath(_i2.Path? path, {bool? doAntiAlias = true}) => + super.noSuchMethod( + Invocation.method(#clipPath, [path], {#doAntiAlias: doAntiAlias}), + returnValueForMissingStub: null, + ); + + @override + _i2.Rect getLocalClipBounds() => + (super.noSuchMethod( + Invocation.method(#getLocalClipBounds, []), + returnValue: _FakeRect_0( + this, + Invocation.method(#getLocalClipBounds, []), + ), + ) + as _i2.Rect); + + @override + _i2.Rect getDestinationClipBounds() => + (super.noSuchMethod( + Invocation.method(#getDestinationClipBounds, []), + returnValue: _FakeRect_0( + this, + Invocation.method(#getDestinationClipBounds, []), + ), + ) + as _i2.Rect); + + @override + void drawColor(_i2.Color? color, _i2.BlendMode? blendMode) => + super.noSuchMethod( + Invocation.method(#drawColor, [color, blendMode]), + returnValueForMissingStub: null, + ); + + @override + void drawLine(_i2.Offset? p1, _i2.Offset? p2, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawLine, [p1, p2, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawPaint(_i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawPaint, [paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRect(_i2.Rect? rect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRect, [rect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRRect(_i2.RRect? rrect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRRect, [rrect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawDRRect(_i2.RRect? outer, _i2.RRect? inner, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawDRRect, [outer, inner, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawOval(_i2.Rect? rect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawOval, [rect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawCircle(_i2.Offset? c, double? radius, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawCircle, [c, radius, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawArc( + _i2.Rect? rect, + double? startAngle, + double? sweepAngle, + bool? useCenter, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawArc, [ + rect, + startAngle, + sweepAngle, + useCenter, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawPath(_i2.Path? path, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawPath, [path, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImage(_i2.Image? image, _i2.Offset? offset, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawImage, [image, offset, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImageRect( + _i2.Image? image, + _i2.Rect? src, + _i2.Rect? dst, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawImageRect, [image, src, dst, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImageNine( + _i2.Image? image, + _i2.Rect? center, + _i2.Rect? dst, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawImageNine, [image, center, dst, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawPicture(_i2.Picture? picture) => super.noSuchMethod( + Invocation.method(#drawPicture, [picture]), + returnValueForMissingStub: null, + ); + + @override + void drawParagraph(_i2.Paragraph? paragraph, _i2.Offset? offset) => + super.noSuchMethod( + Invocation.method(#drawParagraph, [paragraph, offset]), + returnValueForMissingStub: null, + ); + + @override + void drawPoints( + _i2.PointMode? pointMode, + List<_i2.Offset>? points, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawPoints, [pointMode, points, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRawPoints( + _i2.PointMode? pointMode, + _i5.Float32List? points, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawRawPoints, [pointMode, points, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawVertices( + _i2.Vertices? vertices, + _i2.BlendMode? blendMode, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawVertices, [vertices, blendMode, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawAtlas( + _i2.Image? atlas, + List<_i2.RSTransform>? transforms, + List<_i2.Rect>? rects, + List<_i2.Color>? colors, + _i2.BlendMode? blendMode, + _i2.Rect? cullRect, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawAtlas, [ + atlas, + transforms, + rects, + colors, + blendMode, + cullRect, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawRawAtlas( + _i2.Image? atlas, + _i5.Float32List? rstTransforms, + _i5.Float32List? rects, + _i5.Int32List? colors, + _i2.BlendMode? blendMode, + _i2.Rect? cullRect, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawRawAtlas, [ + atlas, + rstTransforms, + rects, + colors, + blendMode, + cullRect, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawShadow( + _i2.Path? path, + _i2.Color? color, + double? elevation, + bool? transparentOccluder, + ) => super.noSuchMethod( + Invocation.method(#drawShadow, [ + path, + color, + elevation, + transparentOccluder, + ]), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [CanvasWrapper]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCanvasWrapper extends _i1.Mock implements _i6.CanvasWrapper { + MockCanvasWrapper() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.Canvas get canvas => + (super.noSuchMethod( + Invocation.getter(#canvas), + returnValue: _FakeCanvas_1(this, Invocation.getter(#canvas)), + ) + as _i2.Canvas); + + @override + _i2.Size get size => + (super.noSuchMethod( + Invocation.getter(#size), + returnValue: _FakeSize_2(this, Invocation.getter(#size)), + ) + as _i2.Size); + + @override + void drawRRect(_i2.RRect? rrect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRRect, [rrect, paint]), + returnValueForMissingStub: null, + ); + + @override + void save() => super.noSuchMethod( + Invocation.method(#save, []), + returnValueForMissingStub: null, + ); + + @override + void restore() => super.noSuchMethod( + Invocation.method(#restore, []), + returnValueForMissingStub: null, + ); + + @override + void clipRect( + _i2.Rect? rect, { + _i2.ClipOp? clipOp = _i2.ClipOp.intersect, + bool? doAntiAlias = true, + }) => super.noSuchMethod( + Invocation.method( + #clipRect, + [rect], + {#clipOp: clipOp, #doAntiAlias: doAntiAlias}, + ), + returnValueForMissingStub: null, + ); + + @override + void translate(double? dx, double? dy) => super.noSuchMethod( + Invocation.method(#translate, [dx, dy]), + returnValueForMissingStub: null, + ); + + @override + void rotate(double? radius) => super.noSuchMethod( + Invocation.method(#rotate, [radius]), + returnValueForMissingStub: null, + ); + + @override + void drawPath(_i2.Path? path, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawPath, [path, paint]), + returnValueForMissingStub: null, + ); + + @override + void saveLayer(_i2.Rect? bounds, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#saveLayer, [bounds, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawPicture(_i2.Picture? picture) => super.noSuchMethod( + Invocation.method(#drawPicture, [picture]), + returnValueForMissingStub: null, + ); + + @override + void drawImage(_i2.Image? image, _i2.Offset? offset, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawImage, [image, offset, paint]), + returnValueForMissingStub: null, + ); + + @override + void clipPath(_i2.Path? path, {bool? doAntiAlias = true}) => + super.noSuchMethod( + Invocation.method(#clipPath, [path], {#doAntiAlias: doAntiAlias}), + returnValueForMissingStub: null, + ); + + @override + void drawRect(_i2.Rect? rect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRect, [rect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawLine(_i2.Offset? p1, _i2.Offset? p2, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawLine, [p1, p2, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawCircle(_i2.Offset? center, double? radius, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawCircle, [center, radius, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawArc( + _i2.Rect? rect, + double? startAngle, + double? sweepAngle, + bool? useCenter, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawArc, [ + rect, + startAngle, + sweepAngle, + useCenter, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawText( + _i3.TextPainter? tp, + _i2.Offset? offset, [ + double? rotateAngle, + ]) => super.noSuchMethod( + Invocation.method(#drawText, [tp, offset, rotateAngle]), + returnValueForMissingStub: null, + ); + + @override + void drawVerticalText(_i3.TextPainter? tp, _i2.Offset? offset) => + super.noSuchMethod( + Invocation.method(#drawVerticalText, [tp, offset]), + returnValueForMissingStub: null, + ); + + @override + void drawDot( + _i7.FlDotPainter? painter, + _i7.FlSpot? spot, + _i2.Offset? offset, + ) => super.noSuchMethod( + Invocation.method(#drawDot, [painter, spot, offset]), + returnValueForMissingStub: null, + ); + + @override + void drawErrorIndicator( + _i7.FlSpotErrorRangePainter? painter, + _i7.FlSpot? origin, + _i2.Offset? offset, + _i2.Rect? errorRelativeRect, + _i7.AxisChartData? axisData, + ) => super.noSuchMethod( + Invocation.method(#drawErrorIndicator, [ + painter, + origin, + offset, + errorRelativeRect, + axisData, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawRotated({ + required _i2.Size? size, + _i2.Offset? rotationOffset = _i2.Offset.zero, + _i2.Offset? drawOffset = _i2.Offset.zero, + required double? angle, + required _i6.DrawCallback? drawCallback, + }) => super.noSuchMethod( + Invocation.method(#drawRotated, [], { + #size: size, + #rotationOffset: rotationOffset, + #drawOffset: drawOffset, + #angle: angle, + #drawCallback: drawCallback, + }), + returnValueForMissingStub: null, + ); + + @override + void drawDashedLine( + _i2.Offset? from, + _i2.Offset? to, + _i2.Paint? painter, + List? dashArray, + ) => super.noSuchMethod( + Invocation.method(#drawDashedLine, [from, to, painter, dashArray]), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [BuildContext]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockBuildContext extends _i1.Mock implements _i3.BuildContext { + MockBuildContext() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Widget get widget => + (super.noSuchMethod( + Invocation.getter(#widget), + returnValue: _FakeWidget_3(this, Invocation.getter(#widget)), + ) + as _i3.Widget); + + @override + bool get mounted => + (super.noSuchMethod(Invocation.getter(#mounted), returnValue: false) + as bool); + + @override + bool get debugDoingBuild => + (super.noSuchMethod( + Invocation.getter(#debugDoingBuild), + returnValue: false, + ) + as bool); + + @override + _i3.InheritedWidget dependOnInheritedElement( + _i3.InheritedElement? ancestor, { + Object? aspect, + }) => + (super.noSuchMethod( + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + returnValue: _FakeInheritedWidget_4( + this, + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + ), + ) + as _i3.InheritedWidget); + + @override + void visitAncestorElements(_i3.ConditionalElementVisitor? visitor) => + super.noSuchMethod( + Invocation.method(#visitAncestorElements, [visitor]), + returnValueForMissingStub: null, + ); + + @override + void visitChildElements(_i3.ElementVisitor? visitor) => super.noSuchMethod( + Invocation.method(#visitChildElements, [visitor]), + returnValueForMissingStub: null, + ); + + @override + void dispatchNotification(_i3.Notification? notification) => + super.noSuchMethod( + Invocation.method(#dispatchNotification, [notification]), + returnValueForMissingStub: null, + ); + + @override + _i3.DiagnosticsNode describeElement( + String? name, { + _i4.DiagnosticsTreeStyle? style = _i4.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method(#describeElement, [name], {#style: style}), + returnValue: _FakeDiagnosticsNode_5( + this, + Invocation.method(#describeElement, [name], {#style: style}), + ), + ) + as _i3.DiagnosticsNode); + + @override + _i3.DiagnosticsNode describeWidget( + String? name, { + _i4.DiagnosticsTreeStyle? style = _i4.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method(#describeWidget, [name], {#style: style}), + returnValue: _FakeDiagnosticsNode_5( + this, + Invocation.method(#describeWidget, [name], {#style: style}), + ), + ) + as _i3.DiagnosticsNode); + + @override + List<_i3.DiagnosticsNode> describeMissingAncestor({ + required Type? expectedAncestorType, + }) => + (super.noSuchMethod( + Invocation.method(#describeMissingAncestor, [], { + #expectedAncestorType: expectedAncestorType, + }), + returnValue: <_i3.DiagnosticsNode>[], + ) + as List<_i3.DiagnosticsNode>); + + @override + _i3.DiagnosticsNode describeOwnershipChain(String? name) => + (super.noSuchMethod( + Invocation.method(#describeOwnershipChain, [name]), + returnValue: _FakeDiagnosticsNode_5( + this, + Invocation.method(#describeOwnershipChain, [name]), + ), + ) + as _i3.DiagnosticsNode); +} + +/// A class which mocks [Utils]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockUtils extends _i1.Mock implements _i8.Utils { + MockUtils() { + _i1.throwOnMissingStub(this); + } + + @override + double radians(double? degrees) => + (super.noSuchMethod( + Invocation.method(#radians, [degrees]), + returnValue: 0.0, + ) + as double); + + @override + double degrees(double? radians) => + (super.noSuchMethod( + Invocation.method(#degrees, [radians]), + returnValue: 0.0, + ) + as double); + + @override + double translateRotatedPosition(double? size, double? degree) => + (super.noSuchMethod( + Invocation.method(#translateRotatedPosition, [size, degree]), + returnValue: 0.0, + ) + as double); + + @override + _i2.Offset calculateRotationOffset(_i2.Size? size, double? degree) => + (super.noSuchMethod( + Invocation.method(#calculateRotationOffset, [size, degree]), + returnValue: _FakeOffset_6( + this, + Invocation.method(#calculateRotationOffset, [size, degree]), + ), + ) + as _i2.Offset); + + @override + _i3.BorderRadius? normalizeBorderRadius( + _i3.BorderRadius? borderRadius, + double? width, + ) => + (super.noSuchMethod( + Invocation.method(#normalizeBorderRadius, [borderRadius, width]), + ) + as _i3.BorderRadius?); + + @override + _i3.BorderSide normalizeBorderSide( + _i3.BorderSide? borderSide, + double? width, + ) => + (super.noSuchMethod( + Invocation.method(#normalizeBorderSide, [borderSide, width]), + returnValue: _FakeBorderSide_7( + this, + Invocation.method(#normalizeBorderSide, [borderSide, width]), + ), + ) + as _i3.BorderSide); + + @override + double getEfficientInterval( + double? axisViewSize, + double? diffInAxis, { + double? pixelPerInterval = 40.0, + }) => + (super.noSuchMethod( + Invocation.method( + #getEfficientInterval, + [axisViewSize, diffInAxis], + {#pixelPerInterval: pixelPerInterval}, + ), + returnValue: 0.0, + ) + as double); + + @override + double roundInterval(double? input) => + (super.noSuchMethod( + Invocation.method(#roundInterval, [input]), + returnValue: 0.0, + ) + as double); + + @override + int getFractionDigits(double? value) => + (super.noSuchMethod( + Invocation.method(#getFractionDigits, [value]), + returnValue: 0, + ) + as int); + + @override + String formatNumber(double? axisMin, double? axisMax, double? axisValue) => + (super.noSuchMethod( + Invocation.method(#formatNumber, [axisMin, axisMax, axisValue]), + returnValue: _i9.dummyValue( + this, + Invocation.method(#formatNumber, [axisMin, axisMax, axisValue]), + ), + ) + as String); + + @override + _i3.TextStyle getThemeAwareTextStyle( + _i3.BuildContext? context, + _i3.TextStyle? providedStyle, + ) => + (super.noSuchMethod( + Invocation.method(#getThemeAwareTextStyle, [ + context, + providedStyle, + ]), + returnValue: _FakeTextStyle_8( + this, + Invocation.method(#getThemeAwareTextStyle, [ + context, + providedStyle, + ]), + ), + ) + as _i3.TextStyle); + + @override + double getBestInitialIntervalValue( + double? min, + double? max, + double? interval, { + double? baseline = 0.0, + }) => + (super.noSuchMethod( + Invocation.method( + #getBestInitialIntervalValue, + [min, max, interval], + {#baseline: baseline}, + ), + returnValue: 0.0, + ) + as double); + + @override + double convertRadiusToSigma(double? radius) => + (super.noSuchMethod( + Invocation.method(#convertRadiusToSigma, [radius]), + returnValue: 0.0, + ) + as double); +} diff --git a/test/chart/candlestick_chart/candlestick_chart_renderer_test.dart b/test/chart/candlestick_chart/candlestick_chart_renderer_test.dart new file mode 100644 index 0000000..f39ee03 --- /dev/null +++ b/test/chart/candlestick_chart/candlestick_chart_renderer_test.dart @@ -0,0 +1,150 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart'; +import 'package:fl_chart/src/chart/candlestick_chart/candlestick_chart_painter.dart'; +import 'package:fl_chart/src/chart/candlestick_chart/candlestick_chart_renderer.dart'; +import 'package:fl_chart/src/utils/canvas_wrapper.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import '../data_pool.dart'; +import 'candlestick_chart_renderer_test.mocks.dart'; + +@GenerateMocks([Canvas, PaintingContext, BuildContext, CandlestickChartPainter]) +void main() { + group('CandlestickChartRenderer', () { + final data = CandlestickChartData( + candlestickSpots: [candlestickSpot1, candlestickSpot2], + ); + + final targetData = CandlestickChartData( + candlestickSpots: [candlestickSpot3], + ); + + const textScaler = TextScaler.linear(4); + + final mockBuildContext = MockBuildContext(); + final renderCandlestickChart = RenderCandlestickChart( + mockBuildContext, + data, + targetData, + textScaler, + null, + canBeScaled: false, + ); + + final mockPainter = MockCandlestickChartPainter(); + final mockPaintingContext = MockPaintingContext(); + final mockCanvas = MockCanvas(); + const mockSize = Size(44, 44); + when(mockPaintingContext.canvas).thenAnswer((realInvocation) => mockCanvas); + renderCandlestickChart + ..mockTestSize = mockSize + ..painter = mockPainter; + + test('test 1 correct data set', () { + expect(renderCandlestickChart.data == data, true); + expect(renderCandlestickChart.data == targetData, false); + expect(renderCandlestickChart.targetData == targetData, true); + expect(renderCandlestickChart.textScaler == textScaler, true); + expect(renderCandlestickChart.paintHolder.data == data, true); + expect(renderCandlestickChart.paintHolder.targetData == targetData, true); + expect(renderCandlestickChart.paintHolder.textScaler == textScaler, true); + }); + + test('test 2 check paint function', () { + renderCandlestickChart.paint(mockPaintingContext, const Offset(10, 10)); + verify(mockCanvas.save()).called(1); + verify(mockCanvas.translate(10, 10)).called(1); + final result = verify(mockPainter.paint(any, captureAny, captureAny)); + expect(result.callCount, 1); + + final canvasWrapper = result.captured[0] as CanvasWrapper; + expect(canvasWrapper.size, const Size(44, 44)); + expect(canvasWrapper.canvas, mockCanvas); + + final paintHolder = result.captured[1] as PaintHolder; + expect(paintHolder.data, data); + expect(paintHolder.targetData, targetData); + expect(paintHolder.textScaler, textScaler); + + verify(mockCanvas.restore()).called(1); + }); + + test('test 3 check getResponseAtLocation function', () { + final results = >[]; + when(mockPainter.handleTouch(captureAny, captureAny, captureAny)) + .thenAnswer((inv) { + results.add({ + 'local_position': inv.positionalArguments[0] as Offset, + 'size': inv.positionalArguments[1] as Size, + 'paint_holder': inv.positionalArguments[2] as PaintHolder, + }); + return candlestickTouchedSpot1; + }); + + when(mockPainter.getChartCoordinateFromPixel(any, any, any)) + .thenAnswer((_) => const Offset(10, 10)); + + final touchResponse = + renderCandlestickChart.getResponseAtLocation(MockData.offset1); + expect(touchResponse.touchedSpot, candlestickTouchedSpot1); + expect(touchResponse.touchChartCoordinate, const Offset(10, 10)); + expect(results[0]['local_position'] as Offset, MockData.offset1); + expect(results[0]['size'] as Size, mockSize); + final paintHolder = results[0]['paint_holder'] as PaintHolder; + expect(paintHolder.data, data); + expect(paintHolder.targetData, targetData); + expect(paintHolder.textScaler, textScaler); + }); + + test('test 4 check setters', () { + renderCandlestickChart + ..data = targetData + ..targetData = data + ..textScaler = const TextScaler.linear(22); + + expect(renderCandlestickChart.data, targetData); + expect(renderCandlestickChart.targetData, data); + expect(renderCandlestickChart.textScaler, const TextScaler.linear(22)); + }); + + test('passes chart virtual rect to paint holder', () { + final rect1 = Offset.zero & const Size(100, 100); + final renderCandlestickChart = RenderCandlestickChart( + mockBuildContext, + data, + targetData, + textScaler, + null, + canBeScaled: false, + ); + + expect(renderCandlestickChart.chartVirtualRect, isNull); + expect(renderCandlestickChart.paintHolder.chartVirtualRect, isNull); + + renderCandlestickChart.chartVirtualRect = rect1; + + expect(renderCandlestickChart.chartVirtualRect, rect1); + expect(renderCandlestickChart.paintHolder.chartVirtualRect, rect1); + }); + + test('uses canBeScaled', () { + final renderCandlestickChart = RenderCandlestickChart( + mockBuildContext, + data, + targetData, + textScaler, + null, + canBeScaled: false, + ); + + expect(renderCandlestickChart.canBeScaled, false); + + renderCandlestickChart.canBeScaled = true; + + expect(renderCandlestickChart.canBeScaled, true); + }); + }); +} diff --git a/test/chart/candlestick_chart/candlestick_chart_renderer_test.mocks.dart b/test/chart/candlestick_chart/candlestick_chart_renderer_test.mocks.dart new file mode 100644 index 0000000..1bfe46d --- /dev/null +++ b/test/chart/candlestick_chart/candlestick_chart_renderer_test.mocks.dart @@ -0,0 +1,1060 @@ +// Mocks generated by Mockito 5.4.5 from annotations +// in fl_chart/test/chart/candlestick_chart/candlestick_chart_renderer_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:typed_data' as _i7; +import 'dart:ui' as _i2; + +import 'package:fl_chart/fl_chart.dart' as _i13; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart' + as _i12; +import 'package:fl_chart/src/chart/candlestick_chart/candlestick_chart_painter.dart' + as _i10; +import 'package:fl_chart/src/utils/canvas_wrapper.dart' as _i11; +import 'package:flutter/foundation.dart' as _i5; +import 'package:flutter/gestures.dart' as _i8; +import 'package:flutter/material.dart' as _i6; +import 'package:flutter/rendering.dart' as _i3; +import 'package:flutter/src/rendering/layer.dart' as _i4; +import 'package:flutter/src/widgets/notification_listener.dart' as _i9; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeRect_0 extends _i1.SmartFake implements _i2.Rect { + _FakeRect_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeCanvas_1 extends _i1.SmartFake implements _i2.Canvas { + _FakeCanvas_1(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakePaintingContext_2 extends _i1.SmartFake + implements _i3.PaintingContext { + _FakePaintingContext_2(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeColorFilterLayer_3 extends _i1.SmartFake + implements _i4.ColorFilterLayer { + _FakeColorFilterLayer_3(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeOpacityLayer_4 extends _i1.SmartFake implements _i4.OpacityLayer { + _FakeOpacityLayer_4(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeWidget_5 extends _i1.SmartFake implements _i6.Widget { + _FakeWidget_5(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeInheritedWidget_6 extends _i1.SmartFake + implements _i6.InheritedWidget { + _FakeInheritedWidget_6(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeDiagnosticsNode_7 extends _i1.SmartFake + implements _i5.DiagnosticsNode { + _FakeDiagnosticsNode_7(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({ + _i5.TextTreeConfiguration? parentConfiguration, + _i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info, + }) => super.toString(); +} + +class _FakeOffset_8 extends _i1.SmartFake implements _i2.Offset { + _FakeOffset_8(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +/// A class which mocks [Canvas]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCanvas extends _i1.Mock implements _i2.Canvas { + MockCanvas() { + _i1.throwOnMissingStub(this); + } + + @override + void save() => super.noSuchMethod( + Invocation.method(#save, []), + returnValueForMissingStub: null, + ); + + @override + void saveLayer(_i2.Rect? bounds, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#saveLayer, [bounds, paint]), + returnValueForMissingStub: null, + ); + + @override + void restore() => super.noSuchMethod( + Invocation.method(#restore, []), + returnValueForMissingStub: null, + ); + + @override + void restoreToCount(int? count) => super.noSuchMethod( + Invocation.method(#restoreToCount, [count]), + returnValueForMissingStub: null, + ); + + @override + int getSaveCount() => + (super.noSuchMethod(Invocation.method(#getSaveCount, []), returnValue: 0) + as int); + + @override + void translate(double? dx, double? dy) => super.noSuchMethod( + Invocation.method(#translate, [dx, dy]), + returnValueForMissingStub: null, + ); + + @override + void scale(double? sx, [double? sy]) => super.noSuchMethod( + Invocation.method(#scale, [sx, sy]), + returnValueForMissingStub: null, + ); + + @override + void rotate(double? radians) => super.noSuchMethod( + Invocation.method(#rotate, [radians]), + returnValueForMissingStub: null, + ); + + @override + void skew(double? sx, double? sy) => super.noSuchMethod( + Invocation.method(#skew, [sx, sy]), + returnValueForMissingStub: null, + ); + + @override + void transform(_i7.Float64List? matrix4) => super.noSuchMethod( + Invocation.method(#transform, [matrix4]), + returnValueForMissingStub: null, + ); + + @override + _i7.Float64List getTransform() => + (super.noSuchMethod( + Invocation.method(#getTransform, []), + returnValue: _i7.Float64List(0), + ) + as _i7.Float64List); + + @override + void clipRect( + _i2.Rect? rect, { + _i2.ClipOp? clipOp = _i2.ClipOp.intersect, + bool? doAntiAlias = true, + }) => super.noSuchMethod( + Invocation.method( + #clipRect, + [rect], + {#clipOp: clipOp, #doAntiAlias: doAntiAlias}, + ), + returnValueForMissingStub: null, + ); + + @override + void clipRRect(_i2.RRect? rrect, {bool? doAntiAlias = true}) => + super.noSuchMethod( + Invocation.method(#clipRRect, [rrect], {#doAntiAlias: doAntiAlias}), + returnValueForMissingStub: null, + ); + + @override + void clipPath(_i2.Path? path, {bool? doAntiAlias = true}) => + super.noSuchMethod( + Invocation.method(#clipPath, [path], {#doAntiAlias: doAntiAlias}), + returnValueForMissingStub: null, + ); + + @override + _i2.Rect getLocalClipBounds() => + (super.noSuchMethod( + Invocation.method(#getLocalClipBounds, []), + returnValue: _FakeRect_0( + this, + Invocation.method(#getLocalClipBounds, []), + ), + ) + as _i2.Rect); + + @override + _i2.Rect getDestinationClipBounds() => + (super.noSuchMethod( + Invocation.method(#getDestinationClipBounds, []), + returnValue: _FakeRect_0( + this, + Invocation.method(#getDestinationClipBounds, []), + ), + ) + as _i2.Rect); + + @override + void drawColor(_i2.Color? color, _i2.BlendMode? blendMode) => + super.noSuchMethod( + Invocation.method(#drawColor, [color, blendMode]), + returnValueForMissingStub: null, + ); + + @override + void drawLine(_i2.Offset? p1, _i2.Offset? p2, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawLine, [p1, p2, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawPaint(_i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawPaint, [paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRect(_i2.Rect? rect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRect, [rect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRRect(_i2.RRect? rrect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRRect, [rrect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawDRRect(_i2.RRect? outer, _i2.RRect? inner, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawDRRect, [outer, inner, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawOval(_i2.Rect? rect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawOval, [rect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawCircle(_i2.Offset? c, double? radius, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawCircle, [c, radius, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawArc( + _i2.Rect? rect, + double? startAngle, + double? sweepAngle, + bool? useCenter, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawArc, [ + rect, + startAngle, + sweepAngle, + useCenter, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawPath(_i2.Path? path, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawPath, [path, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImage(_i2.Image? image, _i2.Offset? offset, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawImage, [image, offset, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImageRect( + _i2.Image? image, + _i2.Rect? src, + _i2.Rect? dst, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawImageRect, [image, src, dst, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImageNine( + _i2.Image? image, + _i2.Rect? center, + _i2.Rect? dst, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawImageNine, [image, center, dst, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawPicture(_i2.Picture? picture) => super.noSuchMethod( + Invocation.method(#drawPicture, [picture]), + returnValueForMissingStub: null, + ); + + @override + void drawParagraph(_i2.Paragraph? paragraph, _i2.Offset? offset) => + super.noSuchMethod( + Invocation.method(#drawParagraph, [paragraph, offset]), + returnValueForMissingStub: null, + ); + + @override + void drawPoints( + _i2.PointMode? pointMode, + List<_i2.Offset>? points, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawPoints, [pointMode, points, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRawPoints( + _i2.PointMode? pointMode, + _i7.Float32List? points, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawRawPoints, [pointMode, points, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawVertices( + _i2.Vertices? vertices, + _i2.BlendMode? blendMode, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawVertices, [vertices, blendMode, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawAtlas( + _i2.Image? atlas, + List<_i2.RSTransform>? transforms, + List<_i2.Rect>? rects, + List<_i2.Color>? colors, + _i2.BlendMode? blendMode, + _i2.Rect? cullRect, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawAtlas, [ + atlas, + transforms, + rects, + colors, + blendMode, + cullRect, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawRawAtlas( + _i2.Image? atlas, + _i7.Float32List? rstTransforms, + _i7.Float32List? rects, + _i7.Int32List? colors, + _i2.BlendMode? blendMode, + _i2.Rect? cullRect, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawRawAtlas, [ + atlas, + rstTransforms, + rects, + colors, + blendMode, + cullRect, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawShadow( + _i2.Path? path, + _i2.Color? color, + double? elevation, + bool? transparentOccluder, + ) => super.noSuchMethod( + Invocation.method(#drawShadow, [ + path, + color, + elevation, + transparentOccluder, + ]), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [PaintingContext]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPaintingContext extends _i1.Mock implements _i3.PaintingContext { + MockPaintingContext() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.Rect get estimatedBounds => + (super.noSuchMethod( + Invocation.getter(#estimatedBounds), + returnValue: _FakeRect_0(this, Invocation.getter(#estimatedBounds)), + ) + as _i2.Rect); + + @override + _i2.Canvas get canvas => + (super.noSuchMethod( + Invocation.getter(#canvas), + returnValue: _FakeCanvas_1(this, Invocation.getter(#canvas)), + ) + as _i2.Canvas); + + @override + void paintChild(_i3.RenderObject? child, _i2.Offset? offset) => + super.noSuchMethod( + Invocation.method(#paintChild, [child, offset]), + returnValueForMissingStub: null, + ); + + @override + void appendLayer(_i4.Layer? layer) => super.noSuchMethod( + Invocation.method(#appendLayer, [layer]), + returnValueForMissingStub: null, + ); + + @override + _i2.VoidCallback addCompositionCallback(_i4.CompositionCallback? callback) => + (super.noSuchMethod( + Invocation.method(#addCompositionCallback, [callback]), + returnValue: () {}, + ) + as _i2.VoidCallback); + + @override + void stopRecordingIfNeeded() => super.noSuchMethod( + Invocation.method(#stopRecordingIfNeeded, []), + returnValueForMissingStub: null, + ); + + @override + void setIsComplexHint() => super.noSuchMethod( + Invocation.method(#setIsComplexHint, []), + returnValueForMissingStub: null, + ); + + @override + void setWillChangeHint() => super.noSuchMethod( + Invocation.method(#setWillChangeHint, []), + returnValueForMissingStub: null, + ); + + @override + void addLayer(_i4.Layer? layer) => super.noSuchMethod( + Invocation.method(#addLayer, [layer]), + returnValueForMissingStub: null, + ); + + @override + void pushLayer( + _i4.ContainerLayer? childLayer, + _i3.PaintingContextCallback? painter, + _i2.Offset? offset, { + _i2.Rect? childPaintBounds, + }) => super.noSuchMethod( + Invocation.method( + #pushLayer, + [childLayer, painter, offset], + {#childPaintBounds: childPaintBounds}, + ), + returnValueForMissingStub: null, + ); + + @override + _i3.PaintingContext createChildContext( + _i4.ContainerLayer? childLayer, + _i2.Rect? bounds, + ) => + (super.noSuchMethod( + Invocation.method(#createChildContext, [childLayer, bounds]), + returnValue: _FakePaintingContext_2( + this, + Invocation.method(#createChildContext, [childLayer, bounds]), + ), + ) + as _i3.PaintingContext); + + @override + _i4.ClipRectLayer? pushClipRect( + bool? needsCompositing, + _i2.Offset? offset, + _i2.Rect? clipRect, + _i3.PaintingContextCallback? painter, { + _i2.Clip? clipBehavior = _i2.Clip.hardEdge, + _i4.ClipRectLayer? oldLayer, + }) => + (super.noSuchMethod( + Invocation.method( + #pushClipRect, + [needsCompositing, offset, clipRect, painter], + {#clipBehavior: clipBehavior, #oldLayer: oldLayer}, + ), + ) + as _i4.ClipRectLayer?); + + @override + _i4.ClipRRectLayer? pushClipRRect( + bool? needsCompositing, + _i2.Offset? offset, + _i2.Rect? bounds, + _i2.RRect? clipRRect, + _i3.PaintingContextCallback? painter, { + _i2.Clip? clipBehavior = _i2.Clip.antiAlias, + _i4.ClipRRectLayer? oldLayer, + }) => + (super.noSuchMethod( + Invocation.method( + #pushClipRRect, + [needsCompositing, offset, bounds, clipRRect, painter], + {#clipBehavior: clipBehavior, #oldLayer: oldLayer}, + ), + ) + as _i4.ClipRRectLayer?); + + @override + _i4.ClipPathLayer? pushClipPath( + bool? needsCompositing, + _i2.Offset? offset, + _i2.Rect? bounds, + _i2.Path? clipPath, + _i3.PaintingContextCallback? painter, { + _i2.Clip? clipBehavior = _i2.Clip.antiAlias, + _i4.ClipPathLayer? oldLayer, + }) => + (super.noSuchMethod( + Invocation.method( + #pushClipPath, + [needsCompositing, offset, bounds, clipPath, painter], + {#clipBehavior: clipBehavior, #oldLayer: oldLayer}, + ), + ) + as _i4.ClipPathLayer?); + + @override + _i4.ColorFilterLayer pushColorFilter( + _i2.Offset? offset, + _i2.ColorFilter? colorFilter, + _i3.PaintingContextCallback? painter, { + _i4.ColorFilterLayer? oldLayer, + }) => + (super.noSuchMethod( + Invocation.method( + #pushColorFilter, + [offset, colorFilter, painter], + {#oldLayer: oldLayer}, + ), + returnValue: _FakeColorFilterLayer_3( + this, + Invocation.method( + #pushColorFilter, + [offset, colorFilter, painter], + {#oldLayer: oldLayer}, + ), + ), + ) + as _i4.ColorFilterLayer); + + @override + _i4.TransformLayer? pushTransform( + bool? needsCompositing, + _i2.Offset? offset, + _i8.Matrix4? transform, + _i3.PaintingContextCallback? painter, { + _i4.TransformLayer? oldLayer, + }) => + (super.noSuchMethod( + Invocation.method( + #pushTransform, + [needsCompositing, offset, transform, painter], + {#oldLayer: oldLayer}, + ), + ) + as _i4.TransformLayer?); + + @override + _i4.OpacityLayer pushOpacity( + _i2.Offset? offset, + int? alpha, + _i3.PaintingContextCallback? painter, { + _i4.OpacityLayer? oldLayer, + }) => + (super.noSuchMethod( + Invocation.method( + #pushOpacity, + [offset, alpha, painter], + {#oldLayer: oldLayer}, + ), + returnValue: _FakeOpacityLayer_4( + this, + Invocation.method( + #pushOpacity, + [offset, alpha, painter], + {#oldLayer: oldLayer}, + ), + ), + ) + as _i4.OpacityLayer); + + @override + void clipPathAndPaint( + _i2.Path? path, + _i2.Clip? clipBehavior, + _i2.Rect? bounds, + _i2.VoidCallback? painter, + ) => super.noSuchMethod( + Invocation.method(#clipPathAndPaint, [path, clipBehavior, bounds, painter]), + returnValueForMissingStub: null, + ); + + @override + void clipRRectAndPaint( + _i2.RRect? rrect, + _i2.Clip? clipBehavior, + _i2.Rect? bounds, + _i2.VoidCallback? painter, + ) => super.noSuchMethod( + Invocation.method(#clipRRectAndPaint, [ + rrect, + clipBehavior, + bounds, + painter, + ]), + returnValueForMissingStub: null, + ); + + @override + void clipRectAndPaint( + _i2.Rect? rect, + _i2.Clip? clipBehavior, + _i2.Rect? bounds, + _i2.VoidCallback? painter, + ) => super.noSuchMethod( + Invocation.method(#clipRectAndPaint, [rect, clipBehavior, bounds, painter]), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [BuildContext]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockBuildContext extends _i1.Mock implements _i6.BuildContext { + MockBuildContext() { + _i1.throwOnMissingStub(this); + } + + @override + _i6.Widget get widget => + (super.noSuchMethod( + Invocation.getter(#widget), + returnValue: _FakeWidget_5(this, Invocation.getter(#widget)), + ) + as _i6.Widget); + + @override + bool get mounted => + (super.noSuchMethod(Invocation.getter(#mounted), returnValue: false) + as bool); + + @override + bool get debugDoingBuild => + (super.noSuchMethod( + Invocation.getter(#debugDoingBuild), + returnValue: false, + ) + as bool); + + @override + _i6.InheritedWidget dependOnInheritedElement( + _i6.InheritedElement? ancestor, { + Object? aspect, + }) => + (super.noSuchMethod( + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + returnValue: _FakeInheritedWidget_6( + this, + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + ), + ) + as _i6.InheritedWidget); + + @override + void visitAncestorElements(_i6.ConditionalElementVisitor? visitor) => + super.noSuchMethod( + Invocation.method(#visitAncestorElements, [visitor]), + returnValueForMissingStub: null, + ); + + @override + void visitChildElements(_i6.ElementVisitor? visitor) => super.noSuchMethod( + Invocation.method(#visitChildElements, [visitor]), + returnValueForMissingStub: null, + ); + + @override + void dispatchNotification(_i9.Notification? notification) => + super.noSuchMethod( + Invocation.method(#dispatchNotification, [notification]), + returnValueForMissingStub: null, + ); + + @override + _i5.DiagnosticsNode describeElement( + String? name, { + _i5.DiagnosticsTreeStyle? style = _i5.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method(#describeElement, [name], {#style: style}), + returnValue: _FakeDiagnosticsNode_7( + this, + Invocation.method(#describeElement, [name], {#style: style}), + ), + ) + as _i5.DiagnosticsNode); + + @override + _i5.DiagnosticsNode describeWidget( + String? name, { + _i5.DiagnosticsTreeStyle? style = _i5.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method(#describeWidget, [name], {#style: style}), + returnValue: _FakeDiagnosticsNode_7( + this, + Invocation.method(#describeWidget, [name], {#style: style}), + ), + ) + as _i5.DiagnosticsNode); + + @override + List<_i5.DiagnosticsNode> describeMissingAncestor({ + required Type? expectedAncestorType, + }) => + (super.noSuchMethod( + Invocation.method(#describeMissingAncestor, [], { + #expectedAncestorType: expectedAncestorType, + }), + returnValue: <_i5.DiagnosticsNode>[], + ) + as List<_i5.DiagnosticsNode>); + + @override + _i5.DiagnosticsNode describeOwnershipChain(String? name) => + (super.noSuchMethod( + Invocation.method(#describeOwnershipChain, [name]), + returnValue: _FakeDiagnosticsNode_7( + this, + Invocation.method(#describeOwnershipChain, [name]), + ), + ) + as _i5.DiagnosticsNode); +} + +/// A class which mocks [CandlestickChartPainter]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCandlestickChartPainter extends _i1.Mock + implements _i10.CandlestickChartPainter { + MockCandlestickChartPainter() { + _i1.throwOnMissingStub(this); + } + + @override + void paint( + _i6.BuildContext? context, + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.CandlestickChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#paint, [context, canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawCandlesticks( + _i6.BuildContext? context, + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.CandlestickChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawCandlesticks, [context, canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawTouchTooltips( + _i6.BuildContext? context, + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.CandlestickChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawTouchTooltips, [context, canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawTouchTooltip( + _i6.BuildContext? context, + _i11.CanvasWrapper? canvasWrapper, + _i13.CandlestickTouchTooltipData? tooltipData, + _i13.CandlestickSpot? showOnSpot, + int? spotIndex, + _i12.PaintHolder<_i13.CandlestickChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawTouchTooltip, [ + context, + canvasWrapper, + tooltipData, + showOnSpot, + spotIndex, + holder, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawAxisSpotIndicator( + _i6.BuildContext? context, + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.CandlestickChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawAxisSpotIndicator, [context, canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + _i13.CandlestickTouchedSpot? handleTouch( + _i2.Offset? localPosition, + _i2.Size? viewSize, + _i12.PaintHolder<_i13.CandlestickChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#handleTouch, [localPosition, viewSize, holder]), + ) + as _i13.CandlestickTouchedSpot?); + + @override + void drawGrid( + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.CandlestickChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawGrid, [canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawBackground( + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.CandlestickChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawBackground, [canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawRangeAnnotation( + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.CandlestickChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawRangeAnnotation, [canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawExtraLines( + _i6.BuildContext? context, + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.CandlestickChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawExtraLines, [context, canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawHorizontalLines( + _i6.BuildContext? context, + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.CandlestickChartData>? holder, + _i2.Size? viewSize, + ) => super.noSuchMethod( + Invocation.method(#drawHorizontalLines, [ + context, + canvasWrapper, + holder, + viewSize, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawVerticalLines( + _i6.BuildContext? context, + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.CandlestickChartData>? holder, + _i2.Size? viewSize, + ) => super.noSuchMethod( + Invocation.method(#drawVerticalLines, [ + context, + canvasWrapper, + holder, + viewSize, + ]), + returnValueForMissingStub: null, + ); + + @override + double getPixelX( + double? spotX, + _i2.Size? viewSize, + _i12.PaintHolder<_i13.CandlestickChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#getPixelX, [spotX, viewSize, holder]), + returnValue: 0.0, + ) + as double); + + @override + double getPixelY( + double? spotY, + _i2.Size? viewSize, + _i12.PaintHolder<_i13.CandlestickChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#getPixelY, [spotY, viewSize, holder]), + returnValue: 0.0, + ) + as double); + + @override + double getXForPixel( + double? pixelX, + _i2.Size? viewSize, + _i12.PaintHolder<_i13.CandlestickChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#getXForPixel, [pixelX, viewSize, holder]), + returnValue: 0.0, + ) + as double); + + @override + double getYForPixel( + double? pixelY, + _i2.Size? viewSize, + _i12.PaintHolder<_i13.CandlestickChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#getYForPixel, [pixelY, viewSize, holder]), + returnValue: 0.0, + ) + as double); + + @override + _i2.Offset getChartCoordinateFromPixel( + _i2.Offset? pixelOffset, + _i2.Size? viewSize, + _i12.PaintHolder<_i13.CandlestickChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#getChartCoordinateFromPixel, [ + pixelOffset, + viewSize, + holder, + ]), + returnValue: _FakeOffset_8( + this, + Invocation.method(#getChartCoordinateFromPixel, [ + pixelOffset, + viewSize, + holder, + ]), + ), + ) + as _i2.Offset); + + @override + double getTooltipLeft( + double? dx, + double? tooltipWidth, + _i13.FLHorizontalAlignment? tooltipHorizontalAlignment, + double? tooltipHorizontalOffset, + ) => + (super.noSuchMethod( + Invocation.method(#getTooltipLeft, [ + dx, + tooltipWidth, + tooltipHorizontalAlignment, + tooltipHorizontalOffset, + ]), + returnValue: 0.0, + ) + as double); +} diff --git a/test/chart/candlestick_chart/candlestick_chart_test.dart b/test/chart/candlestick_chart/candlestick_chart_test.dart new file mode 100644 index 0000000..ed26287 --- /dev/null +++ b/test/chart/candlestick_chart/candlestick_chart_test.dart @@ -0,0 +1,843 @@ +import 'package:fl_chart/src/chart/base/axis_chart/axis_chart_scaffold_widget.dart'; +import 'package:fl_chart/src/chart/base/axis_chart/scale_axis.dart'; +import 'package:fl_chart/src/chart/base/axis_chart/transformation_config.dart'; +import 'package:fl_chart/src/chart/candlestick_chart/candlestick_chart.dart'; +import 'package:fl_chart/src/chart/candlestick_chart/candlestick_chart_data.dart'; +import 'package:fl_chart/src/chart/candlestick_chart/candlestick_chart_renderer.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + Widget createTestWidget({ + required CandlestickChart chart, + }) { + return MaterialApp( + home: chart, + ); + } + + group('CandlestickChart', () { + testWidgets('has correct default values', (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: CandlestickChart( + CandlestickChartData(), + ), + ), + ); + + final candlestickChart = tester.widget( + find.byType(CandlestickChart), + ); + expect( + candlestickChart.transformationConfig, + const FlTransformationConfig(), + ); + }); + + testWidgets('passes interaction parameters to AxisChartScaffoldWidget', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: CandlestickChart( + CandlestickChartData(), + ), + ), + ); + + final axisChartScaffoldWidget = tester.widget( + find.byType(AxisChartScaffoldWidget), + ); + + expect( + axisChartScaffoldWidget.transformationConfig, + const FlTransformationConfig(), + ); + + await tester.pumpAndSettle(); + + final transformationConfig = FlTransformationConfig( + scaleAxis: FlScaleAxis.free, + trackpadScrollCausesScale: true, + maxScale: 10, + minScale: 1.5, + transformationController: TransformationController(), + ); + await tester.pumpWidget( + createTestWidget( + chart: CandlestickChart( + CandlestickChartData(), + transformationConfig: transformationConfig, + ), + ), + ); + + final axisChartScaffoldWidget1 = tester.widget( + find.byType(AxisChartScaffoldWidget), + ); + + expect( + axisChartScaffoldWidget1.transformationConfig, + transformationConfig, + ); + }); + + for (final scaleAxis in FlScaleAxis.scalingEnabledAxis) { + testWidgets('passes canBeScaled true for $scaleAxis', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: CandlestickChart( + CandlestickChartData(), + transformationConfig: FlTransformationConfig( + scaleAxis: scaleAxis, + ), + ), + ), + ); + + final candlestickChartLeaf = tester.widget( + find.byType(CandlestickChartLeaf), + ); + expect(candlestickChartLeaf.canBeScaled, true); + }); + } + + testWidgets('passes canBeScaled false for FlScaleAxis.none', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: CandlestickChart( + CandlestickChartData(), + // This is for test + // ignore: avoid_redundant_argument_values + transformationConfig: const FlTransformationConfig( + // This is for test + // ignore: avoid_redundant_argument_values + scaleAxis: FlScaleAxis.none, + ), + ), + ), + ); + + final candlestickChartLeaf = tester.widget( + find.byType(CandlestickChartLeaf), + ); + expect(candlestickChartLeaf.canBeScaled, false); + }); + + group('touch gesture', () { + testWidgets('does not scale with FlScaleAxis.none', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: CandlestickChart( + CandlestickChartData(), + ), + ), + ); + + final candlestickChartCenterOffset = + tester.getCenter(find.byType(CandlestickChartLeaf)); + final scaleStart1 = candlestickChartCenterOffset; + final scaleStart2 = candlestickChartCenterOffset; + final scaleEnd1 = candlestickChartCenterOffset + const Offset(100, 100); + final scaleEnd2 = candlestickChartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await tester.pump(); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await tester.pump(); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final candlestickChartLeaf = tester.widget( + find.byType(CandlestickChartLeaf), + ); + + expect(candlestickChartLeaf.chartVirtualRect, isNull); + }); + + testWidgets('scales freely with FlScaleAxis.free', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: CandlestickChart( + CandlestickChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.free, + ), + ), + ), + ); + + final candlestickChartCenterOffset = + tester.getCenter(find.byType(CandlestickChartLeaf)); + final scaleStart1 = candlestickChartCenterOffset; + final scaleStart2 = candlestickChartCenterOffset; + final scaleEnd1 = candlestickChartCenterOffset + const Offset(100, 100); + final scaleEnd2 = candlestickChartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await tester.pump(); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await tester.pump(); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final candlestickChartLeaf = tester.widget( + find.byType(CandlestickChartLeaf), + ); + final renderBox = tester.renderObject( + find.byType(CandlestickChartLeaf), + ); + final chartVirtualRect = candlestickChartLeaf.chartVirtualRect!; + + expect(chartVirtualRect.size, greaterThan(renderBox.size)); + expect(chartVirtualRect.left, isNegative); + expect(chartVirtualRect.top, isNegative); + }); + + testWidgets('scales horizontally with FlScaleAxis.horizontal', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: CandlestickChart( + CandlestickChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.horizontal, + ), + ), + ), + ); + + final chartCenterOffset = + tester.getCenter(find.byType(CandlestickChartLeaf)); + final scaleStart1 = chartCenterOffset; + final scaleStart2 = chartCenterOffset; + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await tester.pump(); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await tester.pump(); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final candlestickChartLeaf = tester.widget( + find.byType(CandlestickChartLeaf), + ); + final renderBox = tester.renderObject( + find.byType(CandlestickChartLeaf), + ); + final chartVirtualRect = candlestickChartLeaf.chartVirtualRect!; + + expect(chartVirtualRect.size.height, renderBox.size.height); + expect(chartVirtualRect.size.width, greaterThan(renderBox.size.width)); + expect(chartVirtualRect.left, isNegative); + expect(chartVirtualRect.top, 0); + }); + + testWidgets('scales vertically with FlScaleAxis.vertical', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: CandlestickChart( + CandlestickChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.vertical, + ), + ), + ), + ); + + final chartCenterOffset = + tester.getCenter(find.byType(CandlestickChartLeaf)); + final scaleStart1 = chartCenterOffset; + final scaleStart2 = chartCenterOffset; + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await tester.pump(); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await tester.pump(); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final candlestickChartLeaf = tester.widget( + find.byType(CandlestickChartLeaf), + ); + final renderBox = tester.renderObject( + find.byType(CandlestickChartLeaf), + ); + final chartVirtualRect = candlestickChartLeaf.chartVirtualRect!; + + expect( + chartVirtualRect.size.height, + greaterThan(renderBox.size.height), + ); + expect(chartVirtualRect.size.width, renderBox.size.width); + expect(chartVirtualRect.left, 0); + expect(chartVirtualRect.top, isNegative); + }); + + group('pans', () { + testWidgets('only horizontally with FlScaleAxis.horizontal', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: CandlestickChart( + CandlestickChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.horizontal, + ), + ), + ), + ); + + final chartCenterOffset = + tester.getCenter(find.byType(CandlestickChartLeaf)); + final scaleStart1 = chartCenterOffset; + final scaleStart2 = chartCenterOffset; + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final candlestickChartLeafBeforePan = + tester.widget( + find.byType(CandlestickChartLeaf), + ); + final chartVirtualRectBeforePan = + candlestickChartLeafBeforePan.chartVirtualRect!; + expect(chartVirtualRectBeforePan.top, 0); + + const panOffset = Offset(100, 100); + await tester.dragFrom(chartCenterOffset, panOffset); + await tester.pumpAndSettle(); + + final candlestickChartLeafAfterPan = + tester.widget( + find.byType(CandlestickChartLeaf), + ); + final chartVirtualRectAfterPan = + candlestickChartLeafAfterPan.chartVirtualRect!; + + expect(chartVirtualRectBeforePan.size, chartVirtualRectAfterPan.size); + expect( + chartVirtualRectAfterPan.left, + greaterThan(chartVirtualRectBeforePan.left), + ); + expect(chartVirtualRectAfterPan.top, 0); + }); + + testWidgets('only vertically with FlScaleAxis.vertical', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: CandlestickChart( + CandlestickChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.vertical, + ), + ), + ), + ); + + final chartCenterOffset = + tester.getCenter(find.byType(CandlestickChartLeaf)); + final scaleStart1 = chartCenterOffset; + final scaleStart2 = chartCenterOffset; + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final candlestickChartLeafBeforePan = + tester.widget( + find.byType(CandlestickChartLeaf), + ); + final chartVirtualRectBeforePan = + candlestickChartLeafBeforePan.chartVirtualRect!; + expect(chartVirtualRectBeforePan.left, 0); + + const panOffset = Offset(100, 100); + await tester.dragFrom(chartCenterOffset, panOffset); + await tester.pumpAndSettle(); + + final candlestickChartLeafAfterPan = + tester.widget( + find.byType(CandlestickChartLeaf), + ); + final chartVirtualRectAfterPan = + candlestickChartLeafAfterPan.chartVirtualRect!; + + expect(chartVirtualRectAfterPan.left, 0); + expect( + chartVirtualRectAfterPan.top, + greaterThan(chartVirtualRectBeforePan.top), + ); + }); + + testWidgets('freely with FlScaleAxis.free', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: CandlestickChart( + CandlestickChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.free, + ), + ), + ), + ); + + final chartCenterOffset = + tester.getCenter(find.byType(CandlestickChartLeaf)); + final scaleStart1 = chartCenterOffset; + final scaleStart2 = chartCenterOffset; + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final candlestickChartLeafBeforePan = + tester.widget( + find.byType(CandlestickChartLeaf), + ); + final chartVirtualRectBeforePan = + candlestickChartLeafBeforePan.chartVirtualRect!; + expect(chartVirtualRectBeforePan.top, isNegative); + expect(chartVirtualRectBeforePan.left, isNegative); + + const panOffset = Offset(100, 100); + await tester.dragFrom(chartCenterOffset, panOffset); + await tester.pumpAndSettle(); + + final candlestickChartLeafAfterPan = + tester.widget( + find.byType(CandlestickChartLeaf), + ); + final chartVirtualRectAfterPan = + candlestickChartLeafAfterPan.chartVirtualRect!; + + expect( + chartVirtualRectAfterPan.left, + greaterThan(chartVirtualRectBeforePan.left), + ); + expect( + chartVirtualRectAfterPan.top, + greaterThan(chartVirtualRectBeforePan.top), + ); + }); + }); + }); + + group('trackpad scroll', () { + group('pans', () { + testWidgets('only horizontally with FlScaleAxis.horizontal', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: CandlestickChart( + CandlestickChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.horizontal, + ), + ), + ), + ); + + final chartCenterOffset = + tester.getCenter(find.byType(CandlestickChartLeaf)); + final scaleStart1 = chartCenterOffset; + final scaleStart2 = chartCenterOffset; + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await tester.pump(); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await tester.pump(); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final candlestickChartLeafBeforePan = + tester.widget( + find.byType(CandlestickChartLeaf), + ); + + final chartVirtualRectBeforePan = + candlestickChartLeafBeforePan.chartVirtualRect!; + expect(chartVirtualRectBeforePan.top, 0); + + final pointer = TestPointer(1, PointerDeviceKind.trackpad); + const leftAndUp = Offset(-100, -100); + await tester.sendEventToBinding(pointer.hover(chartCenterOffset)); + await tester.pump(); + await tester.sendEventToBinding(pointer.scroll(leftAndUp)); + await tester.pump(); + + final candlestickChartLeafAfterPan = + tester.widget( + find.byType(CandlestickChartLeaf), + ); + final chartVirtualRectAfterPan = + candlestickChartLeafAfterPan.chartVirtualRect!; + + expect( + chartVirtualRectAfterPan.left, + greaterThan(chartVirtualRectBeforePan.left), + ); + expect(chartVirtualRectAfterPan.top, 0); + }); + + testWidgets('vertically with FlScaleAxis.vertical', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: CandlestickChart( + CandlestickChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.vertical, + ), + ), + ), + ); + + final chartCenterOffset = + tester.getCenter(find.byType(CandlestickChartLeaf)); + final scaleStart1 = chartCenterOffset; + final scaleStart2 = chartCenterOffset; + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await tester.pump(); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await tester.pump(); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final candlestickChartLeafBeforePan = + tester.widget( + find.byType(CandlestickChartLeaf), + ); + + final chartVirtualRectBeforePan = + candlestickChartLeafBeforePan.chartVirtualRect!; + expect(chartVirtualRectBeforePan.left, 0); + + final pointer = TestPointer(1, PointerDeviceKind.trackpad); + const leftAndUp = Offset(-100, -100); + await tester.sendEventToBinding(pointer.hover(chartCenterOffset)); + await tester.pump(); + await tester.sendEventToBinding(pointer.scroll(leftAndUp)); + await tester.pump(); + + final candlestickChartLeafAfterPan = + tester.widget( + find.byType(CandlestickChartLeaf), + ); + final chartVirtualRectAfterPan = + candlestickChartLeafAfterPan.chartVirtualRect!; + + expect(chartVirtualRectAfterPan.left, 0); + expect( + chartVirtualRectAfterPan.top, + greaterThan(chartVirtualRectBeforePan.top), + ); + }); + + testWidgets('freely with FlScaleAxis.free', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: CandlestickChart( + CandlestickChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.free, + ), + ), + ), + ); + + final chartCenterOffset = + tester.getCenter(find.byType(CandlestickChartLeaf)); + final scaleStart1 = chartCenterOffset; + final scaleStart2 = chartCenterOffset; + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await tester.pump(); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await tester.pump(); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final candlestickChartLeafBeforePan = + tester.widget( + find.byType(CandlestickChartLeaf), + ); + + final chartVirtualRectBeforePan = + candlestickChartLeafBeforePan.chartVirtualRect!; + + final pointer = TestPointer(1, PointerDeviceKind.trackpad); + const leftAndUp = Offset(-100, -100); + await tester.sendEventToBinding(pointer.hover(chartCenterOffset)); + await tester.pump(); + await tester.sendEventToBinding(pointer.scroll(leftAndUp)); + await tester.pump(); + + final candlestickChartLeafAfterPan = + tester.widget( + find.byType(CandlestickChartLeaf), + ); + final chartVirtualRectAfterPan = + candlestickChartLeafAfterPan.chartVirtualRect!; + + expect( + chartVirtualRectAfterPan.left, + greaterThan(chartVirtualRectBeforePan.left), + ); + expect( + chartVirtualRectAfterPan.top, + greaterThan(chartVirtualRectBeforePan.top), + ); + }); + }); + + testWidgets( + 'does not scale with FlScaleAxis.none when ' + 'trackpadScrollCausesScale is true', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: CandlestickChart( + CandlestickChartData(), + transformationConfig: const FlTransformationConfig( + // This is for test + // ignore: avoid_redundant_argument_values + scaleAxis: FlScaleAxis.none, + trackpadScrollCausesScale: true, + ), + ), + ), + ); + + final pointer = TestPointer(1, PointerDeviceKind.trackpad); + final chartCenterOffset = + tester.getCenter(find.byType(CandlestickChartLeaf)); + const scrollAmount = Offset(0, -100); + + await tester.sendEventToBinding(pointer.hover(chartCenterOffset)); + await tester.pump(); + await tester.sendEventToBinding(pointer.scroll(scrollAmount)); + await tester.pump(); + + final candlestickChartLeaf = tester.widget( + find.byType(CandlestickChartLeaf), + ); + expect(candlestickChartLeaf.chartVirtualRect, null); + }, + ); + + for (final scaleAxis in FlScaleAxis.scalingEnabledAxis) { + testWidgets( + 'does not scale when trackpadScrollCausesScale is false ' + 'for $scaleAxis', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: CandlestickChart( + CandlestickChartData(), + transformationConfig: FlTransformationConfig( + scaleAxis: scaleAxis, + // This is for test + // ignore: avoid_redundant_argument_values + trackpadScrollCausesScale: false, + ), + ), + ), + ); + + final pointer = TestPointer(1, PointerDeviceKind.trackpad); + final chartCenterOffset = tester.getCenter( + find.byType(CandlestickChartLeaf), + ); + const scrollAmount = Offset(0, -100); + + await tester.sendEventToBinding(pointer.hover(chartCenterOffset)); + await tester.pump(); + await tester.sendEventToBinding(pointer.scroll(scrollAmount)); + await tester.pump(); + + final candlestickChartLeaf = tester.widget( + find.byType(CandlestickChartLeaf), + ); + expect(candlestickChartLeaf.chartVirtualRect, null); + }, + ); + } + + testWidgets('scales horizontally with FlScaleAxis.horizontal', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: CandlestickChart( + CandlestickChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.horizontal, + trackpadScrollCausesScale: true, + ), + ), + ), + ); + + final pointer = TestPointer(1, PointerDeviceKind.trackpad); + final chartCenterOffset = + tester.getCenter(find.byType(CandlestickChartLeaf)); + const scrollAmount = Offset(0, -100); + + await tester.sendEventToBinding(pointer.hover(chartCenterOffset)); + await tester.pump(); + await tester.sendEventToBinding(pointer.scroll(scrollAmount)); + await tester.pump(); + + final candlestickChartLeaf = tester.widget( + find.byType(CandlestickChartLeaf), + ); + final renderBox = tester.renderObject( + find.byType(CandlestickChartLeaf), + ); + final chartVirtualRect = candlestickChartLeaf.chartVirtualRect!; + + expect(chartVirtualRect.size.height, renderBox.size.height); + expect(chartVirtualRect.size.width, greaterThan(renderBox.size.width)); + expect(chartVirtualRect.left, isNegative); + expect(chartVirtualRect.top, 0); + }); + + testWidgets('scales vertically with FlScaleAxis.vertical', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: CandlestickChart( + CandlestickChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.vertical, + trackpadScrollCausesScale: true, + ), + ), + ), + ); + + final pointer = TestPointer(1, PointerDeviceKind.trackpad); + final chartCenterOffset = + tester.getCenter(find.byType(CandlestickChartLeaf)); + const scrollAmount = Offset(0, -100); + + await tester.sendEventToBinding(pointer.hover(chartCenterOffset)); + await tester.pump(); + await tester.sendEventToBinding(pointer.scroll(scrollAmount)); + await tester.pump(); + + final candlestickChartLeaf = tester.widget( + find.byType(CandlestickChartLeaf), + ); + final renderBox = tester.renderObject( + find.byType(CandlestickChartLeaf), + ); + final chartVirtualRect = candlestickChartLeaf.chartVirtualRect!; + + expect( + chartVirtualRect.size.height, + greaterThan(renderBox.size.height), + ); + expect(chartVirtualRect.size.width, renderBox.size.width); + expect(chartVirtualRect.left, 0); + expect(chartVirtualRect.top, isNegative); + }); + + testWidgets('scales freely with FlScaleAxis.free', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: CandlestickChart( + CandlestickChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.free, + trackpadScrollCausesScale: true, + ), + ), + ), + ); + + final pointer = TestPointer(1, PointerDeviceKind.trackpad); + final chartCenterOffset = + tester.getCenter(find.byType(CandlestickChartLeaf)); + const scrollAmount = Offset(0, -100); + + await tester.sendEventToBinding(pointer.hover(chartCenterOffset)); + await tester.pump(); + await tester.sendEventToBinding(pointer.scroll(scrollAmount)); + await tester.pump(); + + final candlestickChartLeaf = tester + .widget(find.byType(CandlestickChartLeaf)); + final renderBox = tester.renderObject( + find.byType(CandlestickChartLeaf), + ); + final chartVirtualRect = candlestickChartLeaf.chartVirtualRect!; + + expect(chartVirtualRect.size, greaterThan(renderBox.size)); + expect(chartVirtualRect.left, isNegative); + expect(chartVirtualRect.top, isNegative); + }); + }); + }); +} diff --git a/test/chart/data_pool.dart b/test/chart/data_pool.dart new file mode 100644 index 0000000..a0ba7d2 --- /dev/null +++ b/test/chart/data_pool.dart @@ -0,0 +1,3463 @@ +import 'dart:ui'; + +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/base/line.dart'; +import 'package:flutter/material.dart'; + +class MockData { + static const color0 = Color(0x00000000); + static const color1 = Color(0x11111111); + static const color2 = Color(0x22222222); + static const color3 = Color(0x33333333); + static const color4 = Color(0x44444444); + static const color5 = Color(0x55555555); + static const color6 = Color(0x66666666); + + static final path1 = Path() + ..moveTo(10, 10) + ..lineTo(20, 20) + ..arcTo( + Rect.fromCenter(center: Offset.zero, width: 44, height: 22), + 2, + 3, + false, + ); + + static final path1Duplicate = Path() + ..moveTo(10, 10) + ..lineTo(20, 20) + ..arcTo( + Rect.fromCenter(center: Offset.zero, width: 44, height: 22), + 2, + 3, + false, + ); + + static final path2 = Path() + ..moveTo(10, 10) + ..lineTo(20, 20) + ..arcTo( + Rect.fromCenter(center: Offset.zero, width: 44, height: 22.01), + 2, + 3, + false, + ); + + static final path3 = Path() + ..moveTo(10, 13) + ..lineTo(20, 20) + ..arcTo( + Rect.fromCenter(center: Offset.zero, width: 44, height: 22), + 2, + 3, + false, + ); + + static final path4 = Path() + ..moveTo(24, 13) + ..lineTo(20, 20) + ..arcTo( + Rect.fromCenter(center: Offset.zero, width: 44, height: 22), + 2, + 3, + false, + ); + + static const borderSide1 = BorderSide(color: color1); + static const borderSide2 = BorderSide(color: color2, width: 2); + static const borderSide3 = BorderSide(color: color3, width: 3); + static const borderSide4 = BorderSide(color: color4, width: 4); + static const borderSide5 = BorderSide(color: color5, width: 5); + static const borderSide6 = BorderSide(color: color6, width: 6); + + static const TextStyle textStyle1 = + TextStyle(color: color1, fontWeight: FontWeight.w100); + static const TextStyle textStyle2 = + TextStyle(color: color2, fontWeight: FontWeight.w200); + static const TextStyle textStyle3 = + TextStyle(color: color3, fontWeight: FontWeight.w300); + static const TextStyle textStyle4 = + TextStyle(color: color4, fontWeight: FontWeight.w400); + + static const Offset offset1 = Offset(1, 1); + static const Offset offset1Duplicate = Offset(1, 1); + static const Offset offset2 = Offset(2, 2); + static const Offset offset3 = Offset(2, 2); + static const Offset offset4 = Offset(4, 4); + static const Offset offset5 = Offset(5, 5); + static const Offset offset6 = Offset(6, 6); + + static const size1 = Size(11, 11); + static const size2 = Size(22, 22); + + static final textPainter1 = TextPainter(); + static final textPainter2 = TextPainter() + ..text = const TextSpan(text: 'test') + ..textDirection = TextDirection.ltr + ..layout(); + + static final rect1 = Rect.fromCenter(center: offset1, width: 11, height: 11); + static final rect2 = Rect.fromCenter(center: offset2, width: 22, height: 22); + + static final rRect1 = RRect.fromLTRBR(1, 1, 1, 1, const Radius.circular(11)); + static final rRect2 = RRect.fromLTRBR(2, 2, 2, 2, const Radius.circular(22)); + + static final paint1 = Paint() + ..color = color1 + ..strokeWidth = 11; + static final paint2 = Paint() + ..color = color2 + ..strokeWidth = 22; + + static Picture? _picture1; + + static Picture picture1() { + if (_picture1 != null) { + return _picture1!; + } + final recorder1 = PictureRecorder(); + Canvas(recorder1).drawLine(offset1, offset2, paint1); + _picture1 = recorder1.endRecording(); + return _picture1!; + } + + static Image? _image1; + + static Image image1() { + if (_image1 != null) { + return _image1!; + } + _image1 = Image.asset('asdf/asdf'); + return _image1!; + } + + static final LineChartBarData lineChartBarData1 = LineChartBarData( + dashArray: [0, 1], + gradient: const LinearGradient( + colors: [Colors.red, Colors.green], + stops: [0, 1], + begin: Alignment.center, + end: Alignment.bottomRight, + ), + spots: [ + flSpot1, + flSpot2, + ], + shadow: shadow1, + aboveBarData: barAreaData1, + belowBarData: barAreaData2, + barWidth: 12, + curveSmoothness: 12, + dotData: flDotData1, + isStrokeCapRound: true, + preventCurveOvershootingThreshold: 1.2, + showingIndicators: [0, 1], + ); + + static final LineChartBarData lineChartBarData2 = LineChartBarData( + dashArray: [0, 1], + gradient: const LinearGradient( + colors: [Colors.red, Colors.green], + stops: [0, 1], + begin: Alignment.center, + end: Alignment.bottomRight, + ), + spots: [ + flSpot1, + flSpot2, + ], + shadow: shadow2, + isStepLineChart: true, + aboveBarData: barAreaData1, + belowBarData: barAreaData2, + barWidth: 12, + curveSmoothness: 12, + dotData: flDotData1, + isStrokeCapRound: true, + preventCurveOvershootingThreshold: 1.2, + showingIndicators: [0, 4], + ); + + static const flSpot0 = FlSpot.zero; + static const flSpot1 = FlSpot(1, 1); + static const flSpot2 = FlSpot(2, 2); + static const flSpot3 = FlSpot(3, 3); + static const flSpot4 = FlSpot(4, 4); + static const flSpot5 = FlSpot(5, 5); + + static final horizontalLine0 = HorizontalLine(y: 0, color: color0); + static final horizontalLine1 = HorizontalLine(y: 1, color: color1); + static final horizontalLine2 = HorizontalLine(y: 2, color: color2); + static final horizontalLine3 = HorizontalLine(y: 3, color: color3); + static final horizontalLine4 = HorizontalLine(y: 4, color: color4); + static final horizontalLine5 = HorizontalLine(y: 5, color: color5); + + static final verticalLine0 = VerticalLine(x: 0, color: color0); + static final verticalLine1 = VerticalLine(x: 1, color: color1); + static final verticalLine2 = VerticalLine(x: 2, color: color2); + static final verticalLine3 = VerticalLine(x: 3, color: color3); + static final verticalLine4 = VerticalLine(x: 4, color: color4); + static final verticalLine5 = VerticalLine(x: 5, color: color5); + + static final horizontalRangeAnnotation0 = + HorizontalRangeAnnotation(y1: 0, y2: 1, color: color0); + + static final horizontalRangeAnnotation1 = + HorizontalRangeAnnotation(y1: 1, y2: 2, color: color1); + + static final horizontalRangeAnnotation2 = + HorizontalRangeAnnotation(y1: 2, y2: 3, color: color2); + + static final horizontalRangeAnnotation3 = + HorizontalRangeAnnotation(y1: 3, y2: 4, color: color3); + + static final horizontalRangeAnnotation4 = + HorizontalRangeAnnotation(y1: 4, y2: 5, color: color4); + + static final verticalRangeAnnotation0 = + VerticalRangeAnnotation(x1: 0, x2: 1, color: color0); + + static final verticalRangeAnnotation1 = + VerticalRangeAnnotation(x1: 1, x2: 2, color: color1); + + static final verticalRangeAnnotation2 = + VerticalRangeAnnotation(x1: 2, x2: 3, color: color2); + + static final verticalRangeAnnotation3 = + VerticalRangeAnnotation(x1: 3, x2: 4, color: color3); + + static final verticalRangeAnnotation4 = + VerticalRangeAnnotation(x1: 4, x2: 5, color: color4); + + static const RadarEntry radarEntry0 = RadarEntry(value: 0); + static const RadarEntry radarEntry1 = RadarEntry(value: 1); + static const RadarEntry radarEntry2 = RadarEntry(value: 2); + static const RadarEntry radarEntry3 = RadarEntry(value: 3); + static const RadarEntry radarEntry4 = RadarEntry(value: 4); + static final RadarDataSet radarDataSet1 = RadarDataSet( + dataEntries: [radarEntry1, radarEntry2, radarEntry3], + ); + static final RadarDataSet radarDataSet2 = RadarDataSet( + dataEntries: [radarEntry3, radarEntry1, radarEntry2], + ); + static final RadarTouchedSpot radarTouchedSpot = RadarTouchedSpot( + radarDataSet1, + 0, + radarEntry1, + 0, + flSpot1, + offset1, + ); + + static final pieChartSection0 = PieChartSectionData( + value: 0, + color: color0, + radius: 0, + ); + + static final pieChartSection1 = PieChartSectionData( + value: 1, + color: color1, + radius: 1, + ); + + static final pieChartSection2 = PieChartSectionData( + value: 2, + color: color2, + radius: 2, + ); + + static final pieChartSection3 = PieChartSectionData( + value: 3, + color: color3, + radius: 3, + ); + + static final pieChartSection4 = PieChartSectionData( + value: 4, + color: color4, + radius: 4, + ); + + static final scatterSpot0 = ScatterSpot( + 0, + 0, + dotPainter: FlDotCirclePainter(color: color0), + ); + static final scatterSpot1 = ScatterSpot( + 1, + 1, + dotPainter: FlDotCirclePainter(color: color1), + ); + static final scatterSpot2 = ScatterSpot( + 2, + 2, + dotPainter: FlDotCirclePainter(color: color2), + ); + static final scatterSpot3 = ScatterSpot( + 3, + 3, + dotPainter: FlDotCirclePainter(color: color3), + ); + static final scatterSpot4 = ScatterSpot( + 4, + 4, + dotPainter: FlDotCirclePainter(color: color4), + ); + + static final scatterTouchedSpot = ScatterTouchedSpot(scatterSpot1, 0); + + static final pieChartSectionData1 = PieChartSectionData(value: 12); + + static final pieChartSectionData2 = PieChartSectionData(value: 22); + + static final pieTouchedSection1 = PieTouchedSection( + pieChartSectionData1, + 0, + 12, + 33, + ); + + static final lineBarSpot1 = TouchLineBarSpot( + lineChartBarData1, + 0, + lineChartBarData1.spots.first, + 0, + ); + static final lineBarSpot2 = TouchLineBarSpot( + MockData.lineChartBarData1, + 1, + MockData.lineChartBarData1.spots.last, + 2, + ); + + static final lineTouchResponse1 = LineTouchResponse( + touchLocation: Offset.zero, + touchChartCoordinate: Offset.zero, + lineBarSpots: [lineBarSpot1, lineBarSpot2], + ); + + static final barChartRodData1 = BarChartRodData(toY: 11); + static final barChartRodData2 = BarChartRodData(toY: 22); + static final barTouchedSpot = BarTouchedSpot( + BarChartGroupData(x: 0, barRods: [barChartRodData1, barChartRodData2]), + 0, + barChartRodData1, + 0, + null, + -1, + flSpot1, + offset1, + ); + + static const gradient1 = LinearGradient( + colors: [ + MockData.color0, + MockData.color1, + ], + ); + + static final barGroupData0 = BarChartGroupData( + x: 0, + barRods: [MockData.barChartRodData1, MockData.barChartRodData2], + ); + + static final barGroupData1 = BarChartGroupData( + x: 1, + barRods: [MockData.barChartRodData1, MockData.barChartRodData2], + ); + + static final barGroupData2 = BarChartGroupData( + x: 2, + barRods: [MockData.barChartRodData1, MockData.barChartRodData2], + ); + + static final barChartData1 = BarChartData( + barGroups: [barGroupData0, barGroupData1, barGroupData2], + ); + + static const sideTitles1 = SideTitles( + reservedSize: 10, + interval: 23, + ); + static const sideTitles1Clone = SideTitles( + reservedSize: 10, + interval: 23, + ); + static const sideTitles2 = SideTitles( + reservedSize: 10, + interval: 12, + ); + static const sideTitles3 = SideTitles( + reservedSize: 10, + getTitlesWidget: getTitles, + interval: 12, + ); + static const sideTitles4 = SideTitles( + reservedSize: 11, + showTitles: true, + getTitlesWidget: getTitles, + interval: 12, + ); + static const sideTitles5 = SideTitles( + reservedSize: 10, + getTitlesWidget: getTitles, + interval: 43, + ); + static const sideTitles6 = SideTitles( + reservedSize: 10, + getTitlesWidget: getTitles, + interval: 22, + ); + + static const sideTitleFitInsideData1 = SideTitleFitInsideData( + enabled: true, + axisPosition: 0, + parentAxisSize: 100, + distanceFromEdge: 0, + ); + static const sideTitleFitInsideData1Clone = SideTitleFitInsideData( + enabled: true, + axisPosition: 0, + parentAxisSize: 100, + distanceFromEdge: 0, + ); + static const sideTitleFitInsideData2 = SideTitleFitInsideData( + enabled: true, + axisPosition: 0, + parentAxisSize: 100, + distanceFromEdge: 10, + ); + static const sideTitleFitInsideData3 = SideTitleFitInsideData( + enabled: false, + axisPosition: 0, + parentAxisSize: 100, + distanceFromEdge: 0, + ); + static const sideTitleFitInsideData4 = SideTitleFitInsideData( + enabled: true, + axisPosition: 200, + parentAxisSize: 200, + distanceFromEdge: 0, + ); + static const sideTitleFitInsideData5 = SideTitleFitInsideData( + enabled: true, + axisPosition: 200, + parentAxisSize: 200, + distanceFromEdge: 10, + ); + static const sideTitleFitInsideData6 = SideTitleFitInsideData( + enabled: false, + axisPosition: 200, + parentAxisSize: 200, + distanceFromEdge: 0, + ); + + static const widget1 = Text('axis1'); + static const widget2 = Text('axis2'); + static const widget3 = Text('axis3'); + static const widget4 = Text('axis4'); + static const widget5 = Text('axis5'); + + static const axisTitles1 = AxisTitles( + axisNameWidget: widget1, + sideTitles: sideTitles1, + ); + static const axisTitles1Clone = AxisTitles( + axisNameWidget: widget1, + sideTitles: sideTitles1Clone, + ); + static const axisTitles2 = AxisTitles( + axisNameWidget: widget2, + sideTitles: sideTitles2, + ); + static const axisTitles3 = AxisTitles( + axisNameWidget: widget3, + sideTitles: sideTitles3, + ); + static const axisTitles4 = AxisTitles( + axisNameWidget: widget4, + sideTitles: sideTitles4, + ); + static const axisTitles5 = AxisTitles( + axisNameWidget: widget5, + axisNameSize: 889, + sideTitles: sideTitles4, + ); + + static const flTitlesData1 = FlTitlesData( + bottomTitles: axisTitles1, + topTitles: axisTitles2, + rightTitles: axisTitles3, + leftTitles: axisTitles4, + ); + static const flTitlesData1Clone = FlTitlesData( + bottomTitles: axisTitles1Clone, + topTitles: axisTitles2, + rightTitles: axisTitles3, + leftTitles: axisTitles4, + ); + static const flTitlesData2 = FlTitlesData( + topTitles: axisTitles2, + rightTitles: axisTitles3, + leftTitles: axisTitles4, + ); + static const flTitlesData3 = FlTitlesData( + bottomTitles: axisTitles1, + rightTitles: axisTitles3, + leftTitles: axisTitles4, + ); + static const flTitlesData4 = FlTitlesData( + bottomTitles: axisTitles1, + topTitles: axisTitles2, + leftTitles: axisTitles4, + ); + static const flTitlesData5 = FlTitlesData( + bottomTitles: axisTitles1, + topTitles: axisTitles2, + rightTitles: axisTitles3, + ); + static const flTitlesData6 = FlTitlesData( + show: false, + bottomTitles: axisTitles1, + topTitles: axisTitles2, + rightTitles: axisTitles3, + leftTitles: axisTitles4, + ); +} + +final VerticalRangeAnnotation verticalRangeAnnotation1 = + VerticalRangeAnnotation(color: Colors.green, x2: 12, x1: 12.1); +final VerticalRangeAnnotation verticalRangeAnnotation1Clone = + VerticalRangeAnnotation(color: Colors.green, x2: 12, x1: 12.1); + +final HorizontalRangeAnnotation horizontalRangeAnnotation1 = + HorizontalRangeAnnotation(color: Colors.green, y2: 12, y1: 12.1); +final HorizontalRangeAnnotation horizontalRangeAnnotation1Clone = + HorizontalRangeAnnotation(color: Colors.green, y2: 12, y1: 12.1); + +final RangeAnnotations rangeAnnotations1 = RangeAnnotations( + horizontalRangeAnnotations: [ + horizontalRangeAnnotation1, + horizontalRangeAnnotation1Clone, + ], + verticalRangeAnnotations: [ + verticalRangeAnnotation1, + verticalRangeAnnotation1Clone, + ], +); +final RangeAnnotations rangeAnnotations1Clone = RangeAnnotations( + horizontalRangeAnnotations: [ + horizontalRangeAnnotation1, + horizontalRangeAnnotation1Clone, + ], + verticalRangeAnnotations: [ + verticalRangeAnnotation1, + verticalRangeAnnotation1Clone, + ], +); +final RangeAnnotations rangeAnnotations2 = RangeAnnotations( + horizontalRangeAnnotations: [ + horizontalRangeAnnotation1Clone, + ], + verticalRangeAnnotations: [ + verticalRangeAnnotation1, + verticalRangeAnnotation1Clone, + ], +); + +const FlLine flLine1 = + FlLine(color: Colors.green, strokeWidth: 1, dashArray: [1, 2, 3]); +const FlLine flLine1Clone = + FlLine(color: Colors.green, strokeWidth: 1, dashArray: [1, 2, 3]); + +bool checkToShowLine(double value) => true; + +FlLine getDrawingLine(double value) => const FlLine(); + +const FlSpot flSpot1 = FlSpot(1, 1); +final FlSpot flSpot1Clone = flSpot1.copyWith(); + +const FlSpot flSpot2 = FlSpot(4, 2); +final FlSpot flSpot2Clone = flSpot2.copyWith(); + +const nullSpot1 = FlSpot.nullSpot; +final nullSpot2 = nullSpot1.copyWith(); +const nullSpot3 = FlSpot.nullSpot; + +Widget getTitles(double value, TitleMeta meta) => const Text('sallam'); + +TextStyle getTextStyles(BuildContext context, double value) => + const TextStyle(color: Colors.green); + +const FlGridData flGridData1 = FlGridData( + verticalInterval: 12, + horizontalInterval: 22, + drawVerticalLine: false, + checkToShowVerticalLine: checkToShowLine, + getDrawingHorizontalLine: getDrawingLine, +); +const FlGridData flGridData1Clone = FlGridData( + verticalInterval: 12, + horizontalInterval: 22, + drawVerticalLine: false, + checkToShowVerticalLine: checkToShowLine, + getDrawingHorizontalLine: getDrawingLine, +); +final FlGridData flGridData2 = FlGridData( + verticalInterval: 12, + horizontalInterval: 22, + drawVerticalLine: false, + checkToShowVerticalLine: checkToShowLine, + getDrawingHorizontalLine: getDrawingLine, + getDrawingVerticalLine: (value) => flLine1, +); +const FlGridData flGridData3 = FlGridData( + verticalInterval: 12, + horizontalInterval: 43, + drawVerticalLine: false, + checkToShowVerticalLine: checkToShowLine, + getDrawingHorizontalLine: getDrawingLine, +); +const FlGridData flGridData4 = FlGridData( + verticalInterval: 12, + horizontalInterval: 22, + drawVerticalLine: false, + getDrawingHorizontalLine: getDrawingLine, +); +const FlGridData flGridData5 = FlGridData( + verticalInterval: 12, + horizontalInterval: 22, + checkToShowVerticalLine: checkToShowLine, + getDrawingHorizontalLine: getDrawingLine, +); + +final FlBorderData borderData1 = FlBorderData( + show: true, + border: Border.all(color: Colors.green), +); +final FlBorderData borderData1Clone = FlBorderData( + show: true, + border: Border.all(color: Colors.green), +); +final FlBorderData borderData2 = FlBorderData( + show: true, + border: Border.all(color: Colors.green.withValues(alpha: 0.5)), +); + +bool checkToShowSpotLine(FlSpot spot) => true; + +const BarAreaSpotsLine barAreaSpotsLine1 = + BarAreaSpotsLine(show: true, checkToShowSpotLine: checkToShowSpotLine); +const BarAreaSpotsLine barAreaSpotsLine1Clone = + BarAreaSpotsLine(show: true, checkToShowSpotLine: checkToShowSpotLine); + +const BarAreaSpotsLine barAreaSpotsLine2 = BarAreaSpotsLine( + show: true, +); + +final BarAreaData barAreaData1 = BarAreaData( + show: true, + cutOffY: 12, + gradient: const LinearGradient( + colors: [Colors.green, Colors.blue], + stops: [0, 0.5], + begin: Alignment.topLeft, + end: Alignment.bottomCenter, + ), + spotsLine: barAreaSpotsLine1, +); +final BarAreaData barAreaData1Clone = BarAreaData( + show: true, + cutOffY: 12, + gradient: const LinearGradient( + colors: [Colors.green, Colors.blue], + stops: [0, 0.5], + begin: Alignment.topLeft, + end: Alignment.bottomCenter, + ), + spotsLine: barAreaSpotsLine1, +); + +final BarAreaData barAreaData2 = BarAreaData( + show: true, + cutOffY: 12, + gradient: const LinearGradient( + colors: [Colors.green, Colors.blue], + stops: [0, 0.5], + begin: Alignment.topLeft, + end: Alignment.bottomCenter, + ), + spotsLine: barAreaSpotsLine2, +); +final BarAreaData barAreaData3 = BarAreaData( + show: true, + cutOffY: 12, + gradient: const LinearGradient( + colors: [Colors.green, Colors.blue], + stops: [0, 0.6], + begin: Alignment.topLeft, + end: Alignment.bottomCenter, + ), + spotsLine: barAreaSpotsLine2, +); +final BarAreaData barAreaData4 = BarAreaData( + show: true, + cutOffY: 12, + gradient: const LinearGradient( + colors: [Colors.green, Colors.blue], + stops: [0], + begin: Alignment.topLeft, + end: Alignment.bottomCenter, + ), + spotsLine: barAreaSpotsLine2, +); + +bool checkToShowDot(FlSpot spot, LineChartBarData barData) => true; + +FlDotCirclePainter getDotDrawer( + FlSpot spot, + double percent, + LineChartBarData barData, + int index, +) => + FlDotCirclePainter(radius: 44, strokeWidth: 12); + +FlDotCirclePainter getDotDrawer5( + FlSpot spot, + double percent, + LineChartBarData barData, + int index, +) => + FlDotCirclePainter(radius: 44, strokeWidth: 14); + +FlDotCirclePainter getDotDrawer6( + FlSpot spot, + double percent, + LineChartBarData barData, + int index, +) => + FlDotCirclePainter(radius: 44.01, strokeWidth: 14); + +FlDotCirclePainter getDotDrawerTouched( + FlSpot spot, + double percent, + LineChartBarData barData, + int index, +) => + FlDotCirclePainter(radius: 12, color: Colors.red); + +FlDotCirclePainter getDotDrawerTouched4( + FlSpot spot, + double percent, + LineChartBarData barData, + int index, +) => + FlDotCirclePainter(radius: 12); + +FlDotCirclePainter getDotDrawerTouched6( + FlSpot spot, + double percent, + LineChartBarData barData, + int index, +) => + FlDotCirclePainter(radius: 12.01, color: Colors.red); + +const FlDotData flDotData1 = FlDotData( + getDotPainter: getDotDrawer, + checkToShowDot: checkToShowDot, +); +const FlDotData flDotData1Clone = FlDotData( + getDotPainter: getDotDrawer, + checkToShowDot: checkToShowDot, +); + +const FlDotData flDotData4 = FlDotData( + getDotPainter: getDotDrawer, +); + +const FlDotData flDotData5 = FlDotData( + getDotPainter: getDotDrawer5, +); + +const FlDotData flDotData6 = FlDotData( + getDotPainter: getDotDrawer6, +); + +const Shadow shadow1 = Shadow( + color: Colors.red, + blurRadius: 12, +); +const Shadow shadow1Clone = Shadow( + color: Colors.red, + blurRadius: 12, +); +const Shadow shadow2 = Shadow( + color: Colors.green, + blurRadius: 12, +); +const Shadow shadow3 = Shadow( + color: Colors.red, + blurRadius: 14, +); +final Shadow shadow4 = Shadow( + color: Colors.red.withValues(alpha: 0.5), + blurRadius: 12, +); + +const LineChartStepData lineChartStepData1 = LineChartStepData(); + +const LineChartStepData lineChartStepData1Clone = LineChartStepData(); + +const LineChartStepData lineChartStepData2 = LineChartStepData( + stepDirection: LineChartStepData.stepDirectionForward, +); + +final LineChartBarData lineChartBarData1 = LineChartBarData( + dashArray: [0, 1], + gradient: const LinearGradient( + colors: [Colors.red, Colors.green], + stops: [0, 1], + begin: Alignment.center, + end: Alignment.bottomRight, + ), + spots: [ + flSpot1, + flSpot2, + ], + shadow: shadow1, + aboveBarData: barAreaData1, + belowBarData: barAreaData2, + barWidth: 12, + curveSmoothness: 12, + dotData: flDotData1, + isStrokeCapRound: true, + preventCurveOvershootingThreshold: 1.2, + showingIndicators: [0, 1], + errorIndicatorData: const FlErrorIndicatorData( + show: false, + ), +); +final LineChartBarData lineChartBarData1Clone = LineChartBarData( + dashArray: [0, 1], + gradient: const LinearGradient( + colors: [Colors.red, Colors.green], + stops: [0, 1], + begin: Alignment.center, + end: Alignment.bottomRight, + ), + spots: [ + flSpot1Clone, + flSpot2, + ], + shadow: shadow1Clone, + aboveBarData: barAreaData1Clone, + belowBarData: barAreaData2, + barWidth: 12, + curveSmoothness: 12, + dotData: flDotData1Clone, + isStrokeCapRound: true, + preventCurveOvershootingThreshold: 1.2, + showingIndicators: [0, 1], + errorIndicatorData: const FlErrorIndicatorData( + show: false, + ), +); + +final LineChartBarData lineChartBarData2 = LineChartBarData( + dashArray: [0, 1], + gradient: const LinearGradient( + colors: [Colors.red, Colors.green], + stops: [0, 1], + begin: Alignment.center, + end: Alignment.bottomRight, + ), + spots: [ + flSpot1, + flSpot2, + ], + shadow: shadow2, + isStepLineChart: true, + aboveBarData: barAreaData1, + belowBarData: barAreaData2, + barWidth: 12, + curveSmoothness: 12, + dotData: flDotData1, + isStrokeCapRound: true, + preventCurveOvershootingThreshold: 1.2, + showingIndicators: [0, 4], +); + +final LineChartBarData lineChartBarData3 = LineChartBarData( + gradient: const LinearGradient( + colors: [Colors.red, Colors.green], + stops: [0, 1], + begin: Alignment.center, + end: Alignment.bottomRight, + ), + spots: [ + flSpot1, + flSpot2, + ], + shadow: shadow3, + isStepLineChart: true, + lineChartStepData: lineChartStepData2, + aboveBarData: barAreaData1, + belowBarData: barAreaData2, + barWidth: 12, + curveSmoothness: 12, + dotData: flDotData1, + isStrokeCapRound: true, + preventCurveOvershootingThreshold: 1.2, + showingIndicators: [0, 1], +); + +final LineChartBarData lineChartBarData4 = LineChartBarData( + dashArray: [0, 1], + gradient: const LinearGradient( + colors: [Colors.red, Colors.green], + stops: [0, 1], + begin: Alignment.center, + end: Alignment.bottomRight, + ), + spots: [ + flSpot2, + flSpot1, + ], + shadow: shadow4, + lineChartStepData: lineChartStepData2, + aboveBarData: barAreaData1, + belowBarData: barAreaData2, + barWidth: 12, + curveSmoothness: 12, + dotData: flDotData1, + isStrokeCapRound: true, + preventCurveOvershootingThreshold: 1.2, + showingIndicators: [0, 1], +); + +final LineChartBarData lineChartBarData5 = LineChartBarData( + dashArray: [0, 1], + gradient: const LinearGradient( + colors: [Colors.red, Colors.green], + stops: [0, 1], + begin: Alignment.center, + end: Alignment.bottomRight, + ), + spots: [ + flSpot1, + flSpot2, + ], + aboveBarData: barAreaData2, + belowBarData: barAreaData1, + barWidth: 12, + curveSmoothness: 12, + dotData: flDotData1, + isStrokeCapRound: true, + preventCurveOvershootingThreshold: 1.2, + showingIndicators: [0, 1], +); + +final LineChartBarData lineChartBarData6 = LineChartBarData( + dashArray: [0, 1], + gradient: const LinearGradient( + colors: [Colors.red, Colors.green], + stops: [0, 1], + begin: Alignment.center, + end: Alignment.bottomRight, + ), + spots: [ + flSpot1, + flSpot2, + ], + aboveBarData: barAreaData1, + belowBarData: barAreaData2, + barWidth: 12, + curveSmoothness: 12, + dotData: flDotData1, + isStrokeCapRound: true, + preventCurveOvershootingThreshold: 1.2, + showingIndicators: [0, 1], +); + +final LineChartBarData lineChartBarData7 = LineChartBarData( + dashArray: [0, 1], + gradient: LinearGradient( + colors: [Colors.red, Colors.green.withValues(alpha: 0.4)], + stops: const [0, 1], + begin: Alignment.center, + end: Alignment.bottomRight, + ), + spots: [ + flSpot1, + flSpot2, + ], + aboveBarData: barAreaData1, + belowBarData: barAreaData2, + barWidth: 12, + curveSmoothness: 12, + dotData: flDotData1, + isStrokeCapRound: true, + preventCurveOvershootingThreshold: 1.2, + showingIndicators: [0, 1], +); + +final LineChartBarData lineChartBarData8 = LineChartBarData( + dashArray: [0, 1], + gradient: const LinearGradient( + colors: [Colors.red, Colors.green], + stops: [0, 1], + begin: Alignment.center, + end: Alignment.bottomRight, + ), + spots: [ + flSpot1, + flSpot2, + ], + aboveBarData: barAreaData1, + belowBarData: barAreaData2, + barWidth: 12, + curveSmoothness: 12.01, + dotData: flDotData1, + isStrokeCapRound: true, + preventCurveOvershootingThreshold: 1.2, + showingIndicators: [0, 1], +); + +final LineChartBarData lineChartBarData9 = LineChartBarData( + dashArray: [0, 1], + gradient: const LinearGradient( + colors: [Colors.red, Colors.green], + stops: [0, 1], + begin: Alignment.center, + end: Alignment.bottomRight, + ), + spots: [ + flSpot1, + flSpot2, + ], + aboveBarData: barAreaData1, + belowBarData: barAreaData2, + barWidth: 12, + curveSmoothness: 12, + dotData: flDotData1, + isStrokeCapRound: true, + preventCurveOverShooting: true, + preventCurveOvershootingThreshold: 1.2, + showingIndicators: [0, 1], +); + +final TouchLineBarSpot lineBarSpot1 = TouchLineBarSpot( + lineChartBarData1, + 0, + flSpot1, + 0, +); +final TouchLineBarSpot lineBarSpot1Clone = TouchLineBarSpot( + lineChartBarData1Clone, + 0, + flSpot1Clone, + 0, +); + +final TouchLineBarSpot lineBarSpot2 = + TouchLineBarSpot(lineChartBarData1, 2, flSpot1, 2); + +final TouchLineBarSpot lineBarSpot3 = TouchLineBarSpot( + lineChartBarData1, + 100, + flSpot1, + 2, +); + +final lineTouchResponse1 = LineTouchResponse( + touchLocation: Offset.zero, + touchChartCoordinate: Offset.zero, + lineBarSpots: [ + lineBarSpot1, + lineBarSpot2, + ], +); +final lineTouchResponse1Clone = LineTouchResponse( + touchLocation: Offset.zero, + touchChartCoordinate: Offset.zero, + lineBarSpots: [ + lineBarSpot1Clone, + lineBarSpot2, + ], +); + +final lineTouchResponse2 = LineTouchResponse( + touchLocation: Offset.zero, + touchChartCoordinate: Offset.zero, + lineBarSpots: [ + lineBarSpot2, + lineBarSpot1, + ], +); + +final lineTouchResponse3 = LineTouchResponse( + touchLocation: Offset.zero, + touchChartCoordinate: Offset.zero, + lineBarSpots: [], +); + +final lineTouchResponse4 = LineTouchResponse( + touchLocation: Offset.zero, + touchChartCoordinate: Offset.zero, + lineBarSpots: [ + lineBarSpot1, + lineBarSpot2, + ], +); + +final lineTouchResponse5 = LineTouchResponse( + touchLocation: Offset.zero, + touchChartCoordinate: Offset.zero, + lineBarSpots: [ + lineBarSpot1, + lineBarSpot2, + ], +); + +const TouchedSpotIndicatorData touchedSpotIndicatorData1 = + TouchedSpotIndicatorData( + FlLine( + color: Colors.red, + dashArray: [], + ), + FlDotData( + getDotPainter: getDotDrawerTouched, + checkToShowDot: checkToShowDot, + ), +); +const TouchedSpotIndicatorData touchedSpotIndicatorData1Clone = + TouchedSpotIndicatorData( + FlLine( + color: Colors.red, + dashArray: [], + ), + FlDotData( + getDotPainter: getDotDrawerTouched, + checkToShowDot: checkToShowDot, + ), +); + +const TouchedSpotIndicatorData touchedSpotIndicatorData2 = + TouchedSpotIndicatorData( + FlLine( + color: Colors.red, + dashArray: [], + ), + FlDotData( + getDotPainter: getDotDrawerTouched, + ), +); +const TouchedSpotIndicatorData touchedSpotIndicatorData3 = + TouchedSpotIndicatorData( + FlLine( + color: Colors.red, + ), + FlDotData( + getDotPainter: getDotDrawerTouched, + checkToShowDot: checkToShowDot, + ), +); +const TouchedSpotIndicatorData touchedSpotIndicatorData4 = + TouchedSpotIndicatorData( + FlLine( + color: Colors.green, + dashArray: [], + ), + FlDotData( + getDotPainter: getDotDrawerTouched4, + checkToShowDot: checkToShowDot, + ), +); +const TouchedSpotIndicatorData touchedSpotIndicatorData5 = + TouchedSpotIndicatorData( + FlLine( + color: Colors.red, + dashArray: [], + ), + FlDotData( + getDotPainter: getDotDrawerTouched, + checkToShowDot: checkToShowDot, + show: false, + ), +); +const TouchedSpotIndicatorData touchedSpotIndicatorData6 = + TouchedSpotIndicatorData( + FlLine( + color: Colors.red, + dashArray: [], + ), + FlDotData( + getDotPainter: getDotDrawerTouched6, + checkToShowDot: checkToShowDot, + ), +); + +const LineTooltipItem lineTooltipItem1 = + LineTooltipItem('', TextStyle(color: Colors.green)); +const LineTooltipItem lineTooltipItem1Clone = + LineTooltipItem('', TextStyle(color: Colors.green)); + +const LineTooltipItem lineTooltipItem2 = + LineTooltipItem('ss', TextStyle(color: Colors.green)); +const LineTooltipItem lineTooltipItem3 = + LineTooltipItem('', TextStyle(color: Colors.blue)); +const LineTooltipItem lineTooltipItem4 = + LineTooltipItem('', TextStyle(fontSize: 33)); + +List lineChartGetTooltipItems(List list) { + return list.map((s) => lineTooltipItem1).toList(); +} + +Color lineChartGetGreenColor(LineBarSpot touchedSpot) { + return Colors.green; +} + +Color lineChartGetRedColor(LineBarSpot touchedSpot) { + return Colors.red; +} + +final LineTouchTooltipData lineTouchTooltipData1 = LineTouchTooltipData( + tooltipPadding: const EdgeInsets.all(0.1), + getTooltipColor: lineChartGetGreenColor, + maxContentWidth: 12, + getTooltipItems: lineChartGetTooltipItems, + fitInsideHorizontally: true, + tooltipBorderRadius: BorderRadius.circular(12), + tooltipMargin: 33, + tooltipBorder: const BorderSide(color: Colors.red), +); +final LineTouchTooltipData lineTouchTooltipData1Clone = LineTouchTooltipData( + tooltipPadding: const EdgeInsets.all(0.1), + getTooltipColor: lineChartGetGreenColor, + maxContentWidth: 12, + getTooltipItems: lineChartGetTooltipItems, + fitInsideHorizontally: true, + tooltipBorderRadius: BorderRadius.circular(12), + tooltipMargin: 33, + tooltipBorder: const BorderSide(color: Colors.red), +); + +final LineTouchTooltipData lineTouchTooltipData2 = LineTouchTooltipData( + tooltipPadding: const EdgeInsets.all(0.1), + getTooltipColor: lineChartGetRedColor, + maxContentWidth: 12, + getTooltipItems: lineChartGetTooltipItems, + fitInsideHorizontally: true, + tooltipBorderRadius: BorderRadius.circular(12), + tooltipMargin: 33, + tooltipBorder: const BorderSide(color: Colors.red), +); +final LineTouchTooltipData lineTouchTooltipData3 = LineTouchTooltipData( + tooltipPadding: const EdgeInsets.all(0.2), + getTooltipColor: lineChartGetGreenColor, + maxContentWidth: 12, + getTooltipItems: lineChartGetTooltipItems, + fitInsideHorizontally: true, + tooltipBorderRadius: BorderRadius.circular(12), + tooltipMargin: 33, + tooltipBorder: const BorderSide(color: Colors.red), + tooltipHorizontalAlignment: FLHorizontalAlignment.left, +); +final LineTouchTooltipData lineTouchTooltipData4 = LineTouchTooltipData( + tooltipPadding: const EdgeInsets.all(0.1), + getTooltipColor: lineChartGetGreenColor, + maxContentWidth: 13, + getTooltipItems: lineChartGetTooltipItems, + fitInsideHorizontally: true, + tooltipBorderRadius: BorderRadius.circular(12), + tooltipMargin: 33, + tooltipBorder: const BorderSide(color: Colors.red), + tooltipHorizontalAlignment: FLHorizontalAlignment.right, +); +final LineTouchTooltipData lineTouchTooltipData5 = LineTouchTooltipData( + tooltipPadding: const EdgeInsets.all(0.1), + getTooltipColor: lineChartGetGreenColor, + maxContentWidth: 12, + getTooltipItems: lineChartGetTooltipItems, + fitInsideHorizontally: true, + tooltipBorderRadius: BorderRadius.circular(12), + tooltipMargin: 34, + tooltipBorder: const BorderSide(color: Colors.red), + tooltipHorizontalOffset: 10, +); +final LineTouchTooltipData lineTouchTooltipData6 = LineTouchTooltipData( + tooltipPadding: const EdgeInsets.all(0.1), + getTooltipColor: lineChartGetGreenColor, + maxContentWidth: 12, + getTooltipItems: lineChartGetTooltipItems, + fitInsideHorizontally: true, + tooltipBorderRadius: BorderRadius.circular(12), + tooltipMargin: 33, + tooltipBorder: const BorderSide(color: Colors.pink), + tooltipHorizontalAlignment: FLHorizontalAlignment.left, + tooltipHorizontalOffset: -10, +); +final LineTouchTooltipData lineTouchTooltipData7 = LineTouchTooltipData( + tooltipPadding: const EdgeInsets.all(0.1), + getTooltipColor: lineChartGetGreenColor, + maxContentWidth: 12, + getTooltipItems: lineChartGetTooltipItems, + fitInsideHorizontally: true, + tooltipBorderRadius: BorderRadius.circular(12), + tooltipMargin: 33, + tooltipBorder: const BorderSide(color: Colors.red, width: 2), + tooltipHorizontalAlignment: FLHorizontalAlignment.right, + tooltipHorizontalOffset: 10, +); + +void lineTouchCallback(FlTouchEvent event, LineTouchResponse? response) {} + +List getTouchedSpotIndicator( + LineChartBarData barData, + List indexes, +) => + indexes.map((i) => touchedSpotIndicatorData1).toList(); + +final LineTouchData lineTouchData1 = LineTouchData( + touchCallback: lineTouchCallback, + getTouchedSpotIndicator: getTouchedSpotIndicator, + handleBuiltInTouches: false, + touchSpotThreshold: 12, + touchTooltipData: lineTouchTooltipData1, +); +final LineTouchData lineTouchData1Clone = LineTouchData( + touchCallback: lineTouchCallback, + getTouchedSpotIndicator: getTouchedSpotIndicator, + handleBuiltInTouches: false, + touchSpotThreshold: 12, + touchTooltipData: lineTouchTooltipData1, +); + +final LineTouchData lineTouchData2 = LineTouchData( + getTouchedSpotIndicator: getTouchedSpotIndicator, + handleBuiltInTouches: false, + touchSpotThreshold: 12, + touchTooltipData: lineTouchTooltipData1, +); +final LineTouchData lineTouchData3 = LineTouchData( + touchCallback: lineTouchCallback, + handleBuiltInTouches: false, + touchSpotThreshold: 12, + touchTooltipData: lineTouchTooltipData1, +); +const LineTouchData lineTouchData4 = LineTouchData( + touchCallback: lineTouchCallback, + getTouchedSpotIndicator: getTouchedSpotIndicator, + handleBuiltInTouches: false, + touchSpotThreshold: 12, +); +final LineTouchData lineTouchData5 = LineTouchData( + touchCallback: lineTouchCallback, + getTouchedSpotIndicator: getTouchedSpotIndicator, + handleBuiltInTouches: false, + touchSpotThreshold: 12.001, + touchTooltipData: lineTouchTooltipData1, +); +final LineTouchData lineTouchData6 = LineTouchData( + touchCallback: lineTouchCallback, + getTouchedSpotIndicator: getTouchedSpotIndicator, + touchSpotThreshold: 12, + touchTooltipData: lineTouchTooltipData1, +); +final LineTouchData lineTouchData7 = LineTouchData( + touchCallback: lineTouchCallback, + getTouchedSpotIndicator: getTouchedSpotIndicator, + handleBuiltInTouches: false, + touchSpotThreshold: 12, + touchTooltipData: lineTouchTooltipData1, + getTouchLineEnd: (barData, index) => double.infinity, +); +final LineTouchData lineTouchData8 = LineTouchData( + touchCallback: lineTouchCallback, + getTouchedSpotIndicator: getTouchedSpotIndicator, + handleBuiltInTouches: false, + touchSpotThreshold: 12, + touchTooltipData: lineTouchTooltipData1, + longPressDuration: Duration.zero, +); + +String horizontalLabelResolver(HorizontalLine horizontalLine) => 'test'; + +String verticalLabelResolver(VerticalLine horizontalLine) => 'test'; + +final HorizontalLineLabel horizontalLineLabel1 = HorizontalLineLabel( + show: true, + style: const TextStyle(color: Colors.green), + labelResolver: horizontalLabelResolver, + alignment: Alignment.topCenter, + padding: const EdgeInsets.all(12), + direction: LabelDirection.vertical, +); +final HorizontalLineLabel horizontalLineLabel1Clone = HorizontalLineLabel( + show: true, + style: const TextStyle(color: Colors.green), + labelResolver: horizontalLabelResolver, + alignment: Alignment.topCenter, + padding: const EdgeInsets.all(12), + direction: LabelDirection.vertical, +); +final HorizontalLineLabel horizontalLineLabel2 = HorizontalLineLabel( + style: const TextStyle(color: Colors.green), + labelResolver: horizontalLabelResolver, + alignment: Alignment.topCenter, + padding: const EdgeInsets.all(12), +); +final HorizontalLineLabel horizontalLineLabel3 = HorizontalLineLabel( + show: true, + labelResolver: horizontalLabelResolver, + alignment: Alignment.topCenter, + padding: const EdgeInsets.all(12), +); +final HorizontalLineLabel horizontalLineLabel4 = HorizontalLineLabel( + show: true, + style: const TextStyle(color: Colors.green), + alignment: Alignment.topCenter, + padding: const EdgeInsets.all(12), +); +final HorizontalLineLabel horizontalLineLabel5 = HorizontalLineLabel( + show: true, + style: const TextStyle(color: Colors.green), + labelResolver: horizontalLabelResolver, + alignment: Alignment.bottomRight, + padding: const EdgeInsets.all(12), +); +final HorizontalLineLabel horizontalLineLabel6 = HorizontalLineLabel( + show: true, + style: const TextStyle(color: Colors.green), + labelResolver: horizontalLabelResolver, + alignment: Alignment.topCenter, + padding: const EdgeInsets.all(44), +); +final HorizontalLineLabel horizontalLineLabel7 = HorizontalLineLabel( + style: const TextStyle(color: Colors.green), + labelResolver: horizontalLabelResolver, + alignment: Alignment.topCenter, + padding: const EdgeInsets.all(12), + direction: LabelDirection.vertical, +); + +final VerticalLineLabel verticalLineLabel1 = VerticalLineLabel( + show: true, + style: const TextStyle(color: Colors.green), + labelResolver: verticalLabelResolver, + alignment: Alignment.topCenter, + padding: const EdgeInsets.all(12), +); +final VerticalLineLabel verticalLineLabel1Clone = VerticalLineLabel( + show: true, + style: const TextStyle(color: Colors.green), + labelResolver: verticalLabelResolver, + alignment: Alignment.topCenter, + padding: const EdgeInsets.all(12), +); +final VerticalLineLabel verticalLineLabel2 = VerticalLineLabel( + style: const TextStyle(color: Colors.green), + labelResolver: verticalLabelResolver, + alignment: Alignment.topCenter, + padding: const EdgeInsets.all(12), +); +final VerticalLineLabel verticalLineLabel3 = VerticalLineLabel( + show: true, + labelResolver: verticalLabelResolver, + alignment: Alignment.topCenter, + padding: const EdgeInsets.all(12), +); +final VerticalLineLabel verticalLineLabel4 = VerticalLineLabel( + show: true, + style: const TextStyle(color: Colors.green), + alignment: Alignment.topCenter, + padding: const EdgeInsets.all(12), +); +final VerticalLineLabel verticalLineLabel5 = VerticalLineLabel( + show: true, + style: const TextStyle(color: Colors.green), + labelResolver: verticalLabelResolver, + padding: const EdgeInsets.all(12), +); +final VerticalLineLabel verticalLineLabel6 = VerticalLineLabel( + show: true, + style: const TextStyle(color: Colors.green), + labelResolver: verticalLabelResolver, + alignment: Alignment.topCenter, + padding: const EdgeInsets.all(44), +); +final VerticalLineLabel verticalLineLabel7 = VerticalLineLabel( + style: const TextStyle(color: Colors.green), + labelResolver: verticalLabelResolver, + alignment: Alignment.topCenter, + padding: const EdgeInsets.all(12), + direction: LabelDirection.vertical, +); + +final HorizontalLine horizontalLine1 = HorizontalLine( + y: 12, + color: Colors.red, + dashArray: [0, 1], + strokeWidth: 21, + label: horizontalLineLabel1, +); +final HorizontalLine horizontalLine1Clone = HorizontalLine( + y: 12, + color: Colors.red, + dashArray: [0, 1], + strokeWidth: 21, + label: horizontalLineLabel1, +); +final HorizontalLine horizontalLine2 = HorizontalLine( + y: 12, + color: Colors.red, + dashArray: [0, 1, 3], + strokeWidth: 21, + label: horizontalLineLabel1, +); +final HorizontalLine horizontalLine3 = HorizontalLine( + y: 12, + color: Colors.red, + dashArray: [0, 1], + strokeWidth: 22, + label: horizontalLineLabel1, +); +final HorizontalLine horizontalLine4 = HorizontalLine( + y: 12, + color: Colors.red, + dashArray: [1, 0], + strokeWidth: 21, + label: horizontalLineLabel1, +); +final HorizontalLine horizontalLine5 = HorizontalLine( + y: 33, + color: Colors.red, + dashArray: [0, 1], + strokeWidth: 21, + label: horizontalLineLabel1, +); +final HorizontalLine horizontalLine6 = HorizontalLine( + y: 12, + color: Colors.green, + dashArray: [0, 1], + strokeWidth: 21, + label: horizontalLineLabel1, +); +final HorizontalLine horizontalLine7 = HorizontalLine( + y: 12, + color: Colors.red, + dashArray: [0, 1], + strokeWidth: 21, + label: horizontalLineLabel2, +); +final HorizontalLine horizontalLine8 = HorizontalLine( + y: 12, + color: Colors.red, + strokeWidth: 21, + label: horizontalLineLabel1, +); +final HorizontalLine horizontalLine9 = HorizontalLine( + y: 12, + color: Colors.red, + dashArray: [0, 12, 44], + strokeWidth: 21, + label: horizontalLineLabel1, +); + +final VerticalLine verticalLine1 = VerticalLine( + x: 12, + color: Colors.red, + dashArray: [0, 1], + strokeWidth: 21, + label: verticalLineLabel1, +); +final VerticalLine verticalLine1Clone = VerticalLine( + x: 12, + color: Colors.red, + dashArray: [0, 1], + strokeWidth: 21, + label: verticalLineLabel1, +); +final VerticalLine verticalLine2 = VerticalLine( + x: 12, + color: Colors.green, + dashArray: [0, 1], + strokeWidth: 21, + label: verticalLineLabel1, +); +final VerticalLine verticalLine3 = VerticalLine( + x: 12, + color: Colors.red, + dashArray: [0, 1], + strokeWidth: 22, + label: verticalLineLabel1, +); +final VerticalLine verticalLine4 = VerticalLine( + x: 12, + color: Colors.red, + dashArray: [1, 0], + strokeWidth: 21, + label: verticalLineLabel1, +); +final VerticalLine verticalLine5 = VerticalLine( + x: 33, + color: Colors.red, + dashArray: [0, 1], + strokeWidth: 21, + label: verticalLineLabel1, +); +final VerticalLine verticalLine6 = VerticalLine( + x: 12, + color: Colors.green, + dashArray: [0, 1], + strokeWidth: 21, + label: verticalLineLabel1, +); +final VerticalLine verticalLine7 = VerticalLine( + x: 12, + color: Colors.red, + dashArray: [0, 1], + strokeWidth: 21, + label: verticalLineLabel2, +); +final VerticalLine verticalLine8 = VerticalLine( + x: 12, + color: Colors.red, + strokeWidth: 21, + label: verticalLineLabel1, +); +final VerticalLine verticalLine9 = VerticalLine( + x: 12, + color: Colors.red, + dashArray: [0, 12, 44], + strokeWidth: 21, + label: verticalLineLabel1, +); + +final ExtraLinesData extraLinesData1 = ExtraLinesData( + horizontalLines: [ + horizontalLine1, + horizontalLine2, + horizontalLine3, + ], + verticalLines: [ + verticalLine1, + verticalLine2, + verticalLine3, + ], + extraLinesOnTop: false, +); +final ExtraLinesData extraLinesData1Clone = ExtraLinesData( + horizontalLines: [ + horizontalLine1Clone, + horizontalLine2, + horizontalLine3, + ], + verticalLines: [ + verticalLine1Clone, + verticalLine2, + verticalLine3, + ], + extraLinesOnTop: false, +); + +final ExtraLinesData extraLinesData2 = ExtraLinesData( + horizontalLines: [ + horizontalLine3, + horizontalLine1, + horizontalLine2, + ], + verticalLines: [ + verticalLine3, + verticalLine1, + verticalLine2, + ], + extraLinesOnTop: false, +); +final ExtraLinesData extraLinesData3 = ExtraLinesData( + horizontalLines: [ + horizontalLine1, + horizontalLine2, + ], + verticalLines: [ + verticalLine1, + verticalLine2, + ], + extraLinesOnTop: false, +); +final ExtraLinesData extraLinesData4 = ExtraLinesData( + horizontalLines: [ + horizontalLine1, + horizontalLine2, + horizontalLine3, + ], + extraLinesOnTop: false, +); +final ExtraLinesData extraLinesData5 = ExtraLinesData( + verticalLines: [ + verticalLine1, + verticalLine2, + verticalLine3, + ], + extraLinesOnTop: false, +); +final ExtraLinesData extraLinesData6 = ExtraLinesData( + horizontalLines: [ + horizontalLine1, + horizontalLine2, + horizontalLine3, + ], + verticalLines: [ + verticalLine1, + verticalLine2, + verticalLine3, + ], +); + +final SizedPicture sizedPicture1 = SizedPicture( + PictureRecorder().endRecording(), + 10, + 30, +); +final SizedPicture sizedPicture1Clone = SizedPicture( + PictureRecorder().endRecording(), + 10, + 30, +); + +final SizedPicture sizedPicture2 = SizedPicture( + PictureRecorder().endRecording(), + 11, + 30, +); +final SizedPicture sizedPicture3 = SizedPicture( + PictureRecorder().endRecording(), + 10, + 32, +); +final SizedPicture sizedPicture4 = SizedPicture( + PictureRecorder().endRecording(), + 442, + 30, +); + +final BetweenBarsData betweenBarsData1 = BetweenBarsData( + fromIndex: 1, + toIndex: 2, + gradient: const LinearGradient( + begin: Alignment(1, 3), + end: Alignment(4, 1), + stops: [1, 2, 3], + colors: [Colors.green, Colors.blue, Colors.red], + ), +); +final BetweenBarsData betweenBarsData1Clone = BetweenBarsData( + fromIndex: 1, + toIndex: 2, + gradient: const LinearGradient( + begin: Alignment(1, 3), + end: Alignment(4, 1), + stops: [1, 2, 3], + colors: [Colors.green, Colors.blue, Colors.red], + ), +); +final BetweenBarsData betweenBarsData2 = BetweenBarsData( + fromIndex: 2, + toIndex: 2, + gradient: const LinearGradient( + begin: Alignment(1, 3), + end: Alignment(4, 1), + stops: [1, 2, 3], + colors: [Colors.green, Colors.blue, Colors.red], + ), +); +final BetweenBarsData betweenBarsData3 = BetweenBarsData( + fromIndex: 1, + toIndex: 1, + gradient: const LinearGradient( + begin: Alignment(1, 4), + end: Alignment(4, 1), + stops: [1, 2, 3], + colors: [Colors.green, Colors.blue, Colors.red], + ), +); +final BetweenBarsData betweenBarsData4 = BetweenBarsData( + fromIndex: 1, + toIndex: 2, + gradient: const LinearGradient( + begin: Alignment(1, 3), + end: Alignment(5, 1), + stops: [1, 2, 3], + colors: [Colors.green, Colors.blue, Colors.red], + ), +); +final BetweenBarsData betweenBarsData5 = BetweenBarsData( + fromIndex: 1, + toIndex: 2, + gradient: const LinearGradient( + begin: Alignment(1, 3), + end: Alignment(4, 1), + colors: [Colors.green, Colors.blue, Colors.red], + ), +); +final BetweenBarsData betweenBarsData6 = BetweenBarsData( + fromIndex: 1, + toIndex: 2, + gradient: const LinearGradient( + begin: Alignment(1, 3), + end: Alignment(4, 1), + stops: [1, 2, 3], + colors: [Colors.green, Colors.blue], + ), +); +final BetweenBarsData betweenBarsData7 = BetweenBarsData( + fromIndex: 1, + toIndex: 2, + gradient: const LinearGradient( + begin: Alignment(1, 22), + end: Alignment(4, 1), + stops: [1, 2, 3], + colors: [Colors.green, Colors.blue], + ), +); +final BetweenBarsData betweenBarsData8 = BetweenBarsData( + fromIndex: 1, + toIndex: 2, + gradient: const LinearGradient( + begin: Alignment(1, 3), + end: Alignment(4, 1), + stops: [1, 2, 3], + colors: [], + ), +); + +final ShowingTooltipIndicators showingTooltipIndicator1 = + ShowingTooltipIndicators( + [lineBarSpot1, lineBarSpot2], +); +final ShowingTooltipIndicators showingTooltipIndicator1Clone = + ShowingTooltipIndicators( + [lineBarSpot1, lineBarSpot2], +); +const ShowingTooltipIndicators showingTooltipIndicator2 = + ShowingTooltipIndicators( + [], +); +final ShowingTooltipIndicators showingTooltipIndicator3 = + ShowingTooltipIndicators( + [lineBarSpot2], +); +final ShowingTooltipIndicators showingTooltipIndicator4 = + ShowingTooltipIndicators( + [lineBarSpot2, lineBarSpot1], +); +final ShowingTooltipIndicators showingTooltipIndicator5 = + ShowingTooltipIndicators( + [lineBarSpot1, lineBarSpot2, lineBarSpot2], +); + +final LineChartData lineChartData1 = LineChartData( + borderData: borderData1, + lineTouchData: lineTouchData1, + showingTooltipIndicators: [ + showingTooltipIndicator1, + showingTooltipIndicator2, + ], + backgroundColor: Colors.red, + maxY: 23, + rangeAnnotations: rangeAnnotations1, + gridData: flGridData1, + titlesData: MockData.flTitlesData1, + lineBarsData: [lineChartBarData1, lineChartBarData2, lineChartBarData3], + betweenBarsData: [betweenBarsData1, betweenBarsData2, betweenBarsData3], + extraLinesData: extraLinesData1, + maxX: 23, + minX: 11, + minY: 43, +); +final LineChartData lineChartData1Clone = LineChartData( + borderData: borderData1Clone, + lineTouchData: lineTouchData1Clone, + showingTooltipIndicators: [ + showingTooltipIndicator1Clone, + showingTooltipIndicator2, + ], + backgroundColor: Colors.red, + maxY: 23, + rangeAnnotations: rangeAnnotations1, + gridData: flGridData1Clone, + titlesData: MockData.flTitlesData1Clone, + lineBarsData: [lineChartBarData1Clone, lineChartBarData2, lineChartBarData3], + betweenBarsData: [betweenBarsData1Clone, betweenBarsData2, betweenBarsData3], + extraLinesData: extraLinesData1Clone, + maxX: 23, + minX: 11, + minY: 43, +); +final LineChartData lineChartData2 = LineChartData( + lineTouchData: lineTouchData1, + showingTooltipIndicators: [ + showingTooltipIndicator1, + showingTooltipIndicator2, + ], + backgroundColor: Colors.red, + maxY: 23, + rangeAnnotations: rangeAnnotations1, + gridData: flGridData1, + titlesData: MockData.flTitlesData1, + lineBarsData: [lineChartBarData1, lineChartBarData2, lineChartBarData3], + betweenBarsData: [betweenBarsData1, betweenBarsData2, betweenBarsData3], + extraLinesData: extraLinesData1, + maxX: 23, + minX: 11, + minY: 43, +); +final LineChartData lineChartData3 = LineChartData( + borderData: borderData1, + lineTouchData: lineTouchData2, + showingTooltipIndicators: [ + showingTooltipIndicator1, + showingTooltipIndicator2, + ], + backgroundColor: Colors.red, + maxY: 23, + rangeAnnotations: rangeAnnotations1, + gridData: flGridData1, + titlesData: MockData.flTitlesData1, + lineBarsData: [lineChartBarData1, lineChartBarData2, lineChartBarData3], + betweenBarsData: [betweenBarsData1, betweenBarsData2, betweenBarsData3], + extraLinesData: extraLinesData1, + maxX: 23, + minX: 11, + minY: 43, +); +final LineChartData lineChartData4 = LineChartData( + borderData: borderData1, + lineTouchData: lineTouchData1, + showingTooltipIndicators: [], + backgroundColor: Colors.red, + maxY: 23, + rangeAnnotations: rangeAnnotations1, + gridData: flGridData1, + titlesData: MockData.flTitlesData1, + lineBarsData: [lineChartBarData1, lineChartBarData2, lineChartBarData3], + betweenBarsData: [betweenBarsData1, betweenBarsData2, betweenBarsData3], + extraLinesData: extraLinesData1, + maxX: 23, + minX: 11, + minY: 43, +); +final LineChartData lineChartData5 = LineChartData( + borderData: borderData1, + lineTouchData: lineTouchData1, + showingTooltipIndicators: [ + showingTooltipIndicator2, + showingTooltipIndicator1, + ], + backgroundColor: Colors.red, + maxY: 23, + rangeAnnotations: rangeAnnotations1, + gridData: flGridData1, + titlesData: MockData.flTitlesData1, + lineBarsData: [lineChartBarData1, lineChartBarData2, lineChartBarData3], + betweenBarsData: [betweenBarsData1, betweenBarsData2, betweenBarsData3], + extraLinesData: extraLinesData1, + maxX: 23, + minX: 11, + minY: 43, +); +final LineChartData lineChartData6 = LineChartData( + borderData: borderData1, + lineTouchData: lineTouchData1, + backgroundColor: Colors.red, + maxY: 23, + rangeAnnotations: rangeAnnotations1, + gridData: flGridData1, + titlesData: MockData.flTitlesData1, + lineBarsData: [lineChartBarData1, lineChartBarData2, lineChartBarData3], + betweenBarsData: [betweenBarsData1, betweenBarsData2, betweenBarsData3], + extraLinesData: extraLinesData1, + maxX: 23, + minX: 11, + minY: 43, +); +final LineChartData lineChartData7 = LineChartData( + borderData: borderData1, + lineTouchData: lineTouchData1, + showingTooltipIndicators: [ + showingTooltipIndicator1, + showingTooltipIndicator2, + ], + backgroundColor: Colors.red, + maxY: 23, + rangeAnnotations: rangeAnnotations1, + gridData: flGridData1, + titlesData: MockData.flTitlesData2, + lineBarsData: [lineChartBarData1, lineChartBarData2, lineChartBarData3], + betweenBarsData: [betweenBarsData1, betweenBarsData2, betweenBarsData3], + extraLinesData: extraLinesData1, + maxX: 23, + minX: 11, + minY: 43, +); +final LineChartData lineChartData8 = LineChartData( + borderData: borderData1, + lineTouchData: lineTouchData1, + showingTooltipIndicators: [ + showingTooltipIndicator1, + showingTooltipIndicator2, + ], + clipData: const FlClipData.all(), + backgroundColor: Colors.red, + maxY: 23, + rangeAnnotations: rangeAnnotations1, + gridData: flGridData1, + titlesData: MockData.flTitlesData1, + lineBarsData: [lineChartBarData1, lineChartBarData2, lineChartBarData3], + betweenBarsData: [betweenBarsData1, betweenBarsData2, betweenBarsData3], + extraLinesData: extraLinesData1, + maxX: 23, + minX: 11, + minY: 43, +); +final LineChartData lineChartData9 = LineChartData( + borderData: borderData1, + lineTouchData: lineTouchData1, + showingTooltipIndicators: [ + showingTooltipIndicator1, + showingTooltipIndicator2, + ], + backgroundColor: Colors.red.withValues(alpha: 0.2), + maxY: 23, + rangeAnnotations: rangeAnnotations1, + gridData: flGridData1, + titlesData: MockData.flTitlesData1, + lineBarsData: [lineChartBarData1, lineChartBarData2, lineChartBarData3], + betweenBarsData: [betweenBarsData1, betweenBarsData2, betweenBarsData3], + extraLinesData: extraLinesData1, + maxX: 23, + minX: 11, + minY: 43, +); +final LineChartData lineChartData10 = LineChartData( + borderData: borderData1, + lineTouchData: lineTouchData1, + showingTooltipIndicators: [ + showingTooltipIndicator1, + showingTooltipIndicator2, + ], + backgroundColor: Colors.red, + maxY: 24, + rangeAnnotations: rangeAnnotations1, + gridData: flGridData1, + titlesData: MockData.flTitlesData1, + lineBarsData: [lineChartBarData1, lineChartBarData2, lineChartBarData3], + betweenBarsData: [betweenBarsData1, betweenBarsData2, betweenBarsData3], + extraLinesData: extraLinesData1, + maxX: 23, + minX: 11, + minY: 43, +); +final LineChartData lineChartData11 = LineChartData( + borderData: borderData1, + lineTouchData: lineTouchData1, + showingTooltipIndicators: [ + showingTooltipIndicator1, + showingTooltipIndicator2, + ], + backgroundColor: Colors.red, + maxY: 23, + gridData: flGridData1, + titlesData: MockData.flTitlesData1, + lineBarsData: [lineChartBarData1, lineChartBarData2, lineChartBarData3], + betweenBarsData: [betweenBarsData1, betweenBarsData2, betweenBarsData3], + extraLinesData: extraLinesData1, + maxX: 23, + minX: 11, + minY: 43, +); +final LineChartData lineChartData12 = LineChartData( + borderData: borderData1, + lineTouchData: lineTouchData1, + showingTooltipIndicators: [ + showingTooltipIndicator1, + showingTooltipIndicator2, + ], + backgroundColor: Colors.red, + maxY: 23, + rangeAnnotations: rangeAnnotations1, + gridData: flGridData2, + titlesData: MockData.flTitlesData1, + lineBarsData: [lineChartBarData1, lineChartBarData2, lineChartBarData3], + betweenBarsData: [betweenBarsData1, betweenBarsData2, betweenBarsData3], + extraLinesData: extraLinesData1, + maxX: 23, + minX: 11, + minY: 43, +); +final LineChartData lineChartData13 = LineChartData( + borderData: borderData1, + lineTouchData: lineTouchData1, + showingTooltipIndicators: [ + showingTooltipIndicator1, + showingTooltipIndicator2, + ], + backgroundColor: Colors.red, + maxY: 23, + rangeAnnotations: rangeAnnotations1, + gridData: flGridData1, + titlesData: MockData.flTitlesData3, + lineBarsData: [lineChartBarData1, lineChartBarData2, lineChartBarData3], + betweenBarsData: [betweenBarsData1, betweenBarsData2, betweenBarsData3], + extraLinesData: extraLinesData1, + maxX: 23, + minX: 11, + minY: 43, +); +final LineChartData lineChartData14 = LineChartData( + borderData: borderData1, + lineTouchData: lineTouchData1, + showingTooltipIndicators: [ + showingTooltipIndicator1, + showingTooltipIndicator2, + ], + backgroundColor: Colors.red, + maxY: 23, + rangeAnnotations: rangeAnnotations1, + gridData: flGridData1, + titlesData: MockData.flTitlesData1, + lineBarsData: [lineChartBarData2, lineChartBarData3, lineChartBarData1], + betweenBarsData: [betweenBarsData1, betweenBarsData2, betweenBarsData3], + extraLinesData: extraLinesData1, + maxX: 23, + minX: 11, + minY: 43, +); +final LineChartData lineChartData15 = LineChartData( + borderData: borderData1, + lineTouchData: lineTouchData1, + showingTooltipIndicators: [ + showingTooltipIndicator1, + showingTooltipIndicator2, + ], + backgroundColor: Colors.red, + maxY: 23, + rangeAnnotations: rangeAnnotations1, + gridData: flGridData1, + titlesData: MockData.flTitlesData1, + betweenBarsData: [betweenBarsData1, betweenBarsData2, betweenBarsData3], + extraLinesData: extraLinesData1, + maxX: 23, + minX: 11, + minY: 43, +); +final LineChartData lineChartData16 = LineChartData( + borderData: borderData1, + lineTouchData: lineTouchData1, + showingTooltipIndicators: [ + showingTooltipIndicator1, + showingTooltipIndicator2, + ], + backgroundColor: Colors.red, + maxY: 23, + rangeAnnotations: rangeAnnotations1, + gridData: flGridData1, + titlesData: MockData.flTitlesData1, + lineBarsData: [lineChartBarData1, lineChartBarData2, lineChartBarData3], + betweenBarsData: [betweenBarsData2, betweenBarsData3, betweenBarsData1], + extraLinesData: extraLinesData1, + maxX: 23, + minX: 11, + minY: 43, +); +final LineChartData lineChartData17 = LineChartData( + borderData: borderData1, + lineTouchData: lineTouchData1, + showingTooltipIndicators: [ + showingTooltipIndicator1, + showingTooltipIndicator2, + ], + backgroundColor: Colors.red, + maxY: 23, + rangeAnnotations: rangeAnnotations1, + gridData: flGridData1, + titlesData: MockData.flTitlesData1, + lineBarsData: [lineChartBarData1, lineChartBarData2, lineChartBarData3], + betweenBarsData: [betweenBarsData1, betweenBarsData2, betweenBarsData3], + extraLinesData: extraLinesData2, + maxX: 23, + minX: 11, + minY: 43, +); +final LineChartData lineChartData18 = LineChartData( + borderData: borderData1, + lineTouchData: lineTouchData1, + showingTooltipIndicators: [ + showingTooltipIndicator1, + showingTooltipIndicator2, + ], + backgroundColor: Colors.red, + maxY: 23, + rangeAnnotations: rangeAnnotations1, + gridData: flGridData1, + titlesData: MockData.flTitlesData1, + lineBarsData: [lineChartBarData1, lineChartBarData2, lineChartBarData3], + betweenBarsData: [betweenBarsData1, betweenBarsData2, betweenBarsData3], + extraLinesData: extraLinesData1, + maxX: 23.01, + minX: 11, + minY: 43, +); +final LineChartData lineChartData19 = LineChartData( + borderData: borderData1, + lineTouchData: lineTouchData1, + showingTooltipIndicators: [ + showingTooltipIndicator1, + showingTooltipIndicator2, + ], + backgroundColor: Colors.red, + maxY: 23, + rangeAnnotations: rangeAnnotations1, + gridData: flGridData1, + titlesData: MockData.flTitlesData1, + lineBarsData: [lineChartBarData1, lineChartBarData2, lineChartBarData3], + betweenBarsData: [betweenBarsData1, betweenBarsData2, betweenBarsData3], + extraLinesData: extraLinesData1, + maxX: 23, + minX: 44, + minY: 43, +); +final LineChartData lineChartData20 = LineChartData( + borderData: borderData1, + lineTouchData: lineTouchData1, + showingTooltipIndicators: [ + showingTooltipIndicator1, + showingTooltipIndicator2, + ], + backgroundColor: Colors.red, + maxY: 23, + rangeAnnotations: rangeAnnotations1, + gridData: flGridData1, + titlesData: MockData.flTitlesData1, + lineBarsData: [lineChartBarData1, lineChartBarData2, lineChartBarData3], + betweenBarsData: [betweenBarsData1, betweenBarsData2, betweenBarsData3], + extraLinesData: extraLinesData1, + maxX: 23, + minX: 11, + minY: 302, +); +final LineChartData lineChartData21 = LineChartData( + borderData: borderData1, + rotationQuarterTurns: 1, + lineTouchData: lineTouchData1, + showingTooltipIndicators: [ + showingTooltipIndicator1, + showingTooltipIndicator2, + ], + backgroundColor: Colors.red, + maxY: 23, + rangeAnnotations: rangeAnnotations1, + gridData: flGridData1, + titlesData: MockData.flTitlesData1, + lineBarsData: [lineChartBarData1, lineChartBarData2, lineChartBarData3], + betweenBarsData: [betweenBarsData1, betweenBarsData2, betweenBarsData3], + extraLinesData: extraLinesData1, + maxX: 23, + minX: 11, + minY: 302, +); + +final PieChartData pieChartData1 = PieChartData( + borderData: FlBorderData(show: false, border: Border.all()), + startDegreeOffset: 0, + sections: [ + PieChartSectionData(value: 12, color: Colors.red), + PieChartSectionData(value: 22, color: Colors.green), + ], + centerSpaceColor: Colors.white, + centerSpaceRadius: 12, + pieTouchData: PieTouchData( + enabled: false, + ), + sectionsSpace: 44, + titleSunbeamLayout: false, +); +final PieChartData pieChartData1Clone = pieChartData1.copyWith(); + +bool gridCheckToShowLine(double value) => true; + +FlLine gridGetDrawingLine(double value) => const FlLine(); + +ScatterTooltipItem? scatterChartGetTooltipItems(ScatterSpot spots) { + return ScatterTooltipItem( + 'check', + textStyle: const TextStyle(color: Colors.blue), + bottomMargin: 23, + ); +} + +Color scatterChartGetTooltipGreenColor(ScatterSpot spots) { + return Colors.green; //Color +} + +Color scatterChartGetTooltipRedColor(ScatterSpot spots) { + return Colors.red; //Color +} + +final ScatterSpot scatterSpot1 = ScatterSpot(1, 40); +final ScatterSpot scatterSpot1Clone = ScatterSpot(1, 40); +final ScatterSpot scatterSpot2 = ScatterSpot(-4, -8); +final ScatterSpot scatterSpot2Clone = scatterSpot2.copyWith(); +final ScatterSpot scatterSpot3 = ScatterSpot(-14, 5); +final ScatterSpot scatterSpot4 = ScatterSpot(-0, 0); + +String getLabel(int spotIndex, ScatterSpot spot) => 'label'; + +TextStyle getLabelTextStyle(int spotIndex, ScatterSpot spot) => + const TextStyle(color: Colors.green); + +final ScatterChartData scatterChartData1 = ScatterChartData( + minY: 0, + maxY: 12, + maxX: 22, + minX: 11, + gridData: const FlGridData( + show: false, + getDrawingHorizontalLine: gridGetDrawingLine, + getDrawingVerticalLine: gridGetDrawingLine, + checkToShowHorizontalLine: gridCheckToShowLine, + checkToShowVerticalLine: gridCheckToShowLine, + drawVerticalLine: false, + horizontalInterval: 33, + verticalInterval: 1, + ), + backgroundColor: Colors.black, + clipData: const FlClipData.none(), + borderData: FlBorderData( + show: true, + border: Border.all( + color: Colors.white, + ), + ), + scatterSpots: [ + ScatterSpot( + 0, + 0, + show: false, + dotPainter: FlDotCirclePainter(radius: 33, color: Colors.yellow), + ), + ScatterSpot( + 2, + 2, + show: false, + renderPriority: 10, + dotPainter: FlDotCirclePainter(radius: 11, color: Colors.purple), + ), + ScatterSpot( + 1, + 2, + show: false, + renderPriority: -1, + dotPainter: FlDotCirclePainter(radius: 11, color: Colors.white), + ), + ], + scatterTouchData: ScatterTouchData( + enabled: true, + touchTooltipData: ScatterTouchTooltipData( + getTooltipItems: scatterChartGetTooltipItems, + fitInsideHorizontally: true, + fitInsideVertically: false, + maxContentWidth: 33, + getTooltipColor: (touchedSpot) => Colors.white, + tooltipPadding: const EdgeInsets.all(23), + tooltipBorderRadius: BorderRadius.circular(534), + ), + handleBuiltInTouches: false, + touchCallback: scatterTouchCallback, + touchSpotThreshold: 12, + ), + showingTooltipIndicators: [0, 1, 2], + titlesData: const FlTitlesData( + leftTitles: AxisTitles( + axisNameSize: 33, + axisNameWidget: MockData.widget1, + ), + rightTitles: AxisTitles( + axisNameSize: 1326, + axisNameWidget: MockData.widget3, + sideTitles: SideTitles(reservedSize: 500, showTitles: true), + ), + topTitles: AxisTitles( + axisNameSize: 34, + axisNameWidget: MockData.widget4, + ), + bottomTitles: AxisTitles( + axisNameSize: 22, + axisNameWidget: MockData.widget2, + ), + ), + scatterLabelSettings: ScatterLabelSettings( + showLabel: true, + getLabelTextStyleFunction: getLabelTextStyle, + getLabelFunction: getLabel, + ), +); +final scatterChartData1Clone = scatterChartData1.copyWith(); +final scatterTouchTooltipData1 = ScatterTouchTooltipData( + tooltipBorderRadius: BorderRadius.circular(23), + tooltipPadding: const EdgeInsets.all(11), + getTooltipColor: scatterChartGetTooltipGreenColor, + maxContentWidth: 33, + fitInsideVertically: true, + fitInsideHorizontally: false, + getTooltipItems: scatterChartGetTooltipItems, + tooltipBorder: const BorderSide(color: Colors.red), +); +final scatterTouchTooltipData1Clone = ScatterTouchTooltipData( + tooltipBorderRadius: BorderRadius.circular(23), + tooltipPadding: const EdgeInsets.all(11), + getTooltipColor: scatterChartGetTooltipGreenColor, + maxContentWidth: 33, + fitInsideVertically: true, + fitInsideHorizontally: false, + getTooltipItems: scatterChartGetTooltipItems, + tooltipBorder: const BorderSide(color: Colors.red), +); +final scatterTouchTooltipData2 = ScatterTouchTooltipData( + tooltipBorderRadius: BorderRadius.circular(23), + tooltipPadding: const EdgeInsets.all(11), + getTooltipColor: scatterChartGetTooltipGreenColor, + maxContentWidth: 33, + fitInsideVertically: true, + fitInsideHorizontally: false, + getTooltipItems: scatterChartGetTooltipItems, + tooltipBorder: const BorderSide(color: Colors.blue), + tooltipHorizontalAlignment: FLHorizontalAlignment.left, +); +final scatterTouchTooltipData3 = ScatterTouchTooltipData( + tooltipBorderRadius: BorderRadius.circular(23), + tooltipPadding: const EdgeInsets.all(11), + getTooltipColor: scatterChartGetTooltipGreenColor, + maxContentWidth: 33, + fitInsideVertically: true, + fitInsideHorizontally: false, + getTooltipItems: scatterChartGetTooltipItems, + tooltipBorder: const BorderSide(color: Colors.red, width: 2), + tooltipHorizontalAlignment: FLHorizontalAlignment.right, + tooltipHorizontalOffset: 10, +); + +final BarChartRodStackItem barChartRodStackItem1 = BarChartRodStackItem( + 1, + 2, + Colors.green, +); +final BarChartRodStackItem barChartRodStackItem1Clone = + barChartRodStackItem1.copyWith(); + +final BarChartRodStackItem barChartRodStackItem2 = BarChartRodStackItem( + 2, + 3, + Colors.green, +); + +final BackgroundBarChartRodData backgroundBarChartRodData1 = + BackgroundBarChartRodData( + toY: 21, + color: Colors.blue, + show: true, +); +final BackgroundBarChartRodData backgroundBarChartRodData1Clone = + BackgroundBarChartRodData( + toY: 21, + color: Colors.blue, + show: true, +); +final BackgroundBarChartRodData backgroundBarChartRodData2 = + BackgroundBarChartRodData( + toY: 44, + color: Colors.red, + show: true, +); +final BackgroundBarChartRodData backgroundBarChartRodData3 = + BackgroundBarChartRodData( + toY: 44, + color: Colors.green, + show: true, +); + +final BarChartRodData barChartRodData1 = BarChartRodData( + color: Colors.red, + toY: 12, + width: 32, + borderRadius: const BorderRadius.all(Radius.circular(12)), + rodStackItems: [ + barChartRodStackItem1, + barChartRodStackItem2, + ], + backDrawRodData: backgroundBarChartRodData1, +); + +final BarChartRodData barChartRodData1Clone = barChartRodData1.copyWith( + rodStackItems: [ + barChartRodStackItem1Clone, + barChartRodStackItem2, + ], +); + +final BarChartRodData barChartRodData2 = BarChartRodData( + color: Colors.red, + toY: 1132, + width: 32, + borderRadius: const BorderRadius.all(Radius.circular(12)), + rodStackItems: [ + barChartRodStackItem1, + barChartRodStackItem2, + ], + backDrawRodData: backgroundBarChartRodData1, +); +final BarChartRodData barChartRodData3 = BarChartRodData( + color: Colors.green, + toY: 12, + width: 32, + borderRadius: const BorderRadius.all(Radius.circular(12)), + rodStackItems: [ + barChartRodStackItem2, + ], + backDrawRodData: backgroundBarChartRodData1, +); +final BarChartRodData barChartRodData4 = BarChartRodData( + color: Colors.red, + toY: 12, + width: 32, + borderRadius: const BorderRadius.all(Radius.circular(12)), + rodStackItems: [ + barChartRodStackItem2, + barChartRodStackItem1, + ], + backDrawRodData: backgroundBarChartRodData1, +); +final BarChartRodData barChartRodData5 = BarChartRodData( + color: Colors.red, + toY: 12, + width: 55, + borderRadius: const BorderRadius.all(Radius.circular(12)), + rodStackItems: [ + barChartRodStackItem1, + barChartRodStackItem2, + ], + backDrawRodData: backgroundBarChartRodData1, +); +final BarChartRodData barChartRodData6 = BarChartRodData( + color: Colors.red, + toY: 12, + width: 32, + borderRadius: const BorderRadius.all(Radius.circular(12)), + backDrawRodData: backgroundBarChartRodData1, +); +final BarChartRodData barChartRodData7 = BarChartRodData( + color: Colors.red, + toY: 12, + width: 32, + borderRadius: const BorderRadius.all(Radius.circular(12)), + rodStackItems: [ + barChartRodStackItem1, + barChartRodStackItem2, + ], + backDrawRodData: backgroundBarChartRodData2, +); +final BarChartRodData barChartRodData8 = BarChartRodData( + color: Colors.red, + toY: 12, + width: 32, + borderRadius: const BorderRadius.all(Radius.circular(14)), + rodStackItems: [ + barChartRodStackItem1, + barChartRodStackItem2, + ], + backDrawRodData: backgroundBarChartRodData1, +); + +final BarChartGroupData barChartGroupData1 = BarChartGroupData( + x: 12, + showingTooltipIndicators: [0, 1, 2], + barRods: [ + barChartRodData1, + barChartRodData2, + barChartRodData3, + barChartRodData4, + ], + barsSpace: 23, +); +final BarChartGroupData barChartGroupData1Clone = BarChartGroupData( + x: 12, + showingTooltipIndicators: [0, 1, 2], + barRods: [ + barChartRodData1Clone, + barChartRodData2, + barChartRodData3, + barChartRodData4, + ], + barsSpace: 23, +); +final BarChartGroupData barChartGroupData2 = BarChartGroupData( + x: 13, + showingTooltipIndicators: [0, 1, 2], + barRods: [ + barChartRodData1, + barChartRodData2, + barChartRodData3, + barChartRodData4, + ], + barsSpace: 23, +); +final BarChartGroupData barChartGroupData3 = BarChartGroupData( + x: 12, + showingTooltipIndicators: [0, 1], + barRods: [ + barChartRodData1, + barChartRodData2, + barChartRodData3, + barChartRodData4, + ], + barsSpace: 23, +); +final BarChartGroupData barChartGroupData4 = BarChartGroupData( + x: 12, + showingTooltipIndicators: [0, 1, 2], + barRods: [ + barChartRodData1, + barChartRodData2, + barChartRodData4, + ], + barsSpace: 23, +); +final BarChartGroupData barChartGroupData5 = BarChartGroupData( + x: 12, + showingTooltipIndicators: [0, 1, 2], + barsSpace: 23, +); +final BarChartGroupData barChartGroupData6 = BarChartGroupData( + x: 12, + showingTooltipIndicators: [0, 1, 2], + barRods: [ + barChartRodData2, + barChartRodData3, + barChartRodData4, + barChartRodData1, + ], + barsSpace: 23, +); +final BarChartGroupData barChartGroupData7 = BarChartGroupData( + x: 12, + showingTooltipIndicators: [0, 1, 2], + barRods: [ + barChartRodData1, + barChartRodData2, + barChartRodData3, + barChartRodData4, + ], + barsSpace: 44, +); +final BarChartGroupData barChartGroupData8 = BarChartGroupData( + x: 12, + barRods: [ + barChartRodData1, + barChartRodData2, + barChartRodData3, + barChartRodData4, + ], + barsSpace: 23, +); +final BarChartGroupData barChartGroupData9 = BarChartGroupData( + x: 12, + showingTooltipIndicators: [0, 1, 2], + barRods: [ + barChartRodData1, + barChartRodData2, + barChartRodData3, + barChartRodData4, + ], + barsSpace: 0, +); + +final BarTouchedSpot barTouchedSpot1 = BarTouchedSpot( + barChartGroupData1, + 1, + barChartRodData1, + 2, + barChartRodStackItem1, + 1, + flSpot1, + Offset.zero, +); +final BarTouchedSpot barTouchedSpot1Clone = BarTouchedSpot( + barChartGroupData1Clone, + 1, + barChartRodData1Clone, + 2, + barChartRodStackItem1Clone, + 1, + flSpot1Clone, + Offset.zero, +); +final BarTouchedSpot barTouchedSpot2 = BarTouchedSpot( + barChartGroupData2, + 1, + barChartRodData1, + 2, + barChartRodStackItem2, + 2, + flSpot1, + Offset.zero, +); +final BarTouchedSpot barTouchedSpot3 = BarTouchedSpot( + barChartGroupData1, + 1, + barChartRodData2, + 2, + barChartRodStackItem2, + 2, + flSpot1, + Offset.zero, +); +final BarTouchedSpot barTouchedSpot4 = BarTouchedSpot( + barChartGroupData1, + 2, + barChartRodData1, + 2, + barChartRodStackItem2, + 2, + flSpot1, + Offset.zero, +); +final BarTouchedSpot barTouchedSpot5 = BarTouchedSpot( + barChartGroupData1, + 1, + barChartRodData1, + 3, + barChartRodStackItem2, + 2, + flSpot1, + Offset.zero, +); +final BarTouchedSpot barTouchedSpot6 = BarTouchedSpot( + barChartGroupData1, + 1, + barChartRodData1, + 2, + barChartRodStackItem2, + 2, + flSpot2, + Offset.zero, +); +final BarTouchedSpot barTouchedSpot7 = BarTouchedSpot( + barChartGroupData1, + 1, + barChartRodData1, + 2, + barChartRodStackItem2, + 2, + flSpot1, + const Offset(1, 10), +); + +final barTouchResponse1 = BarTouchResponse( + touchLocation: Offset.zero, + touchChartCoordinate: Offset.zero, + spot: barTouchedSpot1, +); +final barTouchResponse1Clone = BarTouchResponse( + touchLocation: Offset.zero, + touchChartCoordinate: Offset.zero, + spot: barTouchedSpot1Clone, +); +final barTouchResponse2 = BarTouchResponse( + touchLocation: Offset.zero, + touchChartCoordinate: Offset.zero, + spot: barTouchedSpot2, +); + +final BarTooltipItem barTooltipItem1 = BarTooltipItem( + 'pashmam 1', + const TextStyle(color: Colors.red), +); +final BarTooltipItem barTooltipItem1Clone = BarTooltipItem( + 'pashmam 1', + const TextStyle(color: Colors.red), +); +final BarTooltipItem barTooltipItem2 = BarTooltipItem( + 'pashmam 2', + const TextStyle(color: Colors.red), +); +final BarTooltipItem barTooltipItem3 = BarTooltipItem( + 'pashmam 1', + const TextStyle(color: Colors.green), +); +final BarTooltipItem barTooltipItem4 = BarTooltipItem( + 'null', + const TextStyle(color: Colors.red), +); +final BarTooltipItem barTooltipItem5 = BarTooltipItem( + 'pashmam 1', + const TextStyle(fontSize: 85), +); + +BarTooltipItem getTooltipItem( + BarChartGroupData group, + int groupIndex, + BarChartRodData rod, + int rodIndex, +) { + const textStyle = TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: 14, + ); + return BarTooltipItem(rod.toY.toString(), textStyle); +} + +Color getTooltipGreenColor( + BarChartGroupData group, +) { + return Colors.green; +} + +Color getTooltipBlueColor( + BarChartGroupData group, +) { + return Colors.blue; +} + +final BarTouchTooltipData barTouchTooltipData1 = BarTouchTooltipData( + tooltipBorderRadius: BorderRadius.circular(12), + fitInsideVertically: false, + fitInsideHorizontally: true, + maxContentWidth: 23, + getTooltipColor: getTooltipGreenColor, + tooltipPadding: const EdgeInsets.all(23), + getTooltipItem: getTooltipItem, + tooltipMargin: 12, + tooltipBorder: const BorderSide(color: Colors.red), +); +final BarTouchTooltipData barTouchTooltipData1Clone = BarTouchTooltipData( + tooltipBorderRadius: BorderRadius.circular(12), + fitInsideVertically: false, + fitInsideHorizontally: true, + maxContentWidth: 23, + getTooltipColor: getTooltipGreenColor, + tooltipPadding: const EdgeInsets.all(23), + getTooltipItem: getTooltipItem, + tooltipMargin: 12, + tooltipBorder: const BorderSide(color: Colors.red), +); +final BarTouchTooltipData barTouchTooltipData2 = BarTouchTooltipData( + tooltipBorderRadius: BorderRadius.circular(13), + fitInsideVertically: false, + fitInsideHorizontally: true, + maxContentWidth: 23, + getTooltipColor: getTooltipGreenColor, + tooltipPadding: const EdgeInsets.all(23), + getTooltipItem: getTooltipItem, + tooltipMargin: 12, + tooltipBorder: const BorderSide(color: Colors.red), + tooltipHorizontalAlignment: FLHorizontalAlignment.center, +); +final BarTouchTooltipData barTouchTooltipData3 = BarTouchTooltipData( + tooltipBorderRadius: BorderRadius.circular(12), + fitInsideVertically: true, + fitInsideHorizontally: true, + maxContentWidth: 23, + getTooltipColor: getTooltipGreenColor, + tooltipPadding: const EdgeInsets.all(23), + getTooltipItem: getTooltipItem, + tooltipMargin: 12, + tooltipBorder: const BorderSide(color: Colors.red), + tooltipHorizontalAlignment: FLHorizontalAlignment.left, +); +final BarTouchTooltipData barTouchTooltipData4 = BarTouchTooltipData( + tooltipBorderRadius: BorderRadius.circular(12), + fitInsideVertically: false, + fitInsideHorizontally: false, + maxContentWidth: 23, + getTooltipColor: getTooltipGreenColor, + tooltipPadding: const EdgeInsets.all(23), + getTooltipItem: getTooltipItem, + tooltipMargin: 12, + tooltipBorder: const BorderSide(color: Colors.red), + tooltipHorizontalAlignment: FLHorizontalAlignment.right, +); +final BarTouchTooltipData barTouchTooltipData5 = BarTouchTooltipData( + tooltipBorderRadius: BorderRadius.circular(12), + fitInsideVertically: false, + fitInsideHorizontally: true, + maxContentWidth: 23.00001, + getTooltipColor: getTooltipGreenColor, + tooltipPadding: const EdgeInsets.all(23), + getTooltipItem: getTooltipItem, + tooltipMargin: 12, + tooltipBorder: const BorderSide(color: Colors.red), + tooltipHorizontalAlignment: FLHorizontalAlignment.center, + tooltipHorizontalOffset: 10, +); +final BarTouchTooltipData barTouchTooltipData6 = BarTouchTooltipData( + tooltipBorderRadius: BorderRadius.circular(12), + fitInsideVertically: false, + fitInsideHorizontally: true, + maxContentWidth: 23, + getTooltipColor: getTooltipBlueColor, + tooltipPadding: const EdgeInsets.all(23), + getTooltipItem: getTooltipItem, + tooltipMargin: 12, + tooltipBorder: const BorderSide(color: Colors.red), + tooltipHorizontalAlignment: FLHorizontalAlignment.left, + tooltipHorizontalOffset: -10, +); +final BarTouchTooltipData barTouchTooltipData7 = BarTouchTooltipData( + tooltipBorderRadius: BorderRadius.circular(12), + fitInsideVertically: false, + fitInsideHorizontally: true, + maxContentWidth: 23, + getTooltipColor: getTooltipGreenColor, + getTooltipItem: getTooltipItem, + tooltipMargin: 12, + tooltipBorder: const BorderSide(color: Colors.red), + tooltipHorizontalAlignment: FLHorizontalAlignment.right, + tooltipHorizontalOffset: 10, +); +final BarTouchTooltipData barTouchTooltipData8 = BarTouchTooltipData( + tooltipBorderRadius: BorderRadius.circular(12), + fitInsideVertically: false, + fitInsideHorizontally: true, + maxContentWidth: 23, + getTooltipColor: getTooltipGreenColor, + tooltipPadding: const EdgeInsets.all(23), + tooltipMargin: 12, + tooltipBorder: const BorderSide(color: Colors.red), +); +final BarTouchTooltipData barTouchTooltipData9 = BarTouchTooltipData( + tooltipBorderRadius: BorderRadius.circular(12), + fitInsideVertically: false, + fitInsideHorizontally: true, + maxContentWidth: 23, + getTooltipColor: getTooltipGreenColor, + tooltipPadding: const EdgeInsets.all(23), + getTooltipItem: getTooltipItem, + tooltipMargin: 333, + tooltipBorder: const BorderSide(color: Colors.red), +); +final BarTouchTooltipData barTouchTooltipData10 = BarTouchTooltipData( + tooltipBorderRadius: BorderRadius.circular(12), + fitInsideVertically: false, + fitInsideHorizontally: true, + maxContentWidth: 23, + getTooltipColor: getTooltipGreenColor, + tooltipPadding: const EdgeInsets.all(23), + getTooltipItem: getTooltipItem, + tooltipMargin: 12, + tooltipBorder: const BorderSide(color: Colors.blue), +); +final BarTouchTooltipData barTouchTooltipData11 = BarTouchTooltipData( + tooltipBorderRadius: BorderRadius.circular(12), + fitInsideVertically: false, + fitInsideHorizontally: true, + maxContentWidth: 23, + getTooltipColor: getTooltipGreenColor, + tooltipPadding: const EdgeInsets.all(23), + getTooltipItem: getTooltipItem, + tooltipMargin: 12, + tooltipBorder: const BorderSide(color: Colors.red, width: 2), +); + +void barTouchCallback(FlTouchEvent event, BarTouchResponse? response) {} + +void scatterTouchCallback(FlTouchEvent event, ScatterTouchResponse? response) {} + +final BarTouchData barTouchData1 = BarTouchData( + touchTooltipData: barTouchTooltipData1, + handleBuiltInTouches: false, + touchCallback: barTouchCallback, + enabled: false, + allowTouchBarBackDraw: true, + touchExtraThreshold: const EdgeInsets.all(12), +); +final BarTouchData barTouchData1Clone = BarTouchData( + touchTooltipData: barTouchTooltipData1Clone, + handleBuiltInTouches: false, + touchCallback: barTouchCallback, + enabled: false, + allowTouchBarBackDraw: true, + touchExtraThreshold: const EdgeInsets.all(12), +); +const BarTouchData barTouchData2 = BarTouchData( + handleBuiltInTouches: false, + touchCallback: barTouchCallback, + enabled: false, + allowTouchBarBackDraw: true, + touchExtraThreshold: EdgeInsets.all(12), +); +final BarTouchData barTouchData3 = BarTouchData( + touchTooltipData: barTouchTooltipData1, + handleBuiltInTouches: true, + touchCallback: barTouchCallback, + enabled: false, + allowTouchBarBackDraw: true, + touchExtraThreshold: const EdgeInsets.all(12), +); +final BarTouchData barTouchData4 = BarTouchData( + touchTooltipData: barTouchTooltipData2, + handleBuiltInTouches: false, + touchCallback: barTouchCallback, + enabled: false, + allowTouchBarBackDraw: true, + touchExtraThreshold: const EdgeInsets.all(12), +); +final BarTouchData barTouchData5 = BarTouchData( + touchTooltipData: barTouchTooltipData1, + handleBuiltInTouches: false, + enabled: false, + allowTouchBarBackDraw: true, + touchExtraThreshold: const EdgeInsets.all(12), +); +final BarTouchData barTouchData6 = BarTouchData( + touchTooltipData: barTouchTooltipData1, + handleBuiltInTouches: false, + touchCallback: barTouchCallback, + enabled: true, + allowTouchBarBackDraw: true, + touchExtraThreshold: const EdgeInsets.all(12), +); +final BarTouchData barTouchData7 = BarTouchData( + touchTooltipData: barTouchTooltipData1, + handleBuiltInTouches: false, + touchCallback: barTouchCallback, + enabled: false, + allowTouchBarBackDraw: false, + touchExtraThreshold: const EdgeInsets.all(12), +); +final BarTouchData barTouchData8 = BarTouchData( + touchTooltipData: barTouchTooltipData1, + handleBuiltInTouches: false, + touchCallback: barTouchCallback, + enabled: false, + allowTouchBarBackDraw: true, +); +final BarTouchData barTouchData9 = BarTouchData( + touchTooltipData: barTouchTooltipData1, + handleBuiltInTouches: false, + touchCallback: barTouchCallback, + enabled: false, + allowTouchBarBackDraw: true, + touchExtraThreshold: const EdgeInsets.only(left: 12), +); +final BarTouchData barTouchData10 = BarTouchData( + touchTooltipData: barTouchTooltipData1, + handleBuiltInTouches: false, + touchCallback: barTouchCallback, + enabled: false, + allowTouchBarBackDraw: true, + touchExtraThreshold: const EdgeInsets.all(12), + longPressDuration: Duration.zero, +); + +final BarChartData barChartData1 = BarChartData( + minY: 12, + titlesData: MockData.flTitlesData1, + gridData: flGridData1, + rangeAnnotations: rangeAnnotations1, + maxY: 23, + backgroundColor: Colors.green, + borderData: borderData1, + alignment: BarChartAlignment.spaceAround, + barGroups: [ + barChartGroupData1, + barChartGroupData2, + barChartGroupData3, + ], + barTouchData: barTouchData1, + groupsSpace: 23, + extraLinesData: extraLinesData1, +); +final BarChartData barChartData1Clone = barChartData1.copyWith( + titlesData: MockData.flTitlesData1Clone, + gridData: flGridData1Clone, + borderData: borderData1Clone, + barTouchData: barTouchData1Clone, + rangeAnnotations: rangeAnnotations1Clone, +); + +final BarChartData barChartData2 = barChartData1.copyWith( + minY: 11, +); +final BarChartData barChartData3 = barChartData1.copyWith( + titlesData: MockData.flTitlesData2, +); +final BarChartData barChartData4 = barChartData1.copyWith( + gridData: flGridData2, +); +final BarChartData barChartData5 = barChartData1.copyWith( + rangeAnnotations: rangeAnnotations2, +); +final BarChartData barChartData6 = barChartData1.copyWith( + maxY: 52345, +); +final BarChartData barChartData7 = barChartData1.copyWith( + backgroundColor: Colors.red, +); +final BarChartData barChartData8 = barChartData1.copyWith( + titlesData: MockData.flTitlesData3, +); +final BarChartData barChartData9 = barChartData1.copyWith( + borderData: borderData2, +); +final BarChartData barChartData10 = barChartData1.copyWith( + alignment: BarChartAlignment.center, +); +final BarChartData barChartData11 = barChartData1.copyWith( + barGroups: [], +); +final BarChartData barChartData12 = barChartData1.copyWith( + barGroups: [ + barChartGroupData3, + barChartGroupData1, + barChartGroupData2, + ], +); +final BarChartData barChartData13 = barChartData1.copyWith( + barTouchData: barTouchData2, +); +final BarChartData barChartData14 = barChartData1.copyWith( + groupsSpace: 444, +); +final BarChartData barChartData15 = barChartData1.copyWith( + extraLinesData: extraLinesData2, +); + +final RadarDataSet radarDataSet1 = RadarDataSet( + dataEntries: const [ + RadarEntry(value: 0), + RadarEntry(value: 1), + RadarEntry(value: 2), + RadarEntry(value: 3), + RadarEntry(value: 4), + ], + borderColor: Colors.blue, + borderWidth: 3, + entryRadius: 3, + fillColor: Colors.grey, +); + +final RadarDataSet radarDataSet1Clone = radarDataSet1.copyWith(); + +final RadarDataSet radarDataSet2 = RadarDataSet( + dataEntries: const [ + RadarEntry(value: 10), + RadarEntry(value: 9), + RadarEntry(value: 8), + RadarEntry(value: 7), + RadarEntry(value: 6), + ], + borderColor: Colors.red, + borderWidth: 5, + entryRadius: 5, + fillColor: Colors.black, +); + +final RadarTouchData radarTouchData1 = RadarTouchData( + enabled: true, + touchCallback: radarTouchCallback, + touchSpotThreshold: 12, +); + +final RadarTouchData radarTouchData2 = RadarTouchData( + enabled: false, + touchCallback: radarTouchCallback, + touchSpotThreshold: 5, +); + +final RadarTouchData radarTouchData1Clone = radarTouchData1; + +void radarTouchCallback(FlTouchEvent event, RadarTouchResponse? response) {} + +final radarTouchedSpot1 = RadarTouchedSpot( + radarDataSet1, + 0, + radarDataSet1.dataEntries.first, + 0, + flSpot1, + Offset.zero, +); + +final radarTouchedSpotClone1 = radarTouchedSpot1; + +final radarTouchedSpot2 = RadarTouchedSpot( + radarDataSet2, + 0, + radarDataSet1.dataEntries.first, + 0, + flSpot1, + Offset.zero, +); + +final radarTouchedSpot3 = RadarTouchedSpot( + radarDataSet1, + 1, + radarDataSet1.dataEntries.first, + 0, + flSpot1, + Offset.zero, +); + +final radarTouchedSpot4 = RadarTouchedSpot( + radarDataSet1, + 0, + radarDataSet1.dataEntries.last, + 0, + flSpot1, + Offset.zero, +); + +final radarTouchedSpot5 = RadarTouchedSpot( + radarDataSet1, + 0, + radarDataSet1.dataEntries.first, + 1, + flSpot1, + Offset.zero, +); + +final radarTouchedSpot6 = RadarTouchedSpot( + radarDataSet1, + 0, + radarDataSet1.dataEntries.first, + 0, + flSpot2, + Offset.zero, +); + +final radarTouchedSpot7 = RadarTouchedSpot( + radarDataSet1, + 0, + radarDataSet1.dataEntries.first, + 0, + flSpot1, + const Offset(3, 5), +); + +final RadarChartData radarChartData1 = RadarChartData( + dataSets: [radarDataSet1], + radarBackgroundColor: Colors.yellow, + radarBorderData: const BorderSide(color: Colors.purple, width: 5), + borderData: borderData1, + gridBorderData: const BorderSide(color: Colors.blue, width: 2), + getTitle: (index, angle) => RadarChartTitle(text: 'testTitle', angle: angle), + titlePositionPercentageOffset: 0.2, + titleTextStyle: const TextStyle(color: Colors.white, fontSize: 12), + radarTouchData: radarTouchData1, + tickCount: 5, + tickBorderData: const BorderSide(width: 4), + ticksTextStyle: const TextStyle(color: Colors.white, fontSize: 12), +); + +final RadarChartData radarChartData1Clone = radarChartData1.copyWith(); + +final RadarChartData radarChartData2 = RadarChartData( + dataSets: [radarDataSet2], + radarBackgroundColor: Colors.blue, + radarBorderData: const BorderSide(color: Colors.pink, width: 3), + borderData: borderData1, + gridBorderData: const BorderSide(color: Colors.red, width: 3), + getTitle: (index, angle) => RadarChartTitle(text: 'testTitle2', angle: angle), + titlePositionPercentageOffset: 0.5, + titleTextStyle: const TextStyle(color: Colors.black, fontSize: 5), + radarTouchData: radarTouchData2, + tickCount: 1, + tickBorderData: const BorderSide(color: Colors.pink, width: 2), + ticksTextStyle: const TextStyle(color: Colors.purple, fontSize: 10), +); + +const Line line1 = Line(Offset.zero, Offset(10, 10)); +const Line line2 = Line(Offset(-4, -12), Offset(6, 8)); +const Line line3 = Line(Offset(18, -1), Offset.zero); +const Line line4 = Line(Offset(8, 8), Offset(-4, -22)); +const Line line5 = Line(Offset(2, 3), Offset(5, 8)); + +const TextStyle textStyle1 = + TextStyle(color: Colors.red, fontWeight: FontWeight.bold); +const TextStyle textStyle2 = + TextStyle(color: Colors.green, fontWeight: FontWeight.w200); + +const TextSpan textSpan1 = TextSpan(text: 'faketext1', style: textStyle1); +const TextSpan textSpan2 = TextSpan(text: 'faketext2', style: textStyle2); + +final DefaultTextStyle defaultTextStyle1 = DefaultTextStyle( + style: const TextStyle(), + child: Container(), +); + +final candlestickTouchData1 = CandlestickTouchData( + enabled: false, +); + +final candlestickSpot1 = CandlestickSpot( + x: 0, + open: 10, + high: 100, + low: 0, + close: 20, +); + +final candlestickSpot1Clone = candlestickSpot1.copyWith(); + +final candlestickSpot2 = CandlestickSpot( + x: 10, + open: 30, + high: 110, + low: 10, + close: 20, +); + +final candlestickSpot2Clone = candlestickSpot2.copyWith(); + +final candlestickSpot3 = CandlestickSpot( + x: 20, + open: 30, + high: 120, + low: 20, + close: 40, +); + +final candlestickSpot4 = CandlestickSpot( + x: 30, + open: 40, + high: 130, + low: 30, + close: 50, +); + +final candlestickSpot5 = CandlestickSpot( + x: -50, + open: -40, + high: -130, + low: -30, + close: -50, +); + +final candleStickChartData1 = CandlestickChartData( + candlestickSpots: [ + candlestickSpot1, + candlestickSpot2, + candlestickSpot3, + candlestickSpot4, + ], + candlestickPainter: DefaultCandlestickPainter(), + titlesData: MockData.flTitlesData1, + candlestickTouchData: candlestickTouchData1, + showingTooltipIndicators: [0, 1, 2], + gridData: flGridData1, + borderData: borderData1, + minX: 0, + maxX: 1000, + minY: 0, + maxY: 1000, + baselineX: 0, + baselineY: 0, + backgroundColor: Colors.white, + rangeAnnotations: rangeAnnotations1, + clipData: const FlClipData.none(), +); + +final candleStickChartData1Clone = candleStickChartData1.copyWith(); + +final candleStickChartData2 = CandlestickChartData( + candlestickSpots: [ + candlestickSpot1, + candlestickSpot2, + candlestickSpot3, + candlestickSpot4, + ], + candlestickPainter: DefaultCandlestickPainter(), + titlesData: MockData.flTitlesData2, + candlestickTouchData: candlestickTouchData1, + showingTooltipIndicators: [1, 2, 3], + gridData: flGridData1, + borderData: borderData1, + minX: 0, + maxX: 1000, + minY: 0, + maxY: 1000, + baselineX: 0, + baselineY: 0, + backgroundColor: Colors.white, + rangeAnnotations: rangeAnnotations1, + clipData: const FlClipData.all(), + rotationQuarterTurns: 1, +); + +Color candlestickChartGetTooltipRedColor(CandlestickSpot spots) => Colors.red; + +Color candlestickChartGetTooltipGreenColor(CandlestickSpot spots) => + Colors.green; + +CandlestickTooltipItem? candlestickChartGetTooltipItems( + FlCandlestickPainter painter, + CandlestickSpot touchedSpot, + int spotIndex, +) { + return CandlestickTooltipItem( + 'check', + textStyle: const TextStyle(color: Colors.blue), + bottomMargin: 23, + ); +} + +final CandlestickTouchTooltipData candlestickTouchTooltipData1 = + CandlestickTouchTooltipData( + tooltipBorderRadius: const BorderRadius.all(Radius.circular(23)), + tooltipPadding: const EdgeInsets.all(11), + getTooltipColor: candlestickChartGetTooltipGreenColor, + maxContentWidth: 33, + fitInsideVertically: true, + fitInsideHorizontally: false, + getTooltipItems: candlestickChartGetTooltipItems, + tooltipBorder: const BorderSide(color: Colors.red), +); +final CandlestickTouchTooltipData candlestickTouchTooltipData1Clone = + CandlestickTouchTooltipData( + tooltipBorderRadius: const BorderRadius.all(Radius.circular(23)), + tooltipPadding: const EdgeInsets.all(11), + getTooltipColor: candlestickChartGetTooltipGreenColor, + maxContentWidth: 33, + fitInsideVertically: true, + fitInsideHorizontally: false, + getTooltipItems: candlestickChartGetTooltipItems, + tooltipBorder: const BorderSide(color: Colors.red), +); +final CandlestickTouchTooltipData candlestickTouchTooltipData2 = + CandlestickTouchTooltipData( + tooltipBorderRadius: const BorderRadius.all(Radius.circular(23)), + tooltipPadding: const EdgeInsets.all(11), + getTooltipColor: candlestickChartGetTooltipGreenColor, + maxContentWidth: 33, + fitInsideVertically: true, + fitInsideHorizontally: false, + getTooltipItems: candlestickChartGetTooltipItems, + tooltipBorder: const BorderSide(color: Colors.blue), + tooltipHorizontalAlignment: FLHorizontalAlignment.left, +); +final CandlestickTouchTooltipData candlestickTouchTooltipData3 = + CandlestickTouchTooltipData( + tooltipBorderRadius: const BorderRadius.all(Radius.circular(23)), + tooltipPadding: const EdgeInsets.all(11), + getTooltipColor: candlestickChartGetTooltipGreenColor, + maxContentWidth: 33, + fitInsideVertically: true, + fitInsideHorizontally: false, + getTooltipItems: candlestickChartGetTooltipItems, + tooltipBorder: const BorderSide(color: Colors.red, width: 2), + tooltipHorizontalAlignment: FLHorizontalAlignment.right, + tooltipHorizontalOffset: 10, +); + +final candlestickTouchedSpot1 = CandlestickTouchedSpot(candlestickSpot1, 0); diff --git a/test/chart/line_chart/line_chart_data_test.dart b/test/chart/line_chart/line_chart_data_test.dart new file mode 100644 index 0000000..15c8435 --- /dev/null +++ b/test/chart/line_chart/line_chart_data_test.dart @@ -0,0 +1,223 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../data_pool.dart'; + +void main() { + group('LineChart data equality check', () { + test('LineChartBarData equality test', () { + expect(lineChartBarData1 == lineChartBarData1Clone, true); + expect(lineChartBarData1 == lineChartBarData2, false); + expect(lineChartBarData1 == lineChartBarData3, false); + expect(lineChartBarData1 == lineChartBarData4, false); + expect(lineChartBarData1 == lineChartBarData5, false); + expect(lineChartBarData1 == lineChartBarData6, false); + expect(lineChartBarData1 == lineChartBarData7, false); + expect(lineChartBarData1 == lineChartBarData8, false); + expect(lineChartBarData1 == lineChartBarData9, false); + }); + + test('LineChartBarData late init values test', () { + final bar = LineChartBarData( + spots: const [ + FlSpot(1, 1), + FlSpot(2, 4), + FlSpot(-3, 8), + FlSpot(2, -5), + FlSpot(9, 0), + ], + ); + expect(bar.mostLeftSpot, const FlSpot(-3, 8)); + expect(bar.mostTopSpot, const FlSpot(-3, 8)); + expect(bar.mostRightSpot, const FlSpot(9, 0)); + expect(bar.mostBottomSpot, const FlSpot(2, -5)); + }); + + test('BarAreaData equality test', () { + expect(barAreaData1 == barAreaData1Clone, true); + + expect(barAreaData1 == barAreaData2, false); + + expect(barAreaData1 == barAreaData3, false); + + expect(barAreaData1 == barAreaData4, false); + }); + + test('BetweenBarsData equality test', () { + expect(betweenBarsData1 == betweenBarsData1Clone, true); + expect(betweenBarsData1 == betweenBarsData2, false); + expect(betweenBarsData1 == betweenBarsData3, false); + expect(betweenBarsData1 == betweenBarsData4, false); + expect(betweenBarsData1 == betweenBarsData5, false); + expect(betweenBarsData1 == betweenBarsData6, false); + expect(betweenBarsData1 == betweenBarsData7, false); + expect(betweenBarsData1 == betweenBarsData8, false); + }); + + test('BarAreaSpotsLine equality test', () { + expect(barAreaSpotsLine1 == barAreaSpotsLine1Clone, true); + + expect(barAreaSpotsLine1 == barAreaSpotsLine2, false); + }); + + test('FlDotData equality test', () { + expect(flDotData1 == flDotData1Clone, true); + + expect(flDotData1 == flDotData4, false); + + expect(flDotData1 == flDotData5, false); + + expect(flDotData1 == flDotData6, false); + }); + + test('HorizontalLine equality test', () { + expect(horizontalLine1 == horizontalLine1Clone, true); + expect(horizontalLine1 == horizontalLine2, false); + expect(horizontalLine1 == horizontalLine3, false); + expect(horizontalLine1 == horizontalLine4, false); + expect(horizontalLine1 == horizontalLine5, false); + expect(horizontalLine1 == horizontalLine6, false); + expect(horizontalLine1 == horizontalLine7, false); + expect(horizontalLine1 == horizontalLine8, false); + expect(horizontalLine1 == horizontalLine9, false); + }); + + test('VerticalLine equality test', () { + expect(verticalLine1 == verticalLine1Clone, true); + expect(verticalLine1 == verticalLine2, false); + expect(verticalLine1 == verticalLine3, false); + expect(verticalLine1 == verticalLine4, false); + expect(verticalLine1 == verticalLine5, false); + expect(verticalLine1 == verticalLine6, false); + expect(verticalLine1 == verticalLine7, false); + expect(verticalLine1 == verticalLine8, false); + expect(verticalLine1 == verticalLine9, false); + }); + + test('HorizontalLineLabel equality test', () { + expect(horizontalLineLabel1 == horizontalLineLabel1Clone, true); + expect(horizontalLineLabel1 == horizontalLineLabel2, false); + expect(horizontalLineLabel1 == horizontalLineLabel3, false); + expect(horizontalLineLabel1 == horizontalLineLabel4, false); + expect(horizontalLineLabel1 == horizontalLineLabel5, false); + expect(horizontalLineLabel1 == horizontalLineLabel6, false); + expect(horizontalLineLabel1 == horizontalLineLabel7, false); + }); + + test('VerticalLineLabel equality test', () { + expect(verticalLineLabel1 == verticalLineLabel1Clone, true); + expect(verticalLineLabel1 == verticalLineLabel2, false); + expect(verticalLineLabel1 == verticalLineLabel3, false); + expect(verticalLineLabel1 == verticalLineLabel4, false); + expect(verticalLineLabel1 == verticalLineLabel5, false); + expect(verticalLineLabel1 == verticalLineLabel6, false); + expect(verticalLineLabel1 == verticalLineLabel7, false); + }); + + test('ExtraLinesData equality test', () { + expect(extraLinesData1 == extraLinesData1Clone, true); + expect(extraLinesData1 == extraLinesData2, false); + expect(extraLinesData1 == extraLinesData3, false); + expect(extraLinesData1 == extraLinesData4, false); + expect(extraLinesData1 == extraLinesData5, false); + expect(extraLinesData1 == extraLinesData6, false); + }); + + test('LineTouchData equality test', () { + expect(lineTouchData1 == lineTouchData1Clone, true); + expect(lineTouchData1 == lineTouchData2, false); + expect(lineTouchData1 == lineTouchData3, false); + expect(lineTouchData1 == lineTouchData4, false); + expect(lineTouchData1 == lineTouchData5, false); + expect(lineTouchData1 == lineTouchData6, false); + expect(lineTouchData1 == lineTouchData7, false); + expect(lineTouchData1 == lineTouchData8, false); + }); + + test('LineTouchTooltipData equality test', () { + expect(lineTouchTooltipData1 == lineTouchTooltipData1Clone, true); + expect(lineTouchTooltipData1 == lineTouchTooltipData2, false); + expect(lineTouchTooltipData1 == lineTouchTooltipData3, false); + expect(lineTouchTooltipData1 == lineTouchTooltipData4, false); + expect(lineTouchTooltipData1 == lineTouchTooltipData5, false); + expect(lineTouchTooltipData1 == lineTouchTooltipData6, false); + expect(lineTouchTooltipData1 == lineTouchTooltipData7, false); + }); + + test('LineBarSpot equality test', () { + expect(lineBarSpot1 == lineBarSpot1Clone, true); + expect(lineBarSpot1 == lineBarSpot2, false); + expect(lineBarSpot1 == lineBarSpot3, false); + }); + + test('LineTooltipItem equality test', () { + expect(lineTooltipItem1 == lineTooltipItem1Clone, true); + expect(lineTooltipItem1 == lineTooltipItem2, false); + expect(lineTooltipItem1 == lineTooltipItem3, false); + expect(lineTooltipItem1 == lineTooltipItem4, false); + }); + + test('TouchedSpotIndicatorData equality test', () { + expect(touchedSpotIndicatorData1 == touchedSpotIndicatorData1Clone, true); + expect(touchedSpotIndicatorData1 == touchedSpotIndicatorData2, false); + expect(touchedSpotIndicatorData1 == touchedSpotIndicatorData3, false); + expect(touchedSpotIndicatorData1 == touchedSpotIndicatorData4, false); + expect(touchedSpotIndicatorData1 == touchedSpotIndicatorData5, false); + expect(touchedSpotIndicatorData1 == touchedSpotIndicatorData6, false); + }); + + test('ShowingTooltipIndicator equality test', () { + expect(showingTooltipIndicator1 == showingTooltipIndicator1Clone, true); + expect(showingTooltipIndicator1 == showingTooltipIndicator2, false); + expect(showingTooltipIndicator1 == showingTooltipIndicator3, false); + expect(showingTooltipIndicator1 == showingTooltipIndicator4, false); + expect(showingTooltipIndicator1 == showingTooltipIndicator5, false); + }); + + test('LineTouchResponse equality test', () { + expect(lineTouchResponse1 == lineTouchResponse1Clone, false); + expect(lineTouchResponse1 == lineTouchResponse2, false); + expect(lineTouchResponse1 == lineTouchResponse3, false); + expect(lineTouchResponse1 == lineTouchResponse4, false); + expect(lineTouchResponse1 == lineTouchResponse5, false); + }); + + test('LineChartData equality test', () { + expect(lineChartData1 == lineChartData1Clone, true); + expect(lineChartData1 == lineChartData2, false); + expect(lineChartData1 == lineChartData3, false); + expect(lineChartData1 == lineChartData4, false); + expect(lineChartData1 == lineChartData5, false); + expect(lineChartData1 == lineChartData6, false); + expect(lineChartData1 == lineChartData7, false); + expect(lineChartData1 == lineChartData8, false); + expect(lineChartData1 == lineChartData9, false); + expect(lineChartData1 == lineChartData10, false); + expect(lineChartData1 == lineChartData11, false); + expect(lineChartData1 == lineChartData12, false); + expect(lineChartData1 == lineChartData13, false); + expect(lineChartData1 == lineChartData14, false); + expect(lineChartData1 == lineChartData15, false); + expect(lineChartData1 == lineChartData16, false); + expect(lineChartData1 == lineChartData17, false); + expect(lineChartData1 == lineChartData18, false); + expect(lineChartData1 == lineChartData19, false); + expect(lineChartData1 == lineChartData20, false); + expect(lineChartData1 == lineChartData21, false); + expect( + lineChartData21 == + lineChartData21.copyWith( + rotationQuarterTurns: 2, + ), + false, + ); + expect( + lineChartData21 == + lineChartData21.copyWith( + rotationQuarterTurns: 1, + ), + true, + ); + }); + }); +} diff --git a/test/chart/line_chart/line_chart_helper_test.dart b/test/chart/line_chart/line_chart_helper_test.dart new file mode 100644 index 0000000..155e6cf --- /dev/null +++ b/test/chart/line_chart/line_chart_helper_test.dart @@ -0,0 +1,78 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/line_chart/line_chart_helper.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../data_pool.dart'; + +void main() { + group('Check caching of LineChartHelper.calculateMaxAxisValues', () { + test('Test validity 1', () { + final lineChartHelper = LineChartHelper(); + final lineBars = [lineChartBarData1, lineChartBarData2]; + final (minX, maxX, minY, maxY) = + lineChartHelper.calculateMaxAxisValues(lineBars); + expect(minX, 1); + expect(maxX, 4); + expect(minY, 1); + expect(maxY, 2); + }); + + test('Test validity 2', () { + final lineChartHelper = LineChartHelper(); + final lineBars = [ + lineChartBarData1.copyWith( + spots: const [ + FlSpot(3, 4), + FlSpot(-3, 50), + FlSpot(14, -10), + ], + ), + ]; + final (minX, maxX, minY, maxY) = + lineChartHelper.calculateMaxAxisValues(lineBars); + expect(minX, -3); + expect(maxX, 14); + expect(minY, -10); + expect(maxY, 50); + }); + + test('Test equality', () { + final lineChartHelper = LineChartHelper(); + final lineBars = [lineChartBarData1, lineChartBarData2]; + final lineBarsClone = [lineChartBarData1Clone, lineChartBarData2]; + final result1 = lineChartHelper.calculateMaxAxisValues(lineBars); + final result2 = lineChartHelper.calculateMaxAxisValues(lineBarsClone); + expect(result1, result2); + }); + + test('Test null spot 1', () { + final lineChartHelper = LineChartHelper(); + final lineBars = [ + LineChartBarData( + spots: [ + FlSpot.nullSpot, + FlSpot.nullSpot, + FlSpot.nullSpot, + FlSpot.nullSpot, + ], + ), + ]; + expect(lineChartHelper.calculateMaxAxisValues(lineBars), (0, 0, 0, 0)); + }); + + test('Test null spot 2', () { + final lineChartHelper = LineChartHelper(); + final lineBars = [ + LineChartBarData( + spots: [ + FlSpot.nullSpot, + const FlSpot(-1, 5), + FlSpot.nullSpot, + const FlSpot(4, -3), + ], + ), + ]; + expect(lineChartHelper.calculateMaxAxisValues(lineBars), (-1, 4, -3, 5)); + }); + }); +} diff --git a/test/chart/line_chart/line_chart_painter_test.dart b/test/chart/line_chart/line_chart_painter_test.dart new file mode 100644 index 0000000..618a27a --- /dev/null +++ b/test/chart/line_chart/line_chart_painter_test.dart @@ -0,0 +1,4429 @@ +import 'dart:math' as math; +import 'dart:ui' as ui show Gradient; +import 'dart:ui'; + +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart'; +import 'package:fl_chart/src/chart/line_chart/line_chart_helper.dart'; +import 'package:fl_chart/src/chart/line_chart/line_chart_painter.dart'; +import 'package:fl_chart/src/extensions/path_extension.dart'; +import 'package:fl_chart/src/utils/canvas_wrapper.dart'; +import 'package:fl_chart/src/utils/utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import '../data_pool.dart'; +import 'line_chart_painter_test.mocks.dart'; + +@GenerateMocks([Canvas, CanvasWrapper, BuildContext, Utils, LineChartPainter]) +@GenerateNiceMocks([MockSpec()]) +void main() { + group('paint()', () { + test('test 1', () { + const viewSize = Size(400, 400); + + final bar1 = LineChartBarData( + spots: const [ + FlSpot(0, 4), + FlSpot(1, 3), + FlSpot(2, 2), + FlSpot(3, 1), + FlSpot(4, 0), + ], + showingIndicators: [ + 0, + 2, + 3, + ], + ); + final bar2 = LineChartBarData( + spots: const [ + FlSpot(0, 5), + FlSpot(1, 3), + FlSpot(2, 2), + FlSpot(3, 5), + FlSpot(4, 0), + ], + ); + + final lineChartBarsData = [bar1, bar2]; + final (minX, maxX, minY, maxY) = LineChartHelper().calculateMaxAxisValues( + lineChartBarsData, + ); + + final data = LineChartData( + minX: minX, + maxX: maxX, + minY: minY, + maxY: maxY, + lineBarsData: lineChartBarsData, + clipData: const FlClipData.all(), + extraLinesData: ExtraLinesData( + horizontalLines: [ + HorizontalLine(y: 1), + ], + verticalLines: [ + VerticalLine(x: 4), + ], + ), + betweenBarsData: [ + BetweenBarsData(fromIndex: 0, toIndex: 1), + ], + showingTooltipIndicators: [ + ShowingTooltipIndicators([ + LineBarSpot(bar1, 0, bar1.spots.first), + LineBarSpot(bar2, 1, bar2.spots.first), + ]), + ], + lineTouchData: LineTouchData( + getTouchedSpotIndicator: + (LineChartBarData barData, List spotIndexes) { + return spotIndexes.asMap().entries.map((entry) { + final i = entry.key; + if (i == 0) { + return null; + } + return const TouchedSpotIndicatorData( + FlLine(color: MockData.color0), + FlDotData(), + ); + }).toList(); + }, + ), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.getThemeAwareTextStyle(any, any)) + .thenAnswer((realInvocation) => textStyle1); + when(mockUtils.calculateRotationOffset(any, any)) + .thenAnswer((realInvocation) => Offset.zero); + when(mockUtils.convertRadiusToSigma(any)) + .thenAnswer((realInvocation) => 4.0); + when(mockUtils.getEfficientInterval(any, any)) + .thenAnswer((realInvocation) => 1.0); + when(mockUtils.getBestInitialIntervalValue(any, any, any)) + .thenAnswer((realInvocation) => 1.0); + + final mockBuildContext = MockBuildContext(); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + lineChartPainter.paint( + mockBuildContext, + mockCanvasWrapper, + holder, + ); + + verify(mockCanvasWrapper.clipRect(any)).called(1); + verify(mockCanvasWrapper.drawDot(any, any, any)).called(12); + verify(mockCanvasWrapper.drawPath(any, any)).called(3); + }); + test('test 2', () { + const viewSize = Size(400, 400); + + final bar1 = LineChartBarData( + spots: const [ + FlSpot(0, 4), + FlSpot(1, 3), + FlSpot(2, 2), + FlSpot(3, 1), + FlSpot(4, 0), + ], + ); + final bar2 = LineChartBarData( + spots: const [ + FlSpot(0, 2), + FlSpot(1, 5), + FlSpot(2, 1), + FlSpot(3, 2), + FlSpot(4, 3), + ], + ); + + final lineChartBarsData = [bar1, bar2]; + final (minX, maxX, minY, maxY) = LineChartHelper().calculateMaxAxisValues( + lineChartBarsData, + ); + + final data = LineChartData( + minX: minX, + maxX: maxX, + minY: minY, + maxY: maxY, + lineBarsData: lineChartBarsData, + clipData: const FlClipData.all(), + lineTouchData: LineTouchData( + getTouchedSpotIndicator: + (LineChartBarData barData, List spotIndexes) { + return List.generate( + spotIndexes.length + 1, + (index) { + return const TouchedSpotIndicatorData( + FlLine(color: MockData.color0), + FlDotData(), + ); + }, + ).toList(); + }, + ), + extraLinesData: ExtraLinesData( + horizontalLines: [ + HorizontalLine(y: 1), + ], + verticalLines: [ + VerticalLine(x: 4), + ], + extraLinesOnTop: false, + ), + showingTooltipIndicators: [ + ShowingTooltipIndicators([ + LineBarSpot(bar1, 0, bar1.spots[0]), + LineBarSpot(bar1, 0, bar1.spots[2]), + LineBarSpot(bar2, 1, bar1.spots[2]), + LineBarSpot(bar2, 1, bar1.spots[3]), + LineBarSpot(bar2, 1, bar1.spots[4]), + ]), + ], + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.getThemeAwareTextStyle(any, any)) + .thenAnswer((realInvocation) => textStyle1); + when(mockUtils.calculateRotationOffset(any, any)) + .thenAnswer((realInvocation) => Offset.zero); + when(mockUtils.convertRadiusToSigma(any)) + .thenAnswer((realInvocation) => 4.0); + when(mockUtils.getEfficientInterval(any, any)) + .thenAnswer((realInvocation) => 1.0); + when(mockUtils.getBestInitialIntervalValue(any, any, any)) + .thenAnswer((realInvocation) => 1.0); + + final mockBuildContext = MockBuildContext(); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + Object? exception; + try { + lineChartPainter.paint( + mockBuildContext, + mockCanvasWrapper, + holder, + ); + } catch (e) { + exception = e; + } + expect(exception != null, true); + }); + + test('test 3 minY == maxY', () { + const viewSize = Size(400, 400); + + final bar1 = LineChartBarData( + spots: const [ + FlSpot(0, 4), + FlSpot(1, 3), + FlSpot(2, 2), + FlSpot(3, 1), + FlSpot(4, 0), + ], + showingIndicators: [ + 0, + 2, + 3, + ], + ); + final bar2 = LineChartBarData( + spots: const [ + FlSpot(0, 5), + FlSpot(1, 3), + FlSpot(2, 2), + FlSpot(3, 5), + FlSpot(4, 0), + ], + ); + + final lineChartBarsData = [bar1, bar2]; + final (minX, maxX, minY, maxY) = LineChartHelper().calculateMaxAxisValues( + lineChartBarsData, + ); + + final data = LineChartData( + minX: minX, + maxX: maxX, + minY: minY, + maxY: minY, + lineBarsData: lineChartBarsData, + clipData: const FlClipData.all(), + extraLinesData: ExtraLinesData( + horizontalLines: [ + HorizontalLine(y: 1), + ], + verticalLines: [ + VerticalLine(x: 4), + ], + ), + betweenBarsData: [ + BetweenBarsData(fromIndex: 0, toIndex: 1), + ], + showingTooltipIndicators: [ + ShowingTooltipIndicators([ + LineBarSpot(bar1, 0, bar1.spots.first), + LineBarSpot(bar2, 1, bar2.spots.first), + ]), + ], + lineTouchData: LineTouchData( + getTouchedSpotIndicator: + (LineChartBarData barData, List spotIndexes) { + return spotIndexes.asMap().entries.map((entry) { + final i = entry.key; + if (i == 0) { + return null; + } + return const TouchedSpotIndicatorData( + FlLine(color: MockData.color0), + FlDotData(), + ); + }).toList(); + }, + ), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.getThemeAwareTextStyle(any, any)) + .thenAnswer((realInvocation) => textStyle1); + when(mockUtils.calculateRotationOffset(any, any)) + .thenAnswer((realInvocation) => Offset.zero); + when(mockUtils.convertRadiusToSigma(any)) + .thenAnswer((realInvocation) => 4.0); + when(mockUtils.getEfficientInterval(any, any)) + .thenAnswer((realInvocation) => 1.0); + when(mockUtils.getBestInitialIntervalValue(any, any, any)) + .thenAnswer((realInvocation) => 1.0); + + final mockBuildContext = MockBuildContext(); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + lineChartPainter.paint( + mockBuildContext, + mockCanvasWrapper, + holder, + ); + + verify(mockCanvasWrapper.clipRect(any)).called(1); + verify(mockCanvasWrapper.drawDot(any, any, any)).called(12); + verify(mockCanvasWrapper.drawPath(any, any)).called(3); + }); + }); + + group('clipToBorder()', () { + test('test 1', () { + const viewSize = Size(400, 400); + + final data = LineChartData(); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + lineChartPainter.clipToBorder( + mockCanvasWrapper, + holder, + ); + + final verifyResult = verify(mockCanvasWrapper.clipRect(captureAny)); + final rect = verifyResult.captured.single as Rect; + verifyResult.called(1); + expect(rect.left, 0); + expect(rect.top, 0); + expect(rect.width, 400); + expect(rect.height, 400); + }); + + test('test 2', () { + const viewSize = Size(400, 400); + + final data = LineChartData( + titlesData: const FlTitlesData( + leftTitles: AxisTitles( + sideTitles: SideTitles(showTitles: true, reservedSize: 10), + ), + topTitles: AxisTitles( + sideTitles: SideTitles(showTitles: true, reservedSize: 20), + ), + rightTitles: AxisTitles( + sideTitles: SideTitles(showTitles: true, reservedSize: 30), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles(showTitles: true, reservedSize: 40), + ), + ), + borderData: FlBorderData(show: true, border: Border.all(width: 8)), + clipData: const FlClipData( + top: false, + bottom: false, + left: true, + right: true, + ), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + lineChartPainter.clipToBorder( + mockCanvasWrapper, + holder, + ); + + final verifyResult = verify(mockCanvasWrapper.clipRect(captureAny)); + final rect = verifyResult.captured.single as Rect; + verifyResult.called(1); + expect(rect.left, 4); + expect(rect.top, 0); + expect(rect.right, 396); + expect(rect.bottom, 400); + }); + + test('test 3', () { + const viewSize = Size(400, 400); + + final data = LineChartData( + titlesData: const FlTitlesData( + leftTitles: AxisTitles( + sideTitles: SideTitles(showTitles: true, reservedSize: 10), + ), + topTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 40, + ), + ), + rightTitles: AxisTitles( + sideTitles: SideTitles(showTitles: true, reservedSize: 30), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles(showTitles: true, reservedSize: 40), + ), + ), + borderData: FlBorderData(show: true, border: Border.all(width: 8)), + clipData: const FlClipData( + top: true, + bottom: true, + left: true, + right: true, + ), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + lineChartPainter.clipToBorder( + mockCanvasWrapper, + holder, + ); + + final verifyResult = verify(mockCanvasWrapper.clipRect(captureAny)); + final rect = verifyResult.captured.single as Rect; + verifyResult.called(1); + expect(rect.left, 4); + expect(rect.top, 4); + expect(rect.right, 396); + expect(rect.bottom, 396); + }); + }); + + group('drawBarLine()', () { + test('test 1', () { + const viewSize = Size(400, 400); + + final barData = LineChartBarData( + spots: const [ + flSpot1, + flSpot2, + FlSpot(20, 11), + FlSpot(11, 11), + ], + ); + + final data = LineChartData(lineBarsData: [barData]); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + lineChartPainter.drawBarLine( + mockCanvasWrapper, + barData, + holder, + ); + + verify(mockCanvasWrapper.drawPath(any, any)).called(1); + }); + + test('test 2', () { + const viewSize = Size(400, 400); + + final barData = LineChartBarData( + spots: const [ + flSpot1, + flSpot2, + FlSpot.nullSpot, + FlSpot(20, 11), + FlSpot(11, 11), + ], + ); + + final data = LineChartData(lineBarsData: [barData]); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + lineChartPainter.drawBarLine( + mockCanvasWrapper, + barData, + holder, + ); + + verify(mockCanvasWrapper.drawPath(any, any)).called(2); + }); + + test('test 3', () { + const viewSize = Size(400, 400); + + final barData = LineChartBarData( + spots: const [ + flSpot1, + flSpot2, + FlSpot(20, 11), + FlSpot(11, 11), + ], + ); + + final data = LineChartData( + lineBarsData: [barData], + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + lineChartPainter.drawBarLine( + mockCanvasWrapper, + barData, + holder, + ); + + final verificationResult = + verify(mockCanvasWrapper.drawPath(any, captureAny)); + final paint = verificationResult.captured.single as Paint; + verificationResult.called(1); + expect( + paint.color, + isSameColorAs(barData.gradient?.colors.first ?? barData.color!), + ); + }); + }); + + group('drawBetweenBarsArea()', () { + test('test 1', () { + const viewSize = Size(400, 400); + + final barData = LineChartBarData( + spots: const [ + flSpot1, + flSpot2, + FlSpot(20, 11), + FlSpot(11, 11), + ], + ); + + final barData2 = LineChartBarData( + spots: const [ + flSpot2, + flSpot1, + FlSpot(20, 11), + FlSpot(11, 11), + ], + ); + + final betweenBarData = BetweenBarsData( + fromIndex: 0, + toIndex: 1, + color: const Color(0xFFFF0000), + ); + + final data = LineChartData( + lineBarsData: [ + barData, + barData2, + ], + betweenBarsData: [ + betweenBarData, + ], + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + lineChartPainter.drawBetweenBarsArea( + mockCanvasWrapper, + data, + betweenBarData, + holder, + ); + + final verifyResult = verifyInOrder([ + mockCanvasWrapper.saveLayer(const Rect.fromLTWH(0, 0, 400, 400), any), + mockCanvasWrapper.drawPath(any, captureAny), + mockCanvasWrapper.restore(), + ]); + + final paint = verifyResult[1].captured.first as Paint; + expect(paint.shader, null); + expect(paint.color, const Color(0xFFFF0000)); + }); + }); + + group('drawDots()', () { + test('test 1', () { + const viewSize = Size(400, 400); + + final barData = LineChartBarData(); + + final data = LineChartData( + lineBarsData: [ + barData, + ], + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + lineChartPainter.drawDots( + mockCanvasWrapper, + barData, + holder, + ); + + verifyNever(mockCanvasWrapper.drawDot(any, any, any)); + }); + + test('test 2', () { + const viewSize = Size(400, 400); + + final barData = LineChartBarData( + spots: const [FlSpot(1, 1)], + dotData: const FlDotData(show: false), + ); + + final data = LineChartData( + lineBarsData: [ + barData, + ], + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + lineChartPainter.drawDots( + mockCanvasWrapper, + barData, + holder, + ); + + verifyNever(mockCanvasWrapper.drawDot(any, any, any)); + }); + + test('test 3', () { + const viewSize = Size(400, 400); + + final barData = LineChartBarData( + spots: const [ + FlSpot(1, 1), + FlSpot(2, 2), + FlSpot(3, 3), + FlSpot(4, 4), + FlSpot.nullSpot, + FlSpot(5, 5), + ], + ); + + final data = LineChartData( + lineBarsData: [ + barData, + ], + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + lineChartPainter.drawDots( + mockCanvasWrapper, + barData, + holder, + ); + + verify(mockCanvasWrapper.drawDot(any, any, any)).called(5); + }); + + test('test 4', () { + const viewSize = Size(100, 100); + + final barData = LineChartBarData( + spots: const [ + FlSpot(1, 1), + FlSpot(2, 2), + FlSpot(3, 3), + FlSpot(4, 4), + FlSpot.nullSpot, + FlSpot(5, 5), + ], + ); + + final data = LineChartData( + minX: 0, + maxX: 10, + minY: 0, + maxY: 10, + lineBarsData: [ + barData, + ], + titlesData: const FlTitlesData(show: false), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + lineChartPainter.drawDots( + mockCanvasWrapper, + barData, + holder, + ); + + verifyInOrder([ + mockCanvasWrapper.drawDot( + any, + const FlSpot(1, 1), + const Offset(10, 90), + ), + mockCanvasWrapper.drawDot( + any, + const FlSpot(2, 2), + const Offset(20, 80), + ), + mockCanvasWrapper.drawDot( + any, + const FlSpot(3, 3), + const Offset(30, 70), + ), + mockCanvasWrapper.drawDot( + any, + const FlSpot(4, 4), + const Offset(40, 60), + ), + mockCanvasWrapper.drawDot( + any, + const FlSpot(5, 5), + const Offset(50, 50), + ), + ]); + }); + }); + + group('drawErrorIndicatorData()', () { + test('test - not showing error indicators', () { + const viewSize = Size(400, 400); + + final barData = LineChartBarData( + spots: [ + const FlSpot( + 1, + 1, + xError: FlErrorRange(lowerBy: 1, upperBy: 1), + ), + ], + errorIndicatorData: const FlErrorIndicatorData( + show: false, + ), + ); + + final data = LineChartData( + lineBarsData: [ + barData, + ], + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + lineChartPainter.drawErrorIndicatorData( + mockCanvasWrapper, + barData, + holder, + ); + + verifyNever( + mockCanvasWrapper.drawErrorIndicator(any, any, any, any, any), + ); + }); + + test('test 2 - showing error indicators with single call', () { + const viewSize = Size(400, 400); + + final barData = LineChartBarData( + spots: [ + const FlSpot( + 1, + 1, + xError: FlErrorRange(lowerBy: 1, upperBy: 1), + ), + ], + ); + + final data = LineChartData( + lineBarsData: [ + barData, + ], + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + lineChartPainter.drawErrorIndicatorData( + mockCanvasWrapper, + barData, + holder, + ); + + verify( + mockCanvasWrapper.drawErrorIndicator(any, any, any, any, any), + ).called(1); + }); + + test('test 3 - different values for different spots', () { + const viewSize = Size(400, 400); + + final colors = [ + Colors.red, + Colors.green, + Colors.blue, + Colors.yellow, + Colors.purple, + ]; + final spots = [ + const FlSpot(1, 1, xError: FlErrorRange.symmetric(1)), + const FlSpot(2, 2, xError: FlErrorRange.symmetric(2)), + const FlSpot(3, 3, xError: FlErrorRange.symmetric(3)), + const FlSpot(4, 2, xError: FlErrorRange.symmetric(2)), + const FlSpot(5, 1, xError: FlErrorRange.symmetric(1)), + ]; + final barData = LineChartBarData( + spots: spots, + errorIndicatorData: FlErrorIndicatorData( + painter: (input) => FlSimpleErrorPainter( + lineColor: colors[input.spotIndex], + lineWidth: input.spotIndex.toDouble(), + capLength: 10, + crossAlignment: input.spotIndex / spots.length, + showErrorTexts: true, + errorTextDirection: TextDirection.rtl, + errorTextStyle: TextStyle( + color: colors[input.spotIndex], + fontSize: input.spot.y, + ), + ), + ), + ); + + final data = LineChartData( + lineBarsData: [ + barData, + ], + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + lineChartPainter.drawErrorIndicatorData( + mockCanvasWrapper, + barData, + holder, + ); + + final result = verify( + mockCanvasWrapper.drawErrorIndicator(captureAny, any, any, any, any), + )..called(5); + for (var i = 0; i < result.captured.length; i++) { + final captured = result.captured[i] as FlSimpleErrorPainter; + expect(captured.lineColor, colors[i]); + expect(captured.lineWidth, i.toDouble()); + expect(captured.capLength, 10); + expect(captured.crossAlignment, i / spots.length); + expect(captured.showErrorTexts, true); + expect(captured.errorTextDirection, TextDirection.rtl); + expect(captured.errorTextStyle.color, colors[i]); + expect(captured.errorTextStyle.fontSize, spots[i].y); + } + verifyNever(mockCanvasWrapper.drawText(any, any, any)); + }); + }); + + group('drawTouchedSpotsIndicator()', () { + List getDrawingInfo(LineChartData data) { + final lineIndexDrawingInfo = []; + + /// draw each line independently on the chart + for (var i = 0; i < data.lineBarsData.length; i++) { + final barData = data.lineBarsData[i]; + + if (!barData.show) { + continue; + } + + final indicatorsData = data.lineTouchData + .getTouchedSpotIndicator(barData, barData.showingIndicators); + + if (indicatorsData.length != barData.showingIndicators.length) { + throw Exception( + 'indicatorsData and touchedSpotOffsets size should be same', + ); + } + + for (var j = 0; j < barData.showingIndicators.length; j++) { + final indicatorData = indicatorsData[j]; + final index = barData.showingIndicators[j]; + final spot = barData.spots[index]; + + if (indicatorData == null) { + continue; + } + lineIndexDrawingInfo.add( + LineIndexDrawingInfo(barData, i, spot, index, indicatorData), + ); + } + } + return lineIndexDrawingInfo; + } + + test('test 1', () { + const viewSize = Size(400, 400); + + final lineChartBarData = LineChartBarData(); + + final data = LineChartData( + lineBarsData: [lineChartBarData], + showingTooltipIndicators: [], + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + lineChartPainter.drawTouchedSpotsIndicator( + mockCanvasWrapper, + getDrawingInfo(data), + holder, + ); + + verifyNever(mockCanvasWrapper.drawPath(any, any)); + }); + + test('test 2', () { + const viewSize = Size(400, 400); + + const spot1 = FlSpot(1, 1); + const spot2 = FlSpot(2, 2); + const spot3 = FlSpot(3, 3); + final lineChartBarData = LineChartBarData( + spots: const [spot1, spot2, spot3], + showingIndicators: [0, 1], + ); + + final data = LineChartData( + lineBarsData: [lineChartBarData], + lineTouchData: LineTouchData( + getTouchedSpotIndicator: (barData, spotIndexes) { + return spotIndexes.asMap().entries.map((e) { + final index = e.key; + final color = index == 0 + ? const Color(0xFF00FF00) + : const Color(0xFF0000FF); + final strokeWidth = index == 0 ? 8.0 : 12.0; + return TouchedSpotIndicatorData( + FlLine(color: color, strokeWidth: strokeWidth), + const FlDotData(show: false), + ); + }).toList(); + }, + ), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final results = >[]; + when( + mockCanvasWrapper.drawDashedLine( + captureAny, + captureAny, + captureAny, + any, + ), + ).thenAnswer((inv) { + results.add({ + 'from': inv.positionalArguments[0] as Offset, + 'to': inv.positionalArguments[1] as Offset, + 'paint_color': (inv.positionalArguments[2] as Paint).color, + 'paint_stroke_width': + (inv.positionalArguments[2] as Paint).strokeWidth, + }); + }); + + lineChartPainter.drawTouchedSpotsIndicator( + mockCanvasWrapper, + getDrawingInfo(data), + holder, + ); + + expect(results.length, 2); + + expect(results[0]['paint_color'], const Color(0xFF0000FF)); + expect(results[0]['paint_stroke_width'], 12); + + expect(results[1]['paint_color'], const Color(0xFF00FF00)); + expect(results[1]['paint_stroke_width'], 8.0); + }); + + test('test 3', () { + const viewSize = Size(400, 400); + + const spot1 = FlSpot(1, 1); + const spot2 = FlSpot(2, 2); + const spot3 = FlSpot(3, 3); + final lineChartBarData = LineChartBarData( + spots: const [spot1, spot2, spot3], + showingIndicators: [0, 1], + ); + + final data = LineChartData( + lineBarsData: [lineChartBarData], + lineTouchData: LineTouchData( + getTouchedSpotIndicator: (barData, spotIndexes) { + return spotIndexes.asMap().entries.map((e) { + final index = e.key; + final color = index == 0 + ? const Color(0xFF00FF00) + : const Color(0xFF0000FF); + final strokeWidth = index == 0 ? 8.0 : 12.0; + return TouchedSpotIndicatorData( + FlLine(color: color, strokeWidth: strokeWidth), + const FlDotData(), + ); + }).toList(); + }, + ), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final results = >[]; + when( + mockCanvasWrapper.drawDashedLine( + captureAny, + captureAny, + captureAny, + any, + ), + ).thenAnswer((inv) { + results.add({ + 'from': inv.positionalArguments[0] as Offset, + 'to': inv.positionalArguments[1] as Offset, + 'paint_color': (inv.positionalArguments[2] as Paint).color, + 'paint_stroke_width': + (inv.positionalArguments[2] as Paint).strokeWidth, + }); + }); + + lineChartPainter.drawTouchedSpotsIndicator( + mockCanvasWrapper, + getDrawingInfo(data), + holder, + ); + + expect(results.length, 2); + + expect(results[0]['paint_color'], const Color(0xFF0000FF)); + expect(results[0]['paint_stroke_width'], 12); + + expect(results[1]['paint_color'], const Color(0xFF00FF00)); + expect(results[1]['paint_stroke_width'], 8.0); + + verify(mockCanvasWrapper.drawDot(any, any, any)).called(2); + }); + }); + + group('generateBarPath()', () { + test('test 1', () { + const viewSize = Size(100, 100); + + final lineChartBarData = LineChartBarData( + spots: const [ + FlSpot.zero, + FlSpot(5, 5), + FlSpot(10, 0), + ], + ); + + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + lineBarsData: [lineChartBarData], + showingTooltipIndicators: [], + titlesData: const FlTitlesData(show: false), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final path = lineChartPainter.generateBarPath( + viewSize, + lineChartBarData, + lineChartBarData.spots, + holder, + ); + + final iterator = path.computeMetrics().iterator; + + PathMetric? firstMetric; + PathMetric? lastMetric; + while (iterator.moveNext()) { + firstMetric ??= iterator.current; + lastMetric = iterator.current; + } + + final tangent1 = firstMetric!.getTangentForOffset(firstMetric.length / 8); + final degrees1 = tangent1!.angle * (180 / math.pi); + expect(degrees1, 45.0); + + final tangent = lastMetric!.getTangentForOffset( + (lastMetric.length / 8) * 7, + ); + final degrees = tangent!.angle * (180 / math.pi); + expect(degrees, -45.0); + }); + + test('test 2', () { + const viewSize = Size(100, 100); + + final lineChartBarData = LineChartBarData( + spots: const [ + FlSpot.zero, + FlSpot(5, 5), + FlSpot(10, 0), + ], + isStepLineChart: true, + ); + + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + lineBarsData: [lineChartBarData], + showingTooltipIndicators: [], + titlesData: const FlTitlesData(show: false), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final path = lineChartPainter.generateBarPath( + viewSize, + lineChartBarData, + lineChartBarData.spots, + holder, + ); + + final iterator = path.computeMetrics().iterator; + + PathMetric? firstMetric; + PathMetric? lastMetric; + while (iterator.moveNext()) { + firstMetric ??= iterator.current; + lastMetric = iterator.current; + } + + final tangent1 = firstMetric!.getTangentForOffset(firstMetric.length / 4); + final degrees1 = tangent1!.angle * (180 / math.pi); + expect(degrees1, 90.0); + + final tangent2 = lastMetric!.getTangentForOffset( + (lastMetric.length / 4) * 3, + ); + final degrees2 = tangent2!.angle * (180 / math.pi); + expect(degrees2, -90.0); + }); + }); + + group('generateNormalBarPath()', () { + test('test 1', () { + const viewSize = Size(100, 100); + + final lineChartBarData = LineChartBarData( + spots: const [ + FlSpot.zero, + FlSpot(5, 5), + FlSpot(10, 0), + ], + ); + + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + lineBarsData: [lineChartBarData], + showingTooltipIndicators: [], + titlesData: const FlTitlesData(show: false), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final path = lineChartPainter.generateNormalBarPath( + viewSize, + lineChartBarData, + lineChartBarData.spots, + holder, + ); + + final iterator = path.computeMetrics().iterator; + + PathMetric? firstMetric; + PathMetric? lastMetric; + while (iterator.moveNext()) { + firstMetric ??= iterator.current; + lastMetric = iterator.current; + } + + final tangent1 = firstMetric!.getTangentForOffset(firstMetric.length / 8); + final degrees1 = tangent1!.angle * (180 / math.pi); + expect(degrees1, 45.0); + + final tangent = lastMetric!.getTangentForOffset( + (lastMetric.length / 8) * 7, + ); + final degrees = tangent!.angle * (180 / math.pi); + expect(degrees, -45.0); + }); + }); + + group('generateStepBarPath()', () { + test('test 1', () { + const viewSize = Size(100, 100); + + final lineChartBarData = LineChartBarData( + spots: const [ + FlSpot.zero, + FlSpot(5, 5), + FlSpot(10, 0), + ], + isStepLineChart: true, + ); + + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + lineBarsData: [lineChartBarData], + showingTooltipIndicators: [], + titlesData: const FlTitlesData(show: false), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final path = lineChartPainter.generateStepBarPath( + viewSize, + lineChartBarData, + lineChartBarData.spots, + holder, + ); + + final iterator = path.computeMetrics().iterator; + + PathMetric? firstMetric; + PathMetric? lastMetric; + while (iterator.moveNext()) { + firstMetric ??= iterator.current; + lastMetric = iterator.current; + } + + final tangent1 = firstMetric!.getTangentForOffset(firstMetric.length / 4); + final degrees1 = tangent1!.angle * (180 / math.pi); + expect(degrees1, 90.0); + + final tangent2 = lastMetric!.getTangentForOffset( + (lastMetric.length / 4) * 3, + ); + final degrees2 = tangent2!.angle * (180 / math.pi); + expect(degrees2, -90.0); + }); + }); + + group('generateBelowBarPath()', () { + test('test 1', () { + const viewSize = Size(100, 100); + + const barSpots = [ + FlSpot(1, 9), + FlSpot(5, 5), + FlSpot(8, 9), + ]; + + final lineChartBarData = LineChartBarData( + spots: barSpots, + isStepLineChart: true, + ); + + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + lineBarsData: [lineChartBarData], + showingTooltipIndicators: [], + titlesData: const FlTitlesData(show: false), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final barPath = Path() + ..moveTo(10, 10) + ..lineTo(50, 50) + ..lineTo(80, 10); + + final belowBarPath = lineChartPainter.generateBelowBarPath( + viewSize, + lineChartBarData, + barPath, + barSpots, + holder, + ); + + expect(belowBarPath.getBounds().bottom, 100); + expect(belowBarPath.getBounds().left, 10); + expect(belowBarPath.getBounds().right, 80); + expect(belowBarPath.getBounds().top, 10); + }); + + test('test 2', () { + const viewSize = Size(100, 100); + + const barSpots = [ + FlSpot(1, 9), + FlSpot(5, 5), + FlSpot(8, 9), + ]; + + final lineChartBarData = LineChartBarData( + spots: barSpots, + isStepLineChart: true, + belowBarData: BarAreaData( + cutOffY: 4, + applyCutOffY: true, + ), + ); + + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + lineBarsData: [lineChartBarData], + showingTooltipIndicators: [], + titlesData: const FlTitlesData(show: false), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final barPath = Path() + ..moveTo(10, 10) + ..lineTo(50, 50) + ..lineTo(80, 10); + + final belowBarPath = lineChartPainter.generateBelowBarPath( + viewSize, + lineChartBarData, + barPath, + barSpots, + holder, + ); + + expect(belowBarPath.getBounds().bottom, 60); + expect(belowBarPath.getBounds().left, 10); + expect(belowBarPath.getBounds().right, 80); + expect(belowBarPath.getBounds().top, 10); + }); + }); + + group('generateAboveBarPath()', () { + test('test 1', () { + const viewSize = Size(100, 100); + + const barSpots = [ + FlSpot(1, 9), + FlSpot(5, 5), + FlSpot(8, 9), + ]; + + final lineChartBarData = LineChartBarData( + spots: barSpots, + isStepLineChart: true, + ); + + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + lineBarsData: [lineChartBarData], + showingTooltipIndicators: [], + titlesData: const FlTitlesData(show: false), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final barPath = Path() + ..moveTo(10, 10) + ..lineTo(50, 50) + ..lineTo(80, 10); + + final belowBarPath = lineChartPainter.generateAboveBarPath( + viewSize, + lineChartBarData, + barPath, + barSpots, + holder, + ); + + expect(belowBarPath.getBounds().bottom, 50); + expect(belowBarPath.getBounds().left, 10); + expect(belowBarPath.getBounds().right, 80); + expect(belowBarPath.getBounds().top, 0); + }); + }); + + group('drawBelowBar()', () { + test('test 1', () { + const viewSize = Size(100, 100); + + const barSpots = [ + FlSpot(1, 9), + FlSpot(8, 9), + ]; + + final lineChartBarData = LineChartBarData( + spots: barSpots, + isStepLineChart: true, + belowBarData: BarAreaData( + show: true, + gradient: const LinearGradient( + colors: [Color(0xFFFF0000), Color(0xFF00FF00)], + ), + ), + ); + + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + lineBarsData: [lineChartBarData], + showingTooltipIndicators: [], + titlesData: const FlTitlesData(show: false), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final belowBarPath = Path() + ..moveTo(10, 10) + ..lineTo(80, 10); + + final filletAboveBarPath = Path() + ..moveTo(10, 10) + ..lineTo(80, 10) + ..lineTo(80, 0) + ..lineTo(10, 0) + ..lineTo(10, 10); + + lineChartPainter.drawBelowBar( + mockCanvasWrapper, + belowBarPath, + filletAboveBarPath, + holder, + lineChartBarData, + ); + + final result = + verify(mockCanvasWrapper.drawPath(belowBarPath, captureAny)) + ..called(1); + + final paint = result.captured.single as Paint; + expect(paint.color, const Color(0xFF000000)); + + expect(paint.shader is ui.Gradient, true); + }); + + test('test 2', () { + const viewSize = Size(100, 100); + + const barSpots = [ + FlSpot(1, 9), + FlSpot(8, 9), + ]; + + final lineChartBarData = LineChartBarData( + spots: barSpots, + isStepLineChart: true, + belowBarData: BarAreaData( + show: true, + gradient: const LinearGradient( + colors: [Color(0xFFFF0000), Color(0xFF00FF00)], + ), + applyCutOffY: true, + cutOffY: 8, + spotsLine: const BarAreaSpotsLine( + show: true, + applyCutOffY: false, + flLineStyle: FlLine( + color: Color(0x00F0F0F0), + strokeWidth: 18, + ), + ), + ), + ); + + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + lineBarsData: [lineChartBarData], + showingTooltipIndicators: [], + titlesData: const FlTitlesData(show: false), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final belowBarPath = Path() + ..moveTo(10, 10) + ..lineTo(80, 10); + + final filletAboveBarPath = Path() + ..moveTo(10, 10) + ..lineTo(80, 10) + ..lineTo(80, 0) + ..lineTo(10, 0) + ..lineTo(10, 10); + + final results = >[]; + when( + mockCanvasWrapper.drawDashedLine( + captureAny, + captureAny, + captureAny, + any, + ), + ).thenAnswer((inv) { + results.add({ + 'from': inv.positionalArguments[0] as Offset, + 'to': inv.positionalArguments[1] as Offset, + 'paint_color': (inv.positionalArguments[2] as Paint).color, + 'paint_stroke_width': + (inv.positionalArguments[2] as Paint).strokeWidth, + }); + }); + + lineChartPainter.drawBelowBar( + mockCanvasWrapper, + belowBarPath, + filletAboveBarPath, + holder, + lineChartBarData, + ); + + verify( + mockCanvasWrapper.saveLayer( + Rect.fromLTWH(0, 0, viewSize.width, viewSize.height), + any, + ), + ).called(1); + + final result = + verify(mockCanvasWrapper.drawPath(belowBarPath, captureAny)) + ..called(1); + final paint = result.captured.single as Paint; + expect(paint.color, const Color(0xFF000000)); + expect(paint.shader is ui.Gradient, true); + + final result2 = + verify(mockCanvasWrapper.drawPath(filletAboveBarPath, captureAny)) + ..called(1); + final paint2 = result2.captured.single as Paint; + expect(paint2.color, const Color(0x00000000)); + expect(paint2.blendMode, BlendMode.dstIn); + expect(paint2.style, PaintingStyle.fill); + + verify(mockCanvasWrapper.restore()).called(1); + + expect(results.length, 2); + + for (final item in results) { + expect((item['paint_color'] as Color).a, 0); + expect(item['paint_stroke_width'], 18); + } + }); + }); + + group('drawAboveBar()', () { + test('test 1', () { + const viewSize = Size(100, 100); + + const barSpots = [ + FlSpot(1, 9), + FlSpot(8, 9), + ]; + + final lineChartBarData = LineChartBarData( + spots: barSpots, + isStepLineChart: true, + aboveBarData: BarAreaData( + show: true, + gradient: const LinearGradient( + colors: [Color(0xFFFF0000), Color(0xFF00FF00)], + ), + ), + ); + + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + lineBarsData: [lineChartBarData], + showingTooltipIndicators: [], + titlesData: const FlTitlesData(show: false), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final aboveBarPath = Path() + ..moveTo(10, 10) + ..lineTo(80, 10); + + final filledBelowBarPath = Path() + ..moveTo(10, 10) + ..lineTo(80, 10) + ..lineTo(80, 0) + ..lineTo(10, 0) + ..lineTo(10, 10); + + lineChartPainter.drawAboveBar( + mockCanvasWrapper, + aboveBarPath, + filledBelowBarPath, + holder, + lineChartBarData, + ); + + final result = + verify(mockCanvasWrapper.drawPath(aboveBarPath, captureAny)) + ..called(1); + + final paint = result.captured.single as Paint; + expect(paint.color, const Color(0xFF000000)); + + expect(paint.shader is ui.Gradient, true); + }); + + test('test 2', () { + const viewSize = Size(100, 100); + + const barSpots = [ + FlSpot(1, 9), + FlSpot(8, 9), + ]; + + final lineChartBarData = LineChartBarData( + spots: barSpots, + isStepLineChart: true, + aboveBarData: BarAreaData( + show: true, + gradient: const LinearGradient( + colors: [Color(0xFFFF0000), Color(0xFF00FF00)], + ), + applyCutOffY: true, + cutOffY: 8, + spotsLine: const BarAreaSpotsLine( + show: true, + applyCutOffY: false, + flLineStyle: FlLine( + color: Color(0x00F0F0F0), + strokeWidth: 18, + ), + ), + ), + ); + + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + lineBarsData: [lineChartBarData], + showingTooltipIndicators: [], + titlesData: const FlTitlesData(show: false), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final aboveBarPath = Path() + ..moveTo(10, 10) + ..lineTo(80, 10); + + final filledBelowBarPath = Path() + ..moveTo(10, 10) + ..lineTo(80, 10) + ..lineTo(80, 0) + ..lineTo(10, 0) + ..lineTo(10, 10); + + final results = >[]; + when( + mockCanvasWrapper.drawDashedLine( + captureAny, + captureAny, + captureAny, + any, + ), + ).thenAnswer((inv) { + results.add({ + 'from': inv.positionalArguments[0] as Offset, + 'to': inv.positionalArguments[1] as Offset, + 'paint_color': (inv.positionalArguments[2] as Paint).color, + 'paint_stroke_width': + (inv.positionalArguments[2] as Paint).strokeWidth, + }); + }); + + lineChartPainter.drawAboveBar( + mockCanvasWrapper, + aboveBarPath, + filledBelowBarPath, + holder, + lineChartBarData, + ); + + verify( + mockCanvasWrapper.saveLayer( + Rect.fromLTWH(0, 0, viewSize.width, viewSize.height), + any, + ), + ).called(1); + + final result = + verify(mockCanvasWrapper.drawPath(aboveBarPath, captureAny)) + ..called(1); + final paint = result.captured.single as Paint; + expect(paint.color, const Color(0xFF000000)); + expect(paint.shader is ui.Gradient, true); + + final result2 = + verify(mockCanvasWrapper.drawPath(filledBelowBarPath, captureAny)) + ..called(1); + final paint2 = result2.captured.single as Paint; + expect(paint2.color, const Color(0x00000000)); + expect(paint2.blendMode, BlendMode.dstIn); + expect(paint2.style, PaintingStyle.fill); + + verify(mockCanvasWrapper.restore()).called(1); + + expect(results.length, 2); + + for (final item in results) { + expect((item['paint_color'] as Color).a, 0); + expect(item['paint_stroke_width'], 18); + } + }); + }); + + group('drawBetweenBar()', () { + test('test 1', () { + const viewSize = Size(100, 100); + + const barSpots1 = [ + FlSpot(1, 9), + FlSpot(8, 9), + ]; + + const barSpots2 = [ + FlSpot(1, 5), + FlSpot(8, 5), + ]; + + final lineChartBarData1 = LineChartBarData( + spots: barSpots1, + isStepLineChart: true, + aboveBarData: BarAreaData( + show: true, + gradient: const LinearGradient( + colors: [Color(0xFFFF0000), Color(0xFF00FF00)], + ), + ), + ); + + final lineChartBarData2 = LineChartBarData( + spots: barSpots2, + isStepLineChart: true, + ); + + final betweenBarData1 = BetweenBarsData( + fromIndex: 0, + toIndex: 1, + color: const Color(0xFFFF0000), + ); + + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + lineBarsData: [lineChartBarData1, lineChartBarData2], + showingTooltipIndicators: [], + titlesData: const FlTitlesData(show: false), + betweenBarsData: [betweenBarData1], + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final aboveBarPath = Path() + ..moveTo(10, 10) + ..lineTo(80, 10); + + lineChartPainter.drawBetweenBar( + mockCanvasWrapper, + aboveBarPath, + betweenBarData1, + MockData.rect1, + holder, + ); + + verify( + mockCanvasWrapper.saveLayer( + Rect.fromLTWH(0, 0, viewSize.width, viewSize.height), + any, + ), + ); + final result = + verify(mockCanvasWrapper.drawPath(aboveBarPath, captureAny)) + ..called(1); + final painter = result.captured.single as Paint; + expect(painter.color, const Color(0xFFFF0000)); + verify(mockCanvasWrapper.restore()); + }); + }); + + group('drawBarShadow()', () { + test('test 1', () { + const barSpots1 = [ + FlSpot(1, 9), + FlSpot(8, 9), + ]; + + final lineChartBarData1 = LineChartBarData( + spots: barSpots1, + isStepLineChart: true, + shadow: const Shadow(color: Color(0x0000FF00)), + ); + + final lineChartPainter = LineChartPainter(); + final mockCanvasWrapper = MockCanvasWrapper(); + + final barPath = Path() + ..moveTo(10, 10) + ..lineTo(80, 10); + + lineChartPainter.drawBarShadow( + mockCanvasWrapper, + barPath, + lineChartBarData1, + ); + verifyNever(mockCanvasWrapper.drawPath(any, any)); + }); + + test('test 2', () { + const viewSize = Size(100, 100); + + const barSpots1 = [ + FlSpot(1, 9), + FlSpot(8, 9), + ]; + + final lineChartBarData1 = LineChartBarData( + spots: barSpots1, + barWidth: 80, + isStrokeCapRound: true, + isStepLineChart: true, + shadow: const Shadow( + color: Color(0x0100FF00), + offset: Offset(10, 15), + blurRadius: 10, + ), + ); + + final lineChartPainter = LineChartPainter(); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final barPath = Path() + ..moveTo(10, 10) + ..lineTo(80, 10); + + lineChartPainter.drawBarShadow( + mockCanvasWrapper, + barPath, + lineChartBarData1, + ); + final result = verify(mockCanvasWrapper.drawPath(captureAny, captureAny)) + ..called(1); + final path = result.captured[0] as Path; + expect(path.getBounds(), barPath.shift(const Offset(10, 15)).getBounds()); + + final paint = result.captured[1] as Paint; + expect(paint.color, isSameColorAs(const Color(0x0100FF00))); + expect(paint.shader, null); + expect(paint.strokeWidth, 80); + expect( + paint.maskFilter.toString(), + MaskFilter.blur(BlurStyle.normal, Utils().convertRadiusToSigma(10)) + .toString(), + ); + expect(paint.strokeCap, StrokeCap.round); + }); + }); + + group('drawBar()', () { + test('test 1', () { + const viewSize = Size(100, 100); + + const barSpots1 = [ + FlSpot(1, 9), + FlSpot(8, 9), + ]; + + final lineChartBarData1 = LineChartBarData( + show: false, + spots: barSpots1, + barWidth: 80, + isStrokeCapRound: true, + isStepLineChart: true, + shadow: const Shadow( + color: Color(0x0100FF00), + offset: Offset(10, 15), + blurRadius: 10, + ), + ); + + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + lineBarsData: [lineChartBarData1], + showingTooltipIndicators: [], + titlesData: const FlTitlesData(show: false), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final barPath = Path() + ..moveTo(10, 10) + ..lineTo(80, 10); + + lineChartPainter.drawBar( + mockCanvasWrapper, + barPath, + lineChartBarData1, + holder, + ); + verifyNever(mockCanvasWrapper.drawPath(any, any)); + }); + + test('test 2', () { + const viewSize = Size(100, 100); + + const barSpots1 = [ + FlSpot(1, 9), + FlSpot(8, 9), + ]; + + final lineChartBarData1 = LineChartBarData( + spots: barSpots1, + barWidth: 80, + isStrokeCapRound: true, + isStepLineChart: true, + color: const Color(0xF0F0F0F0), + shadow: const Shadow( + color: Color(0x0100FF00), + offset: Offset(10, 15), + blurRadius: 10, + ), + ); + + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + lineBarsData: [lineChartBarData1], + showingTooltipIndicators: [], + titlesData: const FlTitlesData(show: false), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final barPath = Path() + ..moveTo(10, 10) + ..lineTo(80, 10); + + lineChartPainter.drawBar( + mockCanvasWrapper, + barPath, + lineChartBarData1, + holder, + ); + final result = verify(mockCanvasWrapper.drawPath(captureAny, captureAny)) + ..called(1); + final drewPath = result.captured[0] as Path; + expect(drewPath, barPath); + + final paint = result.captured[1] as Paint; + expect(paint.color, isSameColorAs(const Color(0xF0F0F0F0))); + expect(paint.shader, null); + expect(paint.maskFilter, null); + expect(paint.strokeWidth, 80); + }); + + test('test 3', () { + const viewSize = Size(100, 100); + + const barSpots1 = [ + FlSpot(1, 9), + FlSpot(8, 9), + ]; + + final mockLinearGradient = MockLinearGradient(); + final lineChartBarData1 = LineChartBarData( + spots: barSpots1, + barWidth: 80, + isStrokeCapRound: true, + isStepLineChart: true, + gradient: mockLinearGradient, + dashArray: [1, 2, 3], + ); + + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + lineBarsData: [lineChartBarData1], + showingTooltipIndicators: [], + titlesData: const FlTitlesData(show: false), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + // just to provide a dummy Shader, don't care about its value + provideDummy( + const LinearGradient(colors: [Color(0xF0F0F0F0), Color(0x0100FF00)]) + .createShader(Rect.zero), + ); + + final barPath = Path() + ..moveTo(10, 10) + ..lineTo(80, 10); + + lineChartPainter.drawBar( + mockCanvasWrapper, + barPath, + lineChartBarData1, + holder, + ); + final result = verify(mockCanvasWrapper.drawPath(captureAny, captureAny)) + ..called(1); + final drewPath = result.captured[0] as Path; + expect( + drewPath.computeMetrics().length, + barPath.toDashedPath([1, 2, 3]).computeMetrics().length, + ); + + final paint = result.captured[1] as Paint; + expect(paint.shader != null, true); + expect(paint.maskFilter, null); + expect(paint.strokeWidth, 80); + + final createShaderWithRectAroundTheLine = + verify(mockLinearGradient.createShader(captureAny))..called(1); + expect( + createShaderWithRectAroundTheLine.captured.single, + const Rect.fromLTRB(10, 10, 80, 10), + ); + + lineChartPainter.drawBar( + mockCanvasWrapper, + barPath, + lineChartBarData1.copyWith( + gradientArea: LineChartGradientArea.wholeChart, + ), + holder, + ); + + final createShaderWithRectWholeChart = + verify(mockLinearGradient.createShader(captureAny))..called(1); + expect( + createShaderWithRectWholeChart.captured.single, + Offset.zero & viewSize, + ); + }); + }); + + group('drawExtraLines()', () { + test('test 1', () { + const viewSize = Size(100, 100); + + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + titlesData: const FlTitlesData(show: false), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final mockBuildContext = MockBuildContext(); + + lineChartPainter.drawExtraLines( + mockBuildContext, + mockCanvasWrapper, + holder, + ); + + verifyNever(mockCanvasWrapper.drawDashedLine(any, any, any, captureAny)); + }); + + test('test 2', () { + const viewSize = Size(100, 100); + + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + titlesData: const FlTitlesData(show: false), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final mockBuildContext = MockBuildContext(); + + lineChartPainter.drawExtraLines( + mockBuildContext, + mockCanvasWrapper, + holder, + ); + + verifyNever(mockCanvasWrapper.drawDashedLine(any, any, any, captureAny)); + }); + + test('test 3', () { + const viewSize = Size(100, 100); + + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + titlesData: const FlTitlesData(show: false), + extraLinesData: ExtraLinesData( + horizontalLines: [ + HorizontalLine( + y: 1, + color: const Color(0x11111111), + strokeWidth: 11, + ), + HorizontalLine( + y: 2, + color: const Color(0x22222222), + strokeWidth: 22, + ), + ], + verticalLines: [ + VerticalLine(x: 4, color: const Color(0x33333333), strokeWidth: 33), + VerticalLine(x: 5, color: const Color(0x44444444), strokeWidth: 44), + ], + ), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final mockBuildContext = MockBuildContext(); + + final results = >[]; + when( + mockCanvasWrapper.drawDashedLine( + captureAny, + captureAny, + captureAny, + any, + ), + ).thenAnswer((inv) { + results.add({ + 'from': inv.positionalArguments[0] as Offset, + 'to': inv.positionalArguments[1] as Offset, + 'paint_color': (inv.positionalArguments[2] as Paint).color, + 'paint_stroke_width': + (inv.positionalArguments[2] as Paint).strokeWidth, + }); + }); + + lineChartPainter.drawExtraLines( + mockBuildContext, + mockCanvasWrapper, + holder, + ); + + expect(results.length, 4); + + expect( + results[0]['paint_color'], + isSameColorAs(const Color(0x11111111)), + ); + expect(results[0]['paint_stroke_width'], 11); + expect(results[0]['from'], const Offset(0, 90)); + expect(results[0]['to'], const Offset(100, 90)); + + expect( + results[1]['paint_color'], + isSameColorAs(const Color(0x22222222)), + ); + expect(results[1]['paint_stroke_width'], 22); + expect(results[1]['from'], const Offset(0, 80)); + expect(results[1]['to'], const Offset(100, 80)); + + expect( + results[2]['paint_color'], + isSameColorAs(const Color(0x33333333)), + ); + expect(results[2]['paint_stroke_width'], 33); + expect(results[2]['from'], const Offset(40, 0)); + expect(results[2]['to'], const Offset(40, 100)); + + expect( + results[3]['paint_color'], + isSameColorAs(const Color(0x44444444)), + ); + expect(results[3]['paint_stroke_width'], 44); + expect(results[3]['from'], const Offset(50, 0)); + expect(results[3]['to'], const Offset(50, 100)); + }); + + test('should draw horizontal lines at max and min', () { + const viewSize = Size(100, 100); + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + titlesData: const FlTitlesData(show: false), + extraLinesData: ExtraLinesData( + horizontalLines: [ + HorizontalLine( + y: 0, + color: Colors.cyanAccent, + dashArray: [12, 22], + ), + HorizontalLine( + y: 10, + color: Colors.cyanAccent, + dashArray: [12, 22], + ), + ], + extraLinesOnTop: false, + ), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final mockBuildContext = MockBuildContext(); + + lineChartPainter.drawExtraLines( + mockBuildContext, + mockCanvasWrapper, + holder, + ); + + verify( + mockCanvasWrapper.drawDashedLine( + any, + any, + argThat( + const TypeMatcher().having( + (p0) => p0.color, + 'colors match', + isSameColorAs(Colors.cyanAccent), + ), + ), + holder.data.extraLinesData.horizontalLines[0].dashArray, + ), + ).called(2); + }); + + test('should draw vertical lines at max and min', () { + const viewSize = Size(100, 100); + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + titlesData: const FlTitlesData(show: false), + extraLinesData: ExtraLinesData( + verticalLines: [ + VerticalLine( + x: 0, + color: Colors.cyanAccent, + dashArray: [12, 22], + ), + VerticalLine( + x: 10, + color: Colors.cyanAccent, + dashArray: [12, 22], + ), + ], + ), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final mockBuildContext = MockBuildContext(); + + lineChartPainter.drawExtraLines( + mockBuildContext, + mockCanvasWrapper, + holder, + ); + + verify( + mockCanvasWrapper.drawDashedLine( + any, + any, + argThat( + const TypeMatcher().having( + (p0) => p0.color, + 'colors match', + isSameColorAs(Colors.cyanAccent), + ), + ), + holder.data.extraLinesData.verticalLines[0].dashArray, + ), + ).called(2); + }); + + test('should not draw extra lines beyond chart max and min', () { + const viewSize = Size(100, 100); + final data = LineChartData( + minY: -1, + maxY: 10, + minX: -1, + maxX: 10, + titlesData: const FlTitlesData(show: false), + extraLinesData: ExtraLinesData( + verticalLines: [ + VerticalLine( + x: -1.1, + color: Colors.cyanAccent, + dashArray: [12, 22], + ), + VerticalLine( + x: 11, + color: Colors.cyanAccent, + dashArray: [12, 22], + ), + ], + horizontalLines: [ + HorizontalLine( + y: -1.1, + color: Colors.cyanAccent, + dashArray: [12, 22], + ), + HorizontalLine( + y: 11, + color: Colors.cyanAccent, + dashArray: [12, 22], + ), + ], + ), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final mockBuildContext = MockBuildContext(); + + lineChartPainter.drawExtraLines( + mockBuildContext, + mockCanvasWrapper, + holder, + ); + + verifyNever( + mockCanvasWrapper.drawDashedLine( + any, + any, + argThat( + const TypeMatcher().having( + (p0) => p0.color, + 'colors match', + isSameColorAs(Colors.cyanAccent), + ), + ), + holder.data.extraLinesData.verticalLines[0].dashArray, + ), + ); + }); + + test('test lines label', () { + const viewSize = Size(100, 100); + + String horizontalLabelResolver(HorizontalLine line) { + return 'test'; + } + + String verticalLabelResolver(VerticalLine line) { + return 'test'; + } + + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + extraLinesData: ExtraLinesData( + verticalLines: [ + VerticalLine( + x: 3, + label: VerticalLineLabel( + show: true, + labelResolver: verticalLabelResolver, + ), + ), + VerticalLine( + x: 6, + label: VerticalLineLabel( + show: true, + labelResolver: verticalLabelResolver, + direction: LabelDirection.vertical, + ), + ), + ], + horizontalLines: [ + HorizontalLine( + y: 3, + label: HorizontalLineLabel( + show: true, + labelResolver: horizontalLabelResolver, + ), + ), + HorizontalLine( + y: 6, + label: HorizontalLineLabel( + show: true, + labelResolver: horizontalLabelResolver, + direction: LabelDirection.vertical, + ), + ), + ], + ), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final mockBuildContext = MockBuildContext(); + + lineChartPainter.drawExtraLines( + mockBuildContext, + mockCanvasWrapper, + holder, + ); + + final result1 = verify(mockCanvasWrapper.drawText(any, captureAny)) + ..called(2); + final result2 = + verify(mockCanvasWrapper.drawVerticalText(any, captureAny)) + ..called(2); + + final offset1 = result1.captured[0] as Offset; + final offset2 = result1.captured[1] as Offset; + final offset3 = result2.captured[0] as Offset; + final offset4 = result2.captured[1] as Offset; + expect(offset1, const Offset(6, 50)); + expect(offset2, const Offset(36, 80)); + expect(offset3, const Offset(20, -22)); + expect(offset4, const Offset(80, 38)); + }); + + test( + 'should restore canvas before drawing extra lines and clip after ' + 'when chart virtual rect is provided', () { + const viewSize = Size(100, 100); + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + titlesData: const FlTitlesData(show: false), + extraLinesData: ExtraLinesData( + verticalLines: [ + VerticalLine( + x: 0, + color: Colors.cyanAccent, + dashArray: [12, 22], + ), + VerticalLine( + x: 10, + color: Colors.cyanAccent, + dashArray: [12, 22], + ), + ], + ), + ); + + final lineChartPainter = LineChartPainter(); + final holder = PaintHolder( + data, + data, + TextScaler.noScaling, + Offset.zero & viewSize, + ); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final mockBuildContext = MockBuildContext(); + + lineChartPainter.drawExtraLines( + mockBuildContext, + mockCanvasWrapper, + holder, + ); + + final viewRect = Offset.zero & viewSize; + verifyInOrder([ + mockCanvasWrapper.restore(), + mockCanvasWrapper.drawDashedLine( + any, + any, + argThat( + const TypeMatcher().having( + (p0) => p0.color, + 'colors match', + isSameColorAs(Colors.cyanAccent), + ), + ), + holder.data.extraLinesData.verticalLines[0].dashArray, + ), + mockCanvasWrapper.saveLayer( + viewRect, + any, + ), + mockCanvasWrapper.clipRect(viewRect), + ]); + }); + }); + + group('drawTouchTooltip()', () { + test('test 1', () { + const viewSize = Size(100, 100); + + final barData = LineChartBarData( + spots: const [ + FlSpot(1, 1), + FlSpot(2, 2), + FlSpot(3, 3), + FlSpot(4, 4), + FlSpot.nullSpot, + FlSpot(5, 5), + ], + ); + + final tooltipData = LineTouchTooltipData( + getTooltipColor: (touchedSpot) => const Color(0x11111111), + tooltipBorderRadius: const BorderRadius.only( + topLeft: Radius.circular(10), + topRight: Radius.circular(8), + ), + rotateAngle: 43, + maxContentWidth: 100, + tooltipMargin: 12, + tooltipPadding: const EdgeInsets.all(12), + fitInsideHorizontally: true, + fitInsideVertically: true, + getTooltipItems: (List touchedSpots) { + return touchedSpots + .map((e) => LineTooltipItem(e.barIndex.toString(), textStyle1)) + .toList(); + }, + tooltipBorder: const BorderSide(color: Color(0x11111111), width: 2), + ); + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + titlesData: const FlTitlesData(show: false), + lineTouchData: LineTouchData( + touchTooltipData: tooltipData, + ), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final mockBuildContext = MockBuildContext(); + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.getThemeAwareTextStyle(any, any)) + .thenAnswer((realInvocation) => textStyle1); + when(mockUtils.calculateRotationOffset(any, any)) + .thenAnswer((realInvocation) => Offset.zero); + when( + mockCanvasWrapper.drawRotated( + size: anyNamed('size'), + rotationOffset: anyNamed('rotationOffset'), + drawOffset: anyNamed('drawOffset'), + angle: anyNamed('angle'), + drawCallback: anyNamed('drawCallback'), + ), + ).thenAnswer((realInvocation) { + final callback = realInvocation + .namedArguments[const Symbol('drawCallback')] as DrawCallback; + callback(); + }); + lineChartPainter.drawTouchTooltip( + mockBuildContext, + mockCanvasWrapper, + tooltipData, + barData.spots.first, + ShowingTooltipIndicators([ + LineBarSpot( + barData, + 0, + barData.spots.first, + ), + ]), + holder, + ); + + final result1 = + verify(mockCanvasWrapper.drawRRect(captureAny, captureAny)) + ..called(2); + final rRect = result1.captured[0] as RRect; + final paint = result1.captured[1] as Paint; + expect( + rRect, + RRect.fromLTRBAndCorners( + 0, + 40, + 38, + 78, + topLeft: const Radius.circular(10), + topRight: const Radius.circular(8), + ), + ); + expect(paint.color, isSameColorAs(const Color(0x11111111))); + final rRectBorder = result1.captured[2] as RRect; + final paintBorder = result1.captured[3] as Paint; + expect( + rRectBorder, + RRect.fromLTRBAndCorners( + 0, + 40, + 38, + 78, + topLeft: const Radius.circular(10), + topRight: const Radius.circular(8), + ), + ); + expect(paintBorder.color, isSameColorAs(const Color(0x11111111))); + expect(paintBorder.strokeWidth, 2); + + final result2 = verify(mockCanvasWrapper.drawText(captureAny, captureAny)) + ..called(1); + final textPainter = result2.captured[0] as TextPainter; + final drawOffset = result2.captured[1] as Offset; + expect((textPainter.text as TextSpan?)!.text, '0'); + expect((textPainter.text as TextSpan?)!.style, textStyle1); + expect(drawOffset, const Offset(12, 52)); + }); + + test('test 2', () { + const viewSize = Size(100, 100); + + final barData = LineChartBarData( + spots: const [ + FlSpot(1, 1), + FlSpot(2, 2), + FlSpot(3, 3), + FlSpot(4, 4), + FlSpot.nullSpot, + FlSpot(5, 5), + ], + ); + + final tooltipData = LineTouchTooltipData( + getTooltipColor: (touchedSpot) => const Color(0x11111111), + tooltipBorderRadius: BorderRadius.circular(12), + rotateAngle: 43, + maxContentWidth: 100, + tooltipMargin: 12, + tooltipHorizontalAlignment: FLHorizontalAlignment.left, + tooltipPadding: const EdgeInsets.all(12), + fitInsideVertically: true, + getTooltipItems: (List touchedSpots) { + return touchedSpots + .map((e) => LineTooltipItem(e.barIndex.toString(), textStyle1)) + .toList(); + }, + tooltipBorder: const BorderSide(color: Color(0x11111111), width: 2), + ); + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + titlesData: const FlTitlesData(show: false), + lineTouchData: LineTouchData( + touchTooltipData: tooltipData, + ), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final mockBuildContext = MockBuildContext(); + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.getThemeAwareTextStyle(any, any)) + .thenAnswer((realInvocation) => textStyle1); + when(mockUtils.calculateRotationOffset(any, any)) + .thenAnswer((realInvocation) => Offset.zero); + when( + mockCanvasWrapper.drawRotated( + size: anyNamed('size'), + rotationOffset: anyNamed('rotationOffset'), + drawOffset: anyNamed('drawOffset'), + angle: anyNamed('angle'), + drawCallback: anyNamed('drawCallback'), + ), + ).thenAnswer((realInvocation) { + final callback = realInvocation + .namedArguments[const Symbol('drawCallback')] as DrawCallback; + callback(); + }); + lineChartPainter.drawTouchTooltip( + mockBuildContext, + mockCanvasWrapper, + tooltipData, + barData.spots.first, + ShowingTooltipIndicators([ + LineBarSpot( + barData, + 0, + barData.spots.first, + ), + ]), + holder, + ); + + final result1 = + verify(mockCanvasWrapper.drawRRect(captureAny, captureAny)) + ..called(2); + final rRect = result1.captured[0] as RRect; + final paint = result1.captured[1] as Paint; + expect( + rRect, + RRect.fromLTRBR(-28, 40, 10, 78, const Radius.circular(12)), + ); + expect(paint.color, isSameColorAs(const Color(0x11111111))); + final rRectBorder = result1.captured[2] as RRect; + final paintBorder = result1.captured[3] as Paint; + expect( + rRectBorder, + RRect.fromLTRBR(-28, 40, 10, 78, const Radius.circular(12)), + ); + expect(paintBorder.color, isSameColorAs(const Color(0x11111111))); + expect(paintBorder.strokeWidth, 2); + + final result2 = verify(mockCanvasWrapper.drawText(captureAny, captureAny)) + ..called(1); + final textPainter = result2.captured[0] as TextPainter; + final drawOffset = result2.captured[1] as Offset; + expect((textPainter.text as TextSpan?)!.text, '0'); + expect((textPainter.text as TextSpan?)!.style, textStyle1); + expect(drawOffset, const Offset(-16, 52)); + }); + + test('test 3', () { + const viewSize = Size(100, 100); + + final barData = LineChartBarData( + spots: const [ + FlSpot(1, 1), + FlSpot(2, 2), + FlSpot(3, 3), + FlSpot(4, 4), + FlSpot.nullSpot, + FlSpot(5, 5), + ], + ); + + final tooltipData = LineTouchTooltipData( + getTooltipColor: (touchedSpot) => const Color(0x11111111), + tooltipBorderRadius: BorderRadius.circular(12), + rotateAngle: 43, + maxContentWidth: 100, + tooltipMargin: 12, + tooltipHorizontalAlignment: FLHorizontalAlignment.right, + tooltipPadding: const EdgeInsets.all(12), + fitInsideVertically: true, + getTooltipItems: (List touchedSpots) { + return touchedSpots + .map((e) => LineTooltipItem(e.barIndex.toString(), textStyle1)) + .toList(); + }, + tooltipBorder: const BorderSide(color: Color(0x11111111), width: 2), + ); + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + titlesData: const FlTitlesData(show: false), + lineTouchData: LineTouchData( + touchTooltipData: tooltipData, + ), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final mockBuildContext = MockBuildContext(); + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.getThemeAwareTextStyle(any, any)) + .thenAnswer((realInvocation) => textStyle1); + when(mockUtils.calculateRotationOffset(any, any)) + .thenAnswer((realInvocation) => Offset.zero); + when( + mockCanvasWrapper.drawRotated( + size: anyNamed('size'), + rotationOffset: anyNamed('rotationOffset'), + drawOffset: anyNamed('drawOffset'), + angle: anyNamed('angle'), + drawCallback: anyNamed('drawCallback'), + ), + ).thenAnswer((realInvocation) { + final callback = realInvocation + .namedArguments[const Symbol('drawCallback')] as DrawCallback; + callback(); + }); + lineChartPainter.drawTouchTooltip( + mockBuildContext, + mockCanvasWrapper, + tooltipData, + barData.spots.first, + ShowingTooltipIndicators([ + LineBarSpot( + barData, + 0, + barData.spots.first, + ), + ]), + holder, + ); + + final result1 = + verify(mockCanvasWrapper.drawRRect(captureAny, captureAny)) + ..called(2); + final rRect = result1.captured[0] as RRect; + final paint = result1.captured[1] as Paint; + expect( + rRect, + RRect.fromLTRBR(10, 40, 48, 78, const Radius.circular(12)), + ); + expect(paint.color, isSameColorAs(const Color(0x11111111))); + final rRectBorder = result1.captured[2] as RRect; + final paintBorder = result1.captured[3] as Paint; + expect( + rRectBorder, + RRect.fromLTRBR(10, 40, 48, 78, const Radius.circular(12)), + ); + expect(paintBorder.color, isSameColorAs(const Color(0x11111111))); + expect(paintBorder.strokeWidth, 2); + + final result2 = verify(mockCanvasWrapper.drawText(captureAny, captureAny)) + ..called(1); + final textPainter = result2.captured[0] as TextPainter; + final drawOffset = result2.captured[1] as Offset; + expect((textPainter.text as TextSpan?)!.text, '0'); + expect((textPainter.text as TextSpan?)!.style, textStyle1); + expect(drawOffset, const Offset(22, 52)); + }); + + test('test 4 - rotated chart with rotationQuarterTurns 2', () { + const viewSize = Size(100, 100); + + final barData1 = LineChartBarData( + spots: const [ + FlSpot(1, 1), + FlSpot(2, 2), + FlSpot(3, 3), + FlSpot(4, 4), + FlSpot.nullSpot, + FlSpot(5, 5), + ], + ); + final barData2 = LineChartBarData( + spots: const [ + FlSpot(1, 6), + FlSpot(2, 7), + FlSpot(3, 8), + FlSpot(4, 9), + FlSpot.nullSpot, + FlSpot(5, 10), + ], + ); + + final tooltipData = LineTouchTooltipData( + getTooltipColor: (touchedSpot) => const Color(0x11111111), + tooltipBorderRadius: BorderRadius.circular(12), + rotateAngle: 43, + maxContentWidth: 100, + tooltipMargin: 12, + tooltipHorizontalAlignment: FLHorizontalAlignment.right, + tooltipPadding: const EdgeInsets.all(12), + fitInsideVertically: true, + getTooltipItems: (List touchedSpots) { + return touchedSpots + .map((e) => LineTooltipItem(e.barIndex.toString(), textStyle1)) + .toList(); + }, + tooltipBorder: const BorderSide(color: Color(0x11111111), width: 2), + ); + final data = LineChartData( + rotationQuarterTurns: 2, + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + titlesData: const FlTitlesData(show: false), + lineTouchData: LineTouchData( + touchTooltipData: tooltipData, + ), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final mockBuildContext = MockBuildContext(); + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.getThemeAwareTextStyle(any, any)) + .thenAnswer((realInvocation) => textStyle1); + when(mockUtils.calculateRotationOffset(any, any)) + .thenAnswer((realInvocation) => Offset.zero); + when( + mockCanvasWrapper.drawRotated( + size: anyNamed('size'), + rotationOffset: anyNamed('rotationOffset'), + drawOffset: anyNamed('drawOffset'), + angle: anyNamed('angle'), + drawCallback: anyNamed('drawCallback'), + ), + ).thenAnswer((realInvocation) { + final callback = realInvocation + .namedArguments[const Symbol('drawCallback')] as DrawCallback; + callback(); + }); + lineChartPainter.drawTouchTooltip( + mockBuildContext, + mockCanvasWrapper, + tooltipData, + barData2.spots.first, + ShowingTooltipIndicators([ + LineBarSpot( + barData1, + 0, + barData1.spots.first, + ), + LineBarSpot( + barData2, + 1, + barData2.spots.first, + ), + ]), + holder, + ); + + final result1 = + verify(mockCanvasWrapper.drawRRect(captureAny, captureAny)) + ..called(2); + final rRect = result1.captured[0] as RRect; + final paint = result1.captured[1] as Paint; + expect( + rRect, + RRect.fromLTRBR(10, 0, 48, 56, const Radius.circular(12)), + ); + expect(paint.color, isSameColorAs(const Color(0x11111111))); + final rRectBorder = result1.captured[2] as RRect; + final paintBorder = result1.captured[3] as Paint; + expect( + rRectBorder, + RRect.fromLTRBR(10, 0, 48, 56, const Radius.circular(12)), + ); + expect(paintBorder.color, isSameColorAs(const Color(0x11111111))); + expect(paintBorder.strokeWidth, 2); + + final result2 = verify(mockCanvasWrapper.drawText(captureAny, captureAny)) + ..called(2); + final textPainter1 = result2.captured[0] as TextPainter; + final drawOffset1 = result2.captured[1] as Offset; + final textPainter2 = result2.captured[2] as TextPainter; + expect((textPainter1.text as TextSpan?)!.text, '1'); + expect((textPainter1.text as TextSpan?)!.style, textStyle1); + expect((textPainter2.text as TextSpan?)!.text, '0'); + expect((textPainter2.text as TextSpan?)!.style, textStyle1); + expect(drawOffset1, const Offset(22, 12)); + }); + + test('does not draw tooltip if it is outside of the chart virtual rect', + () { + const viewSize = Size(100, 100); + final chartVirtualRect = Offset.zero & const Size(200, 200); + + final barData = LineChartBarData( + spots: const [ + FlSpot(1, 1), + FlSpot(2, 2), + FlSpot(3, 3), + FlSpot(4, 4), + FlSpot.nullSpot, + FlSpot(5, 5), + ], + ); + + final tooltipData = LineTouchTooltipData( + getTooltipColor: (touchedSpot) => const Color(0x11111111), + tooltipBorderRadius: BorderRadius.circular(12), + rotateAngle: 43, + maxContentWidth: 100, + tooltipMargin: 12, + tooltipHorizontalAlignment: FLHorizontalAlignment.right, + tooltipPadding: const EdgeInsets.all(12), + fitInsideVertically: true, + getTooltipItems: (List touchedSpots) { + return touchedSpots + .map((e) => LineTooltipItem(e.barIndex.toString(), textStyle1)) + .toList(); + }, + tooltipBorder: const BorderSide(color: Color(0x11111111), width: 2), + ); + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + titlesData: const FlTitlesData(show: false), + lineTouchData: LineTouchData( + touchTooltipData: tooltipData, + ), + ); + + final lineChartPainter = LineChartPainter(); + final holder = PaintHolder( + data, + data, + TextScaler.noScaling, + chartVirtualRect, + ); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final mockBuildContext = MockBuildContext(); + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.getThemeAwareTextStyle(any, any)) + .thenAnswer((realInvocation) => textStyle1); + when(mockUtils.calculateRotationOffset(any, any)) + .thenAnswer((realInvocation) => Offset.zero); + + lineChartPainter.drawTouchTooltip( + mockBuildContext, + mockCanvasWrapper, + tooltipData, + barData.spots.first, + ShowingTooltipIndicators([ + LineBarSpot( + barData, + 0, + barData.spots.first, + ), + ]), + holder, + ); + + verifyNever( + mockCanvasWrapper.drawRotated( + size: anyNamed('size'), + rotationOffset: anyNamed('rotationOffset'), + drawOffset: anyNamed('drawOffset'), + angle: anyNamed('angle'), + drawCallback: anyNamed('drawCallback'), + ), + ); + }); + + test( + 'takes dotHeight into account when deciding if tooltip should be drawn', + () { + const viewSize = Size(100, 100); + const dotRadius = 4.0; + const smallerDotRadius = 3.0; + const dotStrokeWidth = 1.0; + + final barData = LineChartBarData( + spots: const [ + FlSpot(1, 1), + FlSpot(2, 2), + FlSpot(3, 3), + FlSpot(4, 4), + FlSpot.nullSpot, + FlSpot(5, 5), + ], + ); + + final tooltipData = LineTouchTooltipData( + getTooltipColor: (touchedSpot) => const Color(0x11111111), + tooltipBorderRadius: BorderRadius.circular(12), + rotateAngle: 43, + maxContentWidth: 100, + tooltipMargin: 12, + tooltipHorizontalAlignment: FLHorizontalAlignment.right, + tooltipPadding: const EdgeInsets.all(12), + fitInsideVertically: true, + getTooltipItems: (List touchedSpots) { + return touchedSpots + .map((e) => LineTooltipItem(e.barIndex.toString(), textStyle1)) + .toList(); + }, + tooltipBorder: const BorderSide(color: Color(0x11111111), width: 2), + ); + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + titlesData: const FlTitlesData(show: false), + lineBarsData: [barData], + lineTouchData: LineTouchData( + touchTooltipData: tooltipData, + getTouchedSpotIndicator: (barData, spotIndexes) => [ + TouchedSpotIndicatorData( + const FlLine(color: Colors.red, strokeWidth: 1), + FlDotData( + getDotPainter: ( + FlSpot spot, + double xPercentage, + LineChartBarData bar, + int index, { + double? size, + }) => + FlDotCirclePainter( + color: Colors.red, + // smaller first dot ensures we're actually iterating over + // the painters to get the largest dot height + radius: index == 0 ? smallerDotRadius : dotRadius, + strokeWidth: dotStrokeWidth, + ), + ), + ), + ], + ), + ); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final mockBuildContext = MockBuildContext(); + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.getThemeAwareTextStyle(any, any)) + .thenAnswer((realInvocation) => textStyle1); + when(mockUtils.calculateRotationOffset(any, any)) + .thenAnswer((realInvocation) => Offset.zero); + final lineChartPainter = LineChartPainter(); + + const dotHeight = (dotRadius + dotStrokeWidth) * 2; + const dotXOffset = 20.0; + const scaledSize = Size(200, 100); + + const dotVisibleXOffset = dotXOffset + (dotHeight / 2); + final chartVirtualRect = + const Offset(-dotVisibleXOffset, 0) & scaledSize; + + final indicators = ShowingTooltipIndicators([ + LineBarSpot( + barData, + 0, + barData.spots.first, + ), + LineBarSpot( + barData, + 0, + barData.spots[1], + ), + ]); + + final holder = PaintHolder( + data, + data, + TextScaler.noScaling, + chartVirtualRect, + ); + + lineChartPainter.drawTouchTooltip( + mockBuildContext, + mockCanvasWrapper, + tooltipData, + barData.spots.first, + indicators, + holder, + ); + + verify( + mockCanvasWrapper.drawRotated( + size: anyNamed('size'), + rotationOffset: anyNamed('rotationOffset'), + drawOffset: anyNamed('drawOffset'), + angle: anyNamed('angle'), + drawCallback: anyNamed('drawCallback'), + ), + ).called(3); + + const dotHiddenXOffset = dotXOffset + (dotHeight / 2) + 0.1; + final chartVirtualRect2 = + const Offset(-dotHiddenXOffset, 0) & scaledSize; + final holder2 = PaintHolder( + data, + data, + TextScaler.noScaling, + chartVirtualRect2, + ); + + lineChartPainter.drawTouchTooltip( + mockBuildContext, + mockCanvasWrapper, + tooltipData, + barData.spots.first, + indicators, + holder2, + ); + + verifyNever( + mockCanvasWrapper.drawRotated( + size: anyNamed('size'), + rotationOffset: anyNamed('rotationOffset'), + drawOffset: anyNamed('drawOffset'), + angle: anyNamed('angle'), + drawCallback: anyNamed('drawCallback'), + ), + ); + }, + ); + }); + + group('getBarLineXLength()', () { + test('test 1', () { + const viewSize = Size(100, 100); + + final barData = LineChartBarData( + spots: const [ + FlSpot(1, 1), + FlSpot(2, 2), + FlSpot(3, 3), + FlSpot(4, 4), + FlSpot.nullSpot, + FlSpot(5, 5), + ], + ); + + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final result = lineChartPainter.getBarLineXLength( + barData, + viewSize, + holder, + ); + expect(result, 40); + }); + }); + + group('handleTouch()', () { + test('test 1', () { + const viewSize = Size(100, 100); + + final lineChartBarData1 = LineChartBarData( + show: false, + spots: const [ + FlSpot(1, 1), + FlSpot(4, 1), + FlSpot(6, 1), + FlSpot(8, 1), + ], + barWidth: 80, + isStrokeCapRound: true, + isStepLineChart: true, + shadow: const Shadow( + color: Color(0x0100FF00), + offset: Offset(10, 15), + blurRadius: 10, + ), + ); + + final lineChartBarData2 = LineChartBarData( + show: false, + spots: const [ + FlSpot(1.1, 2), + FlSpot(2, 2), + FlSpot(3.5, 2), + FlSpot(4.3, 2), + ], + barWidth: 80, + isStrokeCapRound: true, + isStepLineChart: true, + shadow: const Shadow( + color: Color(0x0100FF00), + offset: Offset(10, 15), + blurRadius: 10, + ), + ); + + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + lineBarsData: [lineChartBarData1, lineChartBarData2], + showingTooltipIndicators: [], + titlesData: const FlTitlesData(show: false), + lineTouchData: const LineTouchData( + touchSpotThreshold: 0.5, + ), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final touchResponse = + lineChartPainter.handleTouch(const Offset(35, 0), viewSize, holder); + expect(touchResponse, null); + }); + + test('test 2', () { + const viewSize = Size(100, 100); + + final lineChartBarData1 = LineChartBarData( + spots: const [ + FlSpot(1, 1), + FlSpot(4, 1), + FlSpot(6, 1), + FlSpot(8, 1), + ], + barWidth: 80, + isStrokeCapRound: true, + isStepLineChart: true, + shadow: const Shadow( + color: Color(0x0100FF00), + offset: Offset(10, 15), + blurRadius: 10, + ), + ); + + final lineChartBarData2 = LineChartBarData( + spots: const [ + FlSpot(1.1, 2), + FlSpot(2, 2), + FlSpot(3.5, 2), + FlSpot(4.3, 2), + ], + barWidth: 80, + isStrokeCapRound: true, + isStepLineChart: true, + shadow: const Shadow( + color: Color(0x0100FF00), + offset: Offset(10, 15), + blurRadius: 10, + ), + ); + + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + lineBarsData: [lineChartBarData1, lineChartBarData2], + showingTooltipIndicators: [], + titlesData: const FlTitlesData(show: false), + lineTouchData: const LineTouchData( + touchSpotThreshold: 5, + ), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + expect( + lineChartPainter + .handleTouch(const Offset(30, 0), viewSize, holder)! + .length, + 1, + ); + expect( + lineChartPainter.handleTouch( + const Offset(29.99, 0), + viewSize, + holder, + ), + null, + ); + expect( + lineChartPainter + .handleTouch(const Offset(10, 0), viewSize, holder)! + .length, + 2, + ); + }); + + test('test 3', () { + const viewSize = Size(100, 100); + + final lineChartBarData1 = LineChartBarData( + spots: const [ + FlSpot(1, 1), + FlSpot(2, 1), + FlSpot(3, 1), + FlSpot(8, 1), + ], + barWidth: 80, + isStrokeCapRound: true, + isStepLineChart: true, + shadow: const Shadow( + color: Color(0x0100FF00), + offset: Offset(10, 15), + blurRadius: 10, + ), + ); + + final lineChartBarData2 = LineChartBarData( + spots: const [ + FlSpot(1.3, 1), + FlSpot(2, 1), + FlSpot(3, 1), + FlSpot(4, 1), + ], + barWidth: 80, + isStrokeCapRound: true, + isStepLineChart: true, + shadow: const Shadow( + color: Color(0x0100FF00), + offset: Offset(10, 15), + blurRadius: 10, + ), + ); + + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + lineBarsData: [lineChartBarData1, lineChartBarData2], + showingTooltipIndicators: [], + titlesData: const FlTitlesData(show: false), + lineTouchData: const LineTouchData( + touchSpotThreshold: 5, + ), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + + final result1 = + lineChartPainter.handleTouch(const Offset(11, 0), viewSize, holder)!; + expect(result1[0].barIndex, 0); + expect(result1[1].barIndex, 1); + + final result2 = + lineChartPainter.handleTouch(const Offset(12, 0), viewSize, holder)!; + expect(result2[0].barIndex, 1); + expect(result2[1].barIndex, 0); + }); + }); + + group('getNearestTouchedSpot()', () { + test('test 1', () { + const viewSize = Size(100, 100); + + final lineChartBarData1 = LineChartBarData( + show: false, + spots: const [ + FlSpot(1, 1), + FlSpot(4, 1), + FlSpot(6, 1), + FlSpot(8, 1), + ], + barWidth: 80, + isStrokeCapRound: true, + isStepLineChart: true, + shadow: const Shadow( + color: Color(0x0100FF00), + offset: Offset(10, 15), + blurRadius: 10, + ), + ); + + final lineChartBarData2 = LineChartBarData( + show: false, + spots: const [ + FlSpot(1.1, 2), + FlSpot(2, 2), + FlSpot(3.5, 2), + FlSpot(4.3, 2), + ], + barWidth: 80, + isStrokeCapRound: true, + isStepLineChart: true, + shadow: const Shadow( + color: Color(0x0100FF00), + offset: Offset(10, 15), + blurRadius: 10, + ), + ); + + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + lineBarsData: [lineChartBarData1, lineChartBarData2], + showingTooltipIndicators: [], + titlesData: const FlTitlesData(show: false), + lineTouchData: const LineTouchData( + touchSpotThreshold: 0.5, + ), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final touchResponse = lineChartPainter.getNearestTouchedSpot( + viewSize, + const Offset(35, 0), + data.lineBarsData[0], + 0, + holder, + ); + expect(touchResponse, null); + + final touchResponse2 = lineChartPainter.getNearestTouchedSpot( + viewSize, + const Offset(35, 0), + data.lineBarsData[0], + 0, + holder, + ); + expect(touchResponse2, null); + }); + + test('test 2', () { + const viewSize = Size(100, 100); + + final lineChartBarData1 = LineChartBarData( + spots: const [ + FlSpot(1, 1), + FlSpot(4, 1), + FlSpot(6, 1), + FlSpot(8, 1), + ], + barWidth: 80, + isStrokeCapRound: true, + isStepLineChart: true, + shadow: const Shadow( + color: Color(0x0100FF00), + offset: Offset(10, 15), + blurRadius: 10, + ), + ); + + final lineChartBarData2 = LineChartBarData( + spots: const [ + FlSpot(1.1, 2), + FlSpot(2, 2), + FlSpot(3.5, 2), + FlSpot(4.3, 2), + ], + barWidth: 80, + isStrokeCapRound: true, + isStepLineChart: true, + shadow: const Shadow( + color: Color(0x0100FF00), + offset: Offset(10, 15), + blurRadius: 10, + ), + ); + + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + lineBarsData: [lineChartBarData1, lineChartBarData2], + showingTooltipIndicators: [], + titlesData: const FlTitlesData(show: false), + lineTouchData: const LineTouchData( + touchSpotThreshold: 5, + ), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + expect( + lineChartPainter.getNearestTouchedSpot( + viewSize, + const Offset(30, 0), + data.lineBarsData[0], + 0, + holder, + ), + null, + ); + final result1 = lineChartPainter.getNearestTouchedSpot( + viewSize, + const Offset(30, 0), + data.lineBarsData[1], + 1, + holder, + ); + expect(result1!.barIndex, 1); + expect(result1.spotIndex, 2); + + expect( + lineChartPainter.getNearestTouchedSpot( + viewSize, + const Offset(29.99, 0), + data.lineBarsData[0], + 0, + holder, + ), + null, + ); + expect( + lineChartPainter.getNearestTouchedSpot( + viewSize, + const Offset(29.99, 0), + data.lineBarsData[1], + 1, + holder, + ), + null, + ); + + final result2 = lineChartPainter.getNearestTouchedSpot( + viewSize, + const Offset(10, 0), + data.lineBarsData[0], + 0, + holder, + ); + expect(result2!.barIndex, 0); + expect(result2.spotIndex, 0); + + final result3 = lineChartPainter.getNearestTouchedSpot( + viewSize, + const Offset(10, 0), + data.lineBarsData[1], + 1, + holder, + ); + expect(result3!.barIndex, 1); + expect(result3.spotIndex, 0); + }); + + test('test 3', () { + const viewSize = Size(100, 100); + + final lineChartBarData1 = LineChartBarData( + spots: const [ + FlSpot(1, 1), + FlSpot(4, 1), + FlSpot(6, 4), + FlSpot(8, 1), + ], + barWidth: 80, + isStrokeCapRound: true, + isStepLineChart: true, + shadow: const Shadow( + color: Color(0x0100FF00), + offset: Offset(10, 15), + blurRadius: 10, + ), + ); + + final lineChartBarData2 = LineChartBarData( + spots: const [ + FlSpot(1.1, 4), + FlSpot(2, 4), + FlSpot(3.5, 1), + FlSpot(4.3, 4), + ], + barWidth: 80, + isStrokeCapRound: true, + isStepLineChart: true, + shadow: const Shadow( + color: Color(0x0100FF00), + offset: Offset(10, 15), + blurRadius: 10, + ), + ); + + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + lineBarsData: [lineChartBarData1, lineChartBarData2], + showingTooltipIndicators: [], + titlesData: const FlTitlesData(show: false), + lineTouchData: LineTouchData( + distanceCalculator: (Offset a, Offset b) { + final dx = a.dx - b.dx; + final dy = a.dy - b.dy; + return math.sqrt(dx * dx + dy * dy); + }, + touchSpotThreshold: 5, + ), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + expect( + lineChartPainter.getNearestTouchedSpot( + viewSize, + const Offset(30, 0), + data.lineBarsData[0], + 0, + holder, + ), + null, + ); + final result1 = lineChartPainter.getNearestTouchedSpot( + viewSize, + const Offset(60, 65), + data.lineBarsData[0], + 0, + holder, + ); + expect(result1!.barIndex, 0); + expect(result1.spotIndex, 2); + + expect( + lineChartPainter.getNearestTouchedSpot( + viewSize, + const Offset(60, 65.01), + data.lineBarsData[0], + 0, + holder, + ), + null, + ); + expect( + lineChartPainter.getNearestTouchedSpot( + viewSize, + const Offset(29.99, 0), + data.lineBarsData[1], + 1, + holder, + ), + null, + ); + + final result2 = lineChartPainter.getNearestTouchedSpot( + viewSize, + const Offset(63.5, 63.5), + data.lineBarsData[0], + 0, + holder, + ); + expect(result2!.barIndex, 0); + expect(result2.spotIndex, 2); + }); + }); + + group('drawGrid()', () { + test('test 1 - none', () { + const viewSize = Size(20, 100); + + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + titlesData: const FlTitlesData(show: false), + gridData: const FlGridData( + show: false, + horizontalInterval: 2, + ), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.getBestInitialIntervalValue(any, any, any)) + .thenAnswer((realInvocation) => 0); + + lineChartPainter.drawGrid(mockCanvasWrapper, holder); + verifyNever(mockCanvasWrapper.drawDashedLine(any, any, any, any)); + }); + + test('test 2 - horizontal', () { + const viewSize = Size(20, 100); + + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + titlesData: const FlTitlesData(show: false), + gridData: FlGridData( + drawVerticalLine: false, + horizontalInterval: 2, + checkToShowHorizontalLine: (value) => value != 2 && value != 8, + getDrawingHorizontalLine: (value) { + if (value == 4) { + return const FlLine( + color: MockData.color1, + strokeWidth: 11, + dashArray: [1, 1], + ); + } else if (value == 6) { + return const FlLine( + color: MockData.color2, + strokeWidth: 22, + dashArray: [2, 2], + ); + } else { + throw StateError("We shouldn't draw these lines"); + } + }, + ), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.getBestInitialIntervalValue(any, any, any)) + .thenAnswer((realInvocation) => 0); + + final results = >[]; + when( + mockCanvasWrapper.drawDashedLine( + captureAny, + captureAny, + captureAny, + captureAny, + ), + ).thenAnswer((inv) { + results.add({ + 'from': inv.positionalArguments[0] as Offset, + 'to': inv.positionalArguments[1] as Offset, + 'paint_color': (inv.positionalArguments[2] as Paint).color, + 'paint_stroke_width': + (inv.positionalArguments[2] as Paint).strokeWidth, + 'dash_array': inv.positionalArguments[3] as List, + }); + }); + + lineChartPainter.drawGrid(mockCanvasWrapper, holder); + expect(results.length, 2); + + expect(results[0]['from'], const Offset(0, 60)); + expect(results[0]['to'], const Offset(20, 60)); + expect(results[0]['paint_color'], isSameColorAs(MockData.color1)); + expect(results[0]['paint_stroke_width'], 11); + expect(results[0]['dash_array'], [1, 1]); + + expect(results[1]['from'], const Offset(0, 40)); + expect(results[1]['to'], const Offset(20, 40)); + expect(results[1]['paint_color'], isSameColorAs(MockData.color2)); + expect(results[1]['paint_stroke_width'], 22); + expect(results[1]['dash_array'], [2, 2]); + }); + + test('test 3 - vertical', () { + const viewSize = Size(100, 20); + + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + titlesData: const FlTitlesData(show: false), + gridData: FlGridData( + drawHorizontalLine: false, + verticalInterval: 2, + checkToShowVerticalLine: (value) => value != 2 && value != 8, + getDrawingVerticalLine: (value) { + if (value == 4) { + return const FlLine( + color: MockData.color1, + strokeWidth: 11, + dashArray: [1, 1], + ); + } else if (value == 6) { + return const FlLine( + color: MockData.color2, + strokeWidth: 22, + dashArray: [2, 2], + ); + } else { + throw StateError("We shouldn't draw these lines"); + } + }, + ), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.getBestInitialIntervalValue(any, any, any)) + .thenAnswer((realInvocation) => 0); + + final results = >[]; + when( + mockCanvasWrapper.drawDashedLine( + captureAny, + captureAny, + captureAny, + captureAny, + ), + ).thenAnswer((inv) { + results.add({ + 'from': inv.positionalArguments[0] as Offset, + 'to': inv.positionalArguments[1] as Offset, + 'paint_color': (inv.positionalArguments[2] as Paint).color, + 'paint_stroke_width': + (inv.positionalArguments[2] as Paint).strokeWidth, + 'dash_array': inv.positionalArguments[3] as List, + }); + }); + + lineChartPainter.drawGrid(mockCanvasWrapper, holder); + expect(results.length, 2); + + expect(results[0]['from'], const Offset(40, 0)); + expect(results[0]['to'], const Offset(40, 20)); + expect(results[0]['paint_color'], isSameColorAs(MockData.color1)); + expect(results[0]['paint_stroke_width'], 11); + expect(results[0]['dash_array'], [1, 1]); + + expect(results[1]['from'], const Offset(60, 0)); + expect(results[1]['to'], const Offset(60, 20)); + expect(results[1]['paint_color'], isSameColorAs(MockData.color2)); + expect(results[1]['paint_stroke_width'], 22); + expect(results[1]['dash_array'], [2, 2]); + }); + + test('test 4 - both', () { + const viewSize = Size(100, 20); + + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.getEfficientInterval(any, any)) + .thenAnswer((realInvocation) => 3); + when(mockUtils.getBestInitialIntervalValue(any, any, any)) + .thenAnswer((realInvocation) => 0); + + lineChartPainter.drawGrid(mockCanvasWrapper, holder); + verify(mockCanvasWrapper.drawDashedLine(any, any, any, any)).called(6); + }); + }); + + group('drawBackground()', () { + test('test 1', () { + const viewSize = Size(20, 100); + + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + titlesData: const FlTitlesData(show: false), + backgroundColor: MockData.color1.withValues(alpha: 0), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + lineChartPainter.drawBackground(mockCanvasWrapper, holder); + verifyNever(mockCanvasWrapper.drawRect(any, any)); + }); + + test('test 2', () { + const viewSize = Size(20, 100); + + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + titlesData: const FlTitlesData(show: false), + backgroundColor: MockData.color1, + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + lineChartPainter.drawBackground(mockCanvasWrapper, holder); + final result = verify( + mockCanvasWrapper.drawRect( + const Rect.fromLTRB(0, 0, 20, 100), + captureAny, + ), + ); + expect(result.callCount, 1); + expect( + (result.captured.single as Paint).color, + isSameColorAs(MockData.color1), + ); + }); + }); + + group('drawRangeAnnotation()', () { + test('test 1 - none', () { + const viewSize = Size(20, 100); + + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + titlesData: const FlTitlesData(show: false), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + lineChartPainter.drawRangeAnnotation(mockCanvasWrapper, holder); + verifyNever(mockCanvasWrapper.drawRect(any, any)); + }); + + test('test 2 - horizontal', () { + const viewSize = Size(20, 100); + + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + titlesData: const FlTitlesData(show: false), + rangeAnnotations: RangeAnnotations( + horizontalRangeAnnotations: [ + HorizontalRangeAnnotation(y1: 4, y2: 10, color: MockData.color1), + HorizontalRangeAnnotation(y1: 12, y2: 14, color: MockData.color2), + ], + ), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final results = >[]; + when(mockCanvasWrapper.drawRect(captureAny, captureAny)) + .thenAnswer((inv) { + results.add({ + 'rect': inv.positionalArguments[0] as Rect, + 'paint_color': (inv.positionalArguments[1] as Paint).color, + }); + }); + + lineChartPainter.drawRangeAnnotation(mockCanvasWrapper, holder); + expect(results.length, 2); + + expect(results[0]['rect'], const Rect.fromLTRB(0, 0, 20, 60)); + expect(results[0]['paint_color'], isSameColorAs(MockData.color1)); + + expect(results[1]['rect'], const Rect.fromLTRB(0, -40, 20, -20)); + expect(results[1]['paint_color'], isSameColorAs(MockData.color2)); + }); + + test('test 3 - vertical', () { + const viewSize = Size(20, 100); + + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + titlesData: const FlTitlesData(show: false), + rangeAnnotations: RangeAnnotations( + verticalRangeAnnotations: [ + VerticalRangeAnnotation(x1: 1, x2: 2, color: MockData.color1), + VerticalRangeAnnotation(x1: 4, x2: 5, color: MockData.color2), + ], + ), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final results = >[]; + when(mockCanvasWrapper.drawRect(captureAny, captureAny)) + .thenAnswer((inv) { + results.add({ + 'rect': inv.positionalArguments[0] as Rect, + 'paint_color': (inv.positionalArguments[1] as Paint).color, + }); + }); + + lineChartPainter.drawRangeAnnotation(mockCanvasWrapper, holder); + expect(results.length, 2); + + expect(results[0]['rect'], const Rect.fromLTRB(2, 0, 4, 100)); + expect(results[0]['paint_color'], isSameColorAs(MockData.color1)); + + expect(results[1]['rect'], const Rect.fromLTRB(8, 0, 10, 100)); + expect(results[1]['paint_color'], isSameColorAs(MockData.color2)); + }); + + test('test 4 - both', () { + const viewSize = Size(20, 100); + + final data = LineChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + titlesData: const FlTitlesData(show: false), + rangeAnnotations: RangeAnnotations( + horizontalRangeAnnotations: [ + HorizontalRangeAnnotation(y1: 4, y2: 10, color: MockData.color1), + HorizontalRangeAnnotation(y1: 12, y2: 14, color: MockData.color2), + ], + verticalRangeAnnotations: [ + VerticalRangeAnnotation(x1: 1, x2: 2, color: MockData.color1), + VerticalRangeAnnotation(x1: 4, x2: 5, color: MockData.color2), + ], + ), + ); + + final lineChartPainter = LineChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + lineChartPainter.drawRangeAnnotation(mockCanvasWrapper, holder); + + verify(mockCanvasWrapper.drawRect(captureAny, captureAny)).called(4); + }); + }); +} diff --git a/test/chart/line_chart/line_chart_painter_test.mocks.dart b/test/chart/line_chart/line_chart_painter_test.mocks.dart new file mode 100644 index 0000000..fe7f6d5 --- /dev/null +++ b/test/chart/line_chart/line_chart_painter_test.mocks.dart @@ -0,0 +1,1588 @@ +// Mocks generated by Mockito 5.4.5 from annotations +// in fl_chart/test/chart/line_chart/line_chart_painter_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:typed_data' as _i5; +import 'dart:ui' as _i2; + +import 'package:fl_chart/fl_chart.dart' as _i7; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart' + as _i11; +import 'package:fl_chart/src/chart/line_chart/line_chart_painter.dart' as _i10; +import 'package:fl_chart/src/utils/canvas_wrapper.dart' as _i6; +import 'package:fl_chart/src/utils/utils.dart' as _i8; +import 'package:flutter/cupertino.dart' as _i3; +import 'package:flutter/foundation.dart' as _i4; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i9; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeRect_0 extends _i1.SmartFake implements _i2.Rect { + _FakeRect_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeCanvas_1 extends _i1.SmartFake implements _i2.Canvas { + _FakeCanvas_1(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeSize_2 extends _i1.SmartFake implements _i2.Size { + _FakeSize_2(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeWidget_3 extends _i1.SmartFake implements _i3.Widget { + _FakeWidget_3(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeInheritedWidget_4 extends _i1.SmartFake + implements _i3.InheritedWidget { + _FakeInheritedWidget_4(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeDiagnosticsNode_5 extends _i1.SmartFake + implements _i3.DiagnosticsNode { + _FakeDiagnosticsNode_5(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({ + _i4.TextTreeConfiguration? parentConfiguration, + _i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info, + }) => super.toString(); +} + +class _FakeOffset_6 extends _i1.SmartFake implements _i2.Offset { + _FakeOffset_6(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeBorderSide_7 extends _i1.SmartFake implements _i3.BorderSide { + _FakeBorderSide_7(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeTextStyle_8 extends _i1.SmartFake implements _i3.TextStyle { + _FakeTextStyle_8(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakePath_9 extends _i1.SmartFake implements _i2.Path { + _FakePath_9(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeAlignmentGeometry_10 extends _i1.SmartFake + implements _i3.AlignmentGeometry { + _FakeAlignmentGeometry_10(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeLinearGradient_11 extends _i1.SmartFake + implements _i3.LinearGradient { + _FakeLinearGradient_11(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +/// A class which mocks [Canvas]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCanvas extends _i1.Mock implements _i2.Canvas { + MockCanvas() { + _i1.throwOnMissingStub(this); + } + + @override + void save() => super.noSuchMethod( + Invocation.method(#save, []), + returnValueForMissingStub: null, + ); + + @override + void saveLayer(_i2.Rect? bounds, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#saveLayer, [bounds, paint]), + returnValueForMissingStub: null, + ); + + @override + void restore() => super.noSuchMethod( + Invocation.method(#restore, []), + returnValueForMissingStub: null, + ); + + @override + void restoreToCount(int? count) => super.noSuchMethod( + Invocation.method(#restoreToCount, [count]), + returnValueForMissingStub: null, + ); + + @override + int getSaveCount() => + (super.noSuchMethod(Invocation.method(#getSaveCount, []), returnValue: 0) + as int); + + @override + void translate(double? dx, double? dy) => super.noSuchMethod( + Invocation.method(#translate, [dx, dy]), + returnValueForMissingStub: null, + ); + + @override + void scale(double? sx, [double? sy]) => super.noSuchMethod( + Invocation.method(#scale, [sx, sy]), + returnValueForMissingStub: null, + ); + + @override + void rotate(double? radians) => super.noSuchMethod( + Invocation.method(#rotate, [radians]), + returnValueForMissingStub: null, + ); + + @override + void skew(double? sx, double? sy) => super.noSuchMethod( + Invocation.method(#skew, [sx, sy]), + returnValueForMissingStub: null, + ); + + @override + void transform(_i5.Float64List? matrix4) => super.noSuchMethod( + Invocation.method(#transform, [matrix4]), + returnValueForMissingStub: null, + ); + + @override + _i5.Float64List getTransform() => + (super.noSuchMethod( + Invocation.method(#getTransform, []), + returnValue: _i5.Float64List(0), + ) + as _i5.Float64List); + + @override + void clipRect( + _i2.Rect? rect, { + _i2.ClipOp? clipOp = _i2.ClipOp.intersect, + bool? doAntiAlias = true, + }) => super.noSuchMethod( + Invocation.method( + #clipRect, + [rect], + {#clipOp: clipOp, #doAntiAlias: doAntiAlias}, + ), + returnValueForMissingStub: null, + ); + + @override + void clipRRect(_i2.RRect? rrect, {bool? doAntiAlias = true}) => + super.noSuchMethod( + Invocation.method(#clipRRect, [rrect], {#doAntiAlias: doAntiAlias}), + returnValueForMissingStub: null, + ); + + @override + void clipPath(_i2.Path? path, {bool? doAntiAlias = true}) => + super.noSuchMethod( + Invocation.method(#clipPath, [path], {#doAntiAlias: doAntiAlias}), + returnValueForMissingStub: null, + ); + + @override + _i2.Rect getLocalClipBounds() => + (super.noSuchMethod( + Invocation.method(#getLocalClipBounds, []), + returnValue: _FakeRect_0( + this, + Invocation.method(#getLocalClipBounds, []), + ), + ) + as _i2.Rect); + + @override + _i2.Rect getDestinationClipBounds() => + (super.noSuchMethod( + Invocation.method(#getDestinationClipBounds, []), + returnValue: _FakeRect_0( + this, + Invocation.method(#getDestinationClipBounds, []), + ), + ) + as _i2.Rect); + + @override + void drawColor(_i2.Color? color, _i2.BlendMode? blendMode) => + super.noSuchMethod( + Invocation.method(#drawColor, [color, blendMode]), + returnValueForMissingStub: null, + ); + + @override + void drawLine(_i2.Offset? p1, _i2.Offset? p2, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawLine, [p1, p2, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawPaint(_i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawPaint, [paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRect(_i2.Rect? rect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRect, [rect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRRect(_i2.RRect? rrect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRRect, [rrect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawDRRect(_i2.RRect? outer, _i2.RRect? inner, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawDRRect, [outer, inner, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawOval(_i2.Rect? rect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawOval, [rect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawCircle(_i2.Offset? c, double? radius, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawCircle, [c, radius, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawArc( + _i2.Rect? rect, + double? startAngle, + double? sweepAngle, + bool? useCenter, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawArc, [ + rect, + startAngle, + sweepAngle, + useCenter, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawPath(_i2.Path? path, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawPath, [path, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImage(_i2.Image? image, _i2.Offset? offset, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawImage, [image, offset, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImageRect( + _i2.Image? image, + _i2.Rect? src, + _i2.Rect? dst, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawImageRect, [image, src, dst, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImageNine( + _i2.Image? image, + _i2.Rect? center, + _i2.Rect? dst, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawImageNine, [image, center, dst, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawPicture(_i2.Picture? picture) => super.noSuchMethod( + Invocation.method(#drawPicture, [picture]), + returnValueForMissingStub: null, + ); + + @override + void drawParagraph(_i2.Paragraph? paragraph, _i2.Offset? offset) => + super.noSuchMethod( + Invocation.method(#drawParagraph, [paragraph, offset]), + returnValueForMissingStub: null, + ); + + @override + void drawPoints( + _i2.PointMode? pointMode, + List<_i2.Offset>? points, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawPoints, [pointMode, points, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRawPoints( + _i2.PointMode? pointMode, + _i5.Float32List? points, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawRawPoints, [pointMode, points, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawVertices( + _i2.Vertices? vertices, + _i2.BlendMode? blendMode, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawVertices, [vertices, blendMode, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawAtlas( + _i2.Image? atlas, + List<_i2.RSTransform>? transforms, + List<_i2.Rect>? rects, + List<_i2.Color>? colors, + _i2.BlendMode? blendMode, + _i2.Rect? cullRect, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawAtlas, [ + atlas, + transforms, + rects, + colors, + blendMode, + cullRect, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawRawAtlas( + _i2.Image? atlas, + _i5.Float32List? rstTransforms, + _i5.Float32List? rects, + _i5.Int32List? colors, + _i2.BlendMode? blendMode, + _i2.Rect? cullRect, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawRawAtlas, [ + atlas, + rstTransforms, + rects, + colors, + blendMode, + cullRect, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawShadow( + _i2.Path? path, + _i2.Color? color, + double? elevation, + bool? transparentOccluder, + ) => super.noSuchMethod( + Invocation.method(#drawShadow, [ + path, + color, + elevation, + transparentOccluder, + ]), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [CanvasWrapper]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCanvasWrapper extends _i1.Mock implements _i6.CanvasWrapper { + MockCanvasWrapper() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.Canvas get canvas => + (super.noSuchMethod( + Invocation.getter(#canvas), + returnValue: _FakeCanvas_1(this, Invocation.getter(#canvas)), + ) + as _i2.Canvas); + + @override + _i2.Size get size => + (super.noSuchMethod( + Invocation.getter(#size), + returnValue: _FakeSize_2(this, Invocation.getter(#size)), + ) + as _i2.Size); + + @override + void drawRRect(_i2.RRect? rrect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRRect, [rrect, paint]), + returnValueForMissingStub: null, + ); + + @override + void save() => super.noSuchMethod( + Invocation.method(#save, []), + returnValueForMissingStub: null, + ); + + @override + void restore() => super.noSuchMethod( + Invocation.method(#restore, []), + returnValueForMissingStub: null, + ); + + @override + void clipRect( + _i2.Rect? rect, { + _i2.ClipOp? clipOp = _i2.ClipOp.intersect, + bool? doAntiAlias = true, + }) => super.noSuchMethod( + Invocation.method( + #clipRect, + [rect], + {#clipOp: clipOp, #doAntiAlias: doAntiAlias}, + ), + returnValueForMissingStub: null, + ); + + @override + void translate(double? dx, double? dy) => super.noSuchMethod( + Invocation.method(#translate, [dx, dy]), + returnValueForMissingStub: null, + ); + + @override + void rotate(double? radius) => super.noSuchMethod( + Invocation.method(#rotate, [radius]), + returnValueForMissingStub: null, + ); + + @override + void drawPath(_i2.Path? path, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawPath, [path, paint]), + returnValueForMissingStub: null, + ); + + @override + void saveLayer(_i2.Rect? bounds, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#saveLayer, [bounds, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawPicture(_i2.Picture? picture) => super.noSuchMethod( + Invocation.method(#drawPicture, [picture]), + returnValueForMissingStub: null, + ); + + @override + void drawImage(_i2.Image? image, _i2.Offset? offset, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawImage, [image, offset, paint]), + returnValueForMissingStub: null, + ); + + @override + void clipPath(_i2.Path? path, {bool? doAntiAlias = true}) => + super.noSuchMethod( + Invocation.method(#clipPath, [path], {#doAntiAlias: doAntiAlias}), + returnValueForMissingStub: null, + ); + + @override + void drawRect(_i2.Rect? rect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRect, [rect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawLine(_i2.Offset? p1, _i2.Offset? p2, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawLine, [p1, p2, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawCircle(_i2.Offset? center, double? radius, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawCircle, [center, radius, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawArc( + _i2.Rect? rect, + double? startAngle, + double? sweepAngle, + bool? useCenter, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawArc, [ + rect, + startAngle, + sweepAngle, + useCenter, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawText( + _i3.TextPainter? tp, + _i2.Offset? offset, [ + double? rotateAngle, + ]) => super.noSuchMethod( + Invocation.method(#drawText, [tp, offset, rotateAngle]), + returnValueForMissingStub: null, + ); + + @override + void drawVerticalText(_i3.TextPainter? tp, _i2.Offset? offset) => + super.noSuchMethod( + Invocation.method(#drawVerticalText, [tp, offset]), + returnValueForMissingStub: null, + ); + + @override + void drawDot( + _i7.FlDotPainter? painter, + _i7.FlSpot? spot, + _i2.Offset? offset, + ) => super.noSuchMethod( + Invocation.method(#drawDot, [painter, spot, offset]), + returnValueForMissingStub: null, + ); + + @override + void drawErrorIndicator( + _i7.FlSpotErrorRangePainter? painter, + _i7.FlSpot? origin, + _i2.Offset? offset, + _i2.Rect? errorRelativeRect, + _i7.AxisChartData? axisData, + ) => super.noSuchMethod( + Invocation.method(#drawErrorIndicator, [ + painter, + origin, + offset, + errorRelativeRect, + axisData, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawRotated({ + required _i2.Size? size, + _i2.Offset? rotationOffset = _i2.Offset.zero, + _i2.Offset? drawOffset = _i2.Offset.zero, + required double? angle, + required _i6.DrawCallback? drawCallback, + }) => super.noSuchMethod( + Invocation.method(#drawRotated, [], { + #size: size, + #rotationOffset: rotationOffset, + #drawOffset: drawOffset, + #angle: angle, + #drawCallback: drawCallback, + }), + returnValueForMissingStub: null, + ); + + @override + void drawDashedLine( + _i2.Offset? from, + _i2.Offset? to, + _i2.Paint? painter, + List? dashArray, + ) => super.noSuchMethod( + Invocation.method(#drawDashedLine, [from, to, painter, dashArray]), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [BuildContext]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockBuildContext extends _i1.Mock implements _i3.BuildContext { + MockBuildContext() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Widget get widget => + (super.noSuchMethod( + Invocation.getter(#widget), + returnValue: _FakeWidget_3(this, Invocation.getter(#widget)), + ) + as _i3.Widget); + + @override + bool get mounted => + (super.noSuchMethod(Invocation.getter(#mounted), returnValue: false) + as bool); + + @override + bool get debugDoingBuild => + (super.noSuchMethod( + Invocation.getter(#debugDoingBuild), + returnValue: false, + ) + as bool); + + @override + _i3.InheritedWidget dependOnInheritedElement( + _i3.InheritedElement? ancestor, { + Object? aspect, + }) => + (super.noSuchMethod( + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + returnValue: _FakeInheritedWidget_4( + this, + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + ), + ) + as _i3.InheritedWidget); + + @override + void visitAncestorElements(_i3.ConditionalElementVisitor? visitor) => + super.noSuchMethod( + Invocation.method(#visitAncestorElements, [visitor]), + returnValueForMissingStub: null, + ); + + @override + void visitChildElements(_i3.ElementVisitor? visitor) => super.noSuchMethod( + Invocation.method(#visitChildElements, [visitor]), + returnValueForMissingStub: null, + ); + + @override + void dispatchNotification(_i3.Notification? notification) => + super.noSuchMethod( + Invocation.method(#dispatchNotification, [notification]), + returnValueForMissingStub: null, + ); + + @override + _i3.DiagnosticsNode describeElement( + String? name, { + _i4.DiagnosticsTreeStyle? style = _i4.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method(#describeElement, [name], {#style: style}), + returnValue: _FakeDiagnosticsNode_5( + this, + Invocation.method(#describeElement, [name], {#style: style}), + ), + ) + as _i3.DiagnosticsNode); + + @override + _i3.DiagnosticsNode describeWidget( + String? name, { + _i4.DiagnosticsTreeStyle? style = _i4.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method(#describeWidget, [name], {#style: style}), + returnValue: _FakeDiagnosticsNode_5( + this, + Invocation.method(#describeWidget, [name], {#style: style}), + ), + ) + as _i3.DiagnosticsNode); + + @override + List<_i3.DiagnosticsNode> describeMissingAncestor({ + required Type? expectedAncestorType, + }) => + (super.noSuchMethod( + Invocation.method(#describeMissingAncestor, [], { + #expectedAncestorType: expectedAncestorType, + }), + returnValue: <_i3.DiagnosticsNode>[], + ) + as List<_i3.DiagnosticsNode>); + + @override + _i3.DiagnosticsNode describeOwnershipChain(String? name) => + (super.noSuchMethod( + Invocation.method(#describeOwnershipChain, [name]), + returnValue: _FakeDiagnosticsNode_5( + this, + Invocation.method(#describeOwnershipChain, [name]), + ), + ) + as _i3.DiagnosticsNode); +} + +/// A class which mocks [Utils]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockUtils extends _i1.Mock implements _i8.Utils { + MockUtils() { + _i1.throwOnMissingStub(this); + } + + @override + double radians(double? degrees) => + (super.noSuchMethod( + Invocation.method(#radians, [degrees]), + returnValue: 0.0, + ) + as double); + + @override + double degrees(double? radians) => + (super.noSuchMethod( + Invocation.method(#degrees, [radians]), + returnValue: 0.0, + ) + as double); + + @override + double translateRotatedPosition(double? size, double? degree) => + (super.noSuchMethod( + Invocation.method(#translateRotatedPosition, [size, degree]), + returnValue: 0.0, + ) + as double); + + @override + _i2.Offset calculateRotationOffset(_i2.Size? size, double? degree) => + (super.noSuchMethod( + Invocation.method(#calculateRotationOffset, [size, degree]), + returnValue: _FakeOffset_6( + this, + Invocation.method(#calculateRotationOffset, [size, degree]), + ), + ) + as _i2.Offset); + + @override + _i3.BorderRadius? normalizeBorderRadius( + _i3.BorderRadius? borderRadius, + double? width, + ) => + (super.noSuchMethod( + Invocation.method(#normalizeBorderRadius, [borderRadius, width]), + ) + as _i3.BorderRadius?); + + @override + _i3.BorderSide normalizeBorderSide( + _i3.BorderSide? borderSide, + double? width, + ) => + (super.noSuchMethod( + Invocation.method(#normalizeBorderSide, [borderSide, width]), + returnValue: _FakeBorderSide_7( + this, + Invocation.method(#normalizeBorderSide, [borderSide, width]), + ), + ) + as _i3.BorderSide); + + @override + double getEfficientInterval( + double? axisViewSize, + double? diffInAxis, { + double? pixelPerInterval = 40.0, + }) => + (super.noSuchMethod( + Invocation.method( + #getEfficientInterval, + [axisViewSize, diffInAxis], + {#pixelPerInterval: pixelPerInterval}, + ), + returnValue: 0.0, + ) + as double); + + @override + double roundInterval(double? input) => + (super.noSuchMethod( + Invocation.method(#roundInterval, [input]), + returnValue: 0.0, + ) + as double); + + @override + int getFractionDigits(double? value) => + (super.noSuchMethod( + Invocation.method(#getFractionDigits, [value]), + returnValue: 0, + ) + as int); + + @override + String formatNumber(double? axisMin, double? axisMax, double? axisValue) => + (super.noSuchMethod( + Invocation.method(#formatNumber, [axisMin, axisMax, axisValue]), + returnValue: _i9.dummyValue( + this, + Invocation.method(#formatNumber, [axisMin, axisMax, axisValue]), + ), + ) + as String); + + @override + _i3.TextStyle getThemeAwareTextStyle( + _i3.BuildContext? context, + _i3.TextStyle? providedStyle, + ) => + (super.noSuchMethod( + Invocation.method(#getThemeAwareTextStyle, [ + context, + providedStyle, + ]), + returnValue: _FakeTextStyle_8( + this, + Invocation.method(#getThemeAwareTextStyle, [ + context, + providedStyle, + ]), + ), + ) + as _i3.TextStyle); + + @override + double getBestInitialIntervalValue( + double? min, + double? max, + double? interval, { + double? baseline = 0.0, + }) => + (super.noSuchMethod( + Invocation.method( + #getBestInitialIntervalValue, + [min, max, interval], + {#baseline: baseline}, + ), + returnValue: 0.0, + ) + as double); + + @override + double convertRadiusToSigma(double? radius) => + (super.noSuchMethod( + Invocation.method(#convertRadiusToSigma, [radius]), + returnValue: 0.0, + ) + as double); +} + +/// A class which mocks [LineChartPainter]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockLineChartPainter extends _i1.Mock implements _i10.LineChartPainter { + MockLineChartPainter() { + _i1.throwOnMissingStub(this); + } + + @override + void paint( + _i3.BuildContext? context, + _i6.CanvasWrapper? canvasWrapper, + _i11.PaintHolder<_i7.LineChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#paint, [context, canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + void clipToBorder( + _i6.CanvasWrapper? canvasWrapper, + _i11.PaintHolder<_i7.LineChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#clipToBorder, [canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawBarLine( + _i6.CanvasWrapper? canvasWrapper, + _i7.LineChartBarData? barData, + _i11.PaintHolder<_i7.LineChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawBarLine, [canvasWrapper, barData, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawBetweenBarsArea( + _i6.CanvasWrapper? canvasWrapper, + _i7.LineChartData? data, + _i7.BetweenBarsData? betweenBarsData, + _i11.PaintHolder<_i7.LineChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawBetweenBarsArea, [ + canvasWrapper, + data, + betweenBarsData, + holder, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawDots( + _i6.CanvasWrapper? canvasWrapper, + _i7.LineChartBarData? barData, + _i11.PaintHolder<_i7.LineChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawDots, [canvasWrapper, barData, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawErrorIndicatorData( + _i6.CanvasWrapper? canvasWrapper, + _i7.LineChartBarData? barData, + _i11.PaintHolder<_i7.LineChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawErrorIndicatorData, [ + canvasWrapper, + barData, + holder, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawTouchedSpotsIndicator( + _i6.CanvasWrapper? canvasWrapper, + List<_i10.LineIndexDrawingInfo>? lineIndexDrawingInfo, + _i11.PaintHolder<_i7.LineChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawTouchedSpotsIndicator, [ + canvasWrapper, + lineIndexDrawingInfo, + holder, + ]), + returnValueForMissingStub: null, + ); + + @override + _i2.Path generateBarPath( + _i2.Size? viewSize, + _i7.LineChartBarData? barData, + List<_i7.FlSpot>? barSpots, + _i11.PaintHolder<_i7.LineChartData>? holder, { + _i2.Path? appendToPath, + }) => + (super.noSuchMethod( + Invocation.method( + #generateBarPath, + [viewSize, barData, barSpots, holder], + {#appendToPath: appendToPath}, + ), + returnValue: _FakePath_9( + this, + Invocation.method( + #generateBarPath, + [viewSize, barData, barSpots, holder], + {#appendToPath: appendToPath}, + ), + ), + ) + as _i2.Path); + + @override + _i2.Path generateNormalBarPath( + _i2.Size? viewSize, + _i7.LineChartBarData? barData, + List<_i7.FlSpot>? barSpots, + _i11.PaintHolder<_i7.LineChartData>? holder, { + _i2.Path? appendToPath, + }) => + (super.noSuchMethod( + Invocation.method( + #generateNormalBarPath, + [viewSize, barData, barSpots, holder], + {#appendToPath: appendToPath}, + ), + returnValue: _FakePath_9( + this, + Invocation.method( + #generateNormalBarPath, + [viewSize, barData, barSpots, holder], + {#appendToPath: appendToPath}, + ), + ), + ) + as _i2.Path); + + @override + _i2.Path generateStepBarPath( + _i2.Size? viewSize, + _i7.LineChartBarData? barData, + List<_i7.FlSpot>? barSpots, + _i11.PaintHolder<_i7.LineChartData>? holder, { + _i2.Path? appendToPath, + }) => + (super.noSuchMethod( + Invocation.method( + #generateStepBarPath, + [viewSize, barData, barSpots, holder], + {#appendToPath: appendToPath}, + ), + returnValue: _FakePath_9( + this, + Invocation.method( + #generateStepBarPath, + [viewSize, barData, barSpots, holder], + {#appendToPath: appendToPath}, + ), + ), + ) + as _i2.Path); + + @override + _i2.Path generateBelowBarPath( + _i2.Size? viewSize, + _i7.LineChartBarData? barData, + _i2.Path? barPath, + List<_i7.FlSpot>? barSpots, + _i11.PaintHolder<_i7.LineChartData>? holder, { + bool? fillCompletely = false, + }) => + (super.noSuchMethod( + Invocation.method( + #generateBelowBarPath, + [viewSize, barData, barPath, barSpots, holder], + {#fillCompletely: fillCompletely}, + ), + returnValue: _FakePath_9( + this, + Invocation.method( + #generateBelowBarPath, + [viewSize, barData, barPath, barSpots, holder], + {#fillCompletely: fillCompletely}, + ), + ), + ) + as _i2.Path); + + @override + _i2.Path generateAboveBarPath( + _i2.Size? viewSize, + _i7.LineChartBarData? barData, + _i2.Path? barPath, + List<_i7.FlSpot>? barSpots, + _i11.PaintHolder<_i7.LineChartData>? holder, { + bool? fillCompletely = false, + }) => + (super.noSuchMethod( + Invocation.method( + #generateAboveBarPath, + [viewSize, barData, barPath, barSpots, holder], + {#fillCompletely: fillCompletely}, + ), + returnValue: _FakePath_9( + this, + Invocation.method( + #generateAboveBarPath, + [viewSize, barData, barPath, barSpots, holder], + {#fillCompletely: fillCompletely}, + ), + ), + ) + as _i2.Path); + + @override + void drawBelowBar( + _i6.CanvasWrapper? canvasWrapper, + _i2.Path? belowBarPath, + _i2.Path? filledAboveBarPath, + _i11.PaintHolder<_i7.LineChartData>? holder, + _i7.LineChartBarData? barData, + ) => super.noSuchMethod( + Invocation.method(#drawBelowBar, [ + canvasWrapper, + belowBarPath, + filledAboveBarPath, + holder, + barData, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawAboveBar( + _i6.CanvasWrapper? canvasWrapper, + _i2.Path? aboveBarPath, + _i2.Path? filledBelowBarPath, + _i11.PaintHolder<_i7.LineChartData>? holder, + _i7.LineChartBarData? barData, + ) => super.noSuchMethod( + Invocation.method(#drawAboveBar, [ + canvasWrapper, + aboveBarPath, + filledBelowBarPath, + holder, + barData, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawBetweenBar( + _i6.CanvasWrapper? canvasWrapper, + _i2.Path? barPath, + _i7.BetweenBarsData? betweenBarsData, + _i2.Rect? aroundRect, + _i11.PaintHolder<_i7.LineChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawBetweenBar, [ + canvasWrapper, + barPath, + betweenBarsData, + aroundRect, + holder, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawBarShadow( + _i6.CanvasWrapper? canvasWrapper, + _i2.Path? barPath, + _i7.LineChartBarData? barData, + ) => super.noSuchMethod( + Invocation.method(#drawBarShadow, [canvasWrapper, barPath, barData]), + returnValueForMissingStub: null, + ); + + @override + void drawBar( + _i6.CanvasWrapper? canvasWrapper, + _i2.Path? barPath, + _i7.LineChartBarData? barData, + _i11.PaintHolder<_i7.LineChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawBar, [canvasWrapper, barPath, barData, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawTouchTooltip( + _i3.BuildContext? context, + _i6.CanvasWrapper? canvasWrapper, + _i7.LineTouchTooltipData? tooltipData, + _i7.FlSpot? showOnSpot, + _i7.ShowingTooltipIndicators? showingTooltipSpots, + _i11.PaintHolder<_i7.LineChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawTouchTooltip, [ + context, + canvasWrapper, + tooltipData, + showOnSpot, + showingTooltipSpots, + holder, + ]), + returnValueForMissingStub: null, + ); + + @override + double getBarLineXLength( + _i7.LineChartBarData? barData, + _i2.Size? chartUsableSize, + _i11.PaintHolder<_i7.LineChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#getBarLineXLength, [ + barData, + chartUsableSize, + holder, + ]), + returnValue: 0.0, + ) + as double); + + @override + List<_i7.TouchLineBarSpot>? handleTouch( + _i2.Offset? localPosition, + _i2.Size? size, + _i11.PaintHolder<_i7.LineChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#handleTouch, [localPosition, size, holder]), + ) + as List<_i7.TouchLineBarSpot>?); + + @override + _i7.TouchLineBarSpot? getNearestTouchedSpot( + _i2.Size? viewSize, + _i2.Offset? touchedPoint, + _i7.LineChartBarData? barData, + int? barDataPosition, + _i11.PaintHolder<_i7.LineChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#getNearestTouchedSpot, [ + viewSize, + touchedPoint, + barData, + barDataPosition, + holder, + ]), + ) + as _i7.TouchLineBarSpot?); + + @override + void drawGrid( + _i6.CanvasWrapper? canvasWrapper, + _i11.PaintHolder<_i7.LineChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawGrid, [canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawBackground( + _i6.CanvasWrapper? canvasWrapper, + _i11.PaintHolder<_i7.LineChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawBackground, [canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawRangeAnnotation( + _i6.CanvasWrapper? canvasWrapper, + _i11.PaintHolder<_i7.LineChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawRangeAnnotation, [canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawExtraLines( + _i3.BuildContext? context, + _i6.CanvasWrapper? canvasWrapper, + _i11.PaintHolder<_i7.LineChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawExtraLines, [context, canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawHorizontalLines( + _i3.BuildContext? context, + _i6.CanvasWrapper? canvasWrapper, + _i11.PaintHolder<_i7.LineChartData>? holder, + _i2.Size? viewSize, + ) => super.noSuchMethod( + Invocation.method(#drawHorizontalLines, [ + context, + canvasWrapper, + holder, + viewSize, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawVerticalLines( + _i3.BuildContext? context, + _i6.CanvasWrapper? canvasWrapper, + _i11.PaintHolder<_i7.LineChartData>? holder, + _i2.Size? viewSize, + ) => super.noSuchMethod( + Invocation.method(#drawVerticalLines, [ + context, + canvasWrapper, + holder, + viewSize, + ]), + returnValueForMissingStub: null, + ); + + @override + double getPixelX( + double? spotX, + _i2.Size? viewSize, + _i11.PaintHolder<_i7.LineChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#getPixelX, [spotX, viewSize, holder]), + returnValue: 0.0, + ) + as double); + + @override + double getPixelY( + double? spotY, + _i2.Size? viewSize, + _i11.PaintHolder<_i7.LineChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#getPixelY, [spotY, viewSize, holder]), + returnValue: 0.0, + ) + as double); + + @override + double getXForPixel( + double? pixelX, + _i2.Size? viewSize, + _i11.PaintHolder<_i7.LineChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#getXForPixel, [pixelX, viewSize, holder]), + returnValue: 0.0, + ) + as double); + + @override + double getYForPixel( + double? pixelY, + _i2.Size? viewSize, + _i11.PaintHolder<_i7.LineChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#getYForPixel, [pixelY, viewSize, holder]), + returnValue: 0.0, + ) + as double); + + @override + _i2.Offset getChartCoordinateFromPixel( + _i2.Offset? pixelOffset, + _i2.Size? viewSize, + _i11.PaintHolder<_i7.LineChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#getChartCoordinateFromPixel, [ + pixelOffset, + viewSize, + holder, + ]), + returnValue: _FakeOffset_6( + this, + Invocation.method(#getChartCoordinateFromPixel, [ + pixelOffset, + viewSize, + holder, + ]), + ), + ) + as _i2.Offset); + + @override + double getTooltipLeft( + double? dx, + double? tooltipWidth, + _i7.FLHorizontalAlignment? tooltipHorizontalAlignment, + double? tooltipHorizontalOffset, + ) => + (super.noSuchMethod( + Invocation.method(#getTooltipLeft, [ + dx, + tooltipWidth, + tooltipHorizontalAlignment, + tooltipHorizontalOffset, + ]), + returnValue: 0.0, + ) + as double); +} + +/// A class which mocks [LinearGradient]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockLinearGradient extends _i1.Mock implements _i3.LinearGradient { + @override + _i3.AlignmentGeometry get begin => + (super.noSuchMethod( + Invocation.getter(#begin), + returnValue: _FakeAlignmentGeometry_10( + this, + Invocation.getter(#begin), + ), + returnValueForMissingStub: _FakeAlignmentGeometry_10( + this, + Invocation.getter(#begin), + ), + ) + as _i3.AlignmentGeometry); + + @override + _i3.AlignmentGeometry get end => + (super.noSuchMethod( + Invocation.getter(#end), + returnValue: _FakeAlignmentGeometry_10( + this, + Invocation.getter(#end), + ), + returnValueForMissingStub: _FakeAlignmentGeometry_10( + this, + Invocation.getter(#end), + ), + ) + as _i3.AlignmentGeometry); + + @override + _i2.TileMode get tileMode => + (super.noSuchMethod( + Invocation.getter(#tileMode), + returnValue: _i2.TileMode.clamp, + returnValueForMissingStub: _i2.TileMode.clamp, + ) + as _i2.TileMode); + + @override + List<_i2.Color> get colors => + (super.noSuchMethod( + Invocation.getter(#colors), + returnValue: <_i2.Color>[], + returnValueForMissingStub: <_i2.Color>[], + ) + as List<_i2.Color>); + + @override + _i2.Shader createShader(_i2.Rect? rect, {_i2.TextDirection? textDirection}) => + (super.noSuchMethod( + Invocation.method( + #createShader, + [rect], + {#textDirection: textDirection}, + ), + returnValue: _i9.dummyValue<_i2.Shader>( + this, + Invocation.method( + #createShader, + [rect], + {#textDirection: textDirection}, + ), + ), + returnValueForMissingStub: _i9.dummyValue<_i2.Shader>( + this, + Invocation.method( + #createShader, + [rect], + {#textDirection: textDirection}, + ), + ), + ) + as _i2.Shader); + + @override + _i3.LinearGradient scale(double? factor) => + (super.noSuchMethod( + Invocation.method(#scale, [factor]), + returnValue: _FakeLinearGradient_11( + this, + Invocation.method(#scale, [factor]), + ), + returnValueForMissingStub: _FakeLinearGradient_11( + this, + Invocation.method(#scale, [factor]), + ), + ) + as _i3.LinearGradient); + + @override + _i3.Gradient? lerpFrom(_i3.Gradient? a, double? t) => + (super.noSuchMethod( + Invocation.method(#lerpFrom, [a, t]), + returnValueForMissingStub: null, + ) + as _i3.Gradient?); + + @override + _i3.Gradient? lerpTo(_i3.Gradient? b, double? t) => + (super.noSuchMethod( + Invocation.method(#lerpTo, [b, t]), + returnValueForMissingStub: null, + ) + as _i3.Gradient?); + + @override + _i3.LinearGradient withOpacity(double? opacity) => + (super.noSuchMethod( + Invocation.method(#withOpacity, [opacity]), + returnValue: _FakeLinearGradient_11( + this, + Invocation.method(#withOpacity, [opacity]), + ), + returnValueForMissingStub: _FakeLinearGradient_11( + this, + Invocation.method(#withOpacity, [opacity]), + ), + ) + as _i3.LinearGradient); +} diff --git a/test/chart/line_chart/line_chart_renderer_test.dart b/test/chart/line_chart/line_chart_renderer_test.dart new file mode 100644 index 0000000..e1c2806 --- /dev/null +++ b/test/chart/line_chart/line_chart_renderer_test.dart @@ -0,0 +1,169 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart'; +import 'package:fl_chart/src/chart/line_chart/line_chart_painter.dart'; +import 'package:fl_chart/src/chart/line_chart/line_chart_renderer.dart'; +import 'package:fl_chart/src/utils/canvas_wrapper.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import '../data_pool.dart'; +import 'line_chart_renderer_test.mocks.dart'; + +@GenerateMocks([Canvas, PaintingContext, BuildContext, LineChartPainter]) +void main() { + group('LineChartRenderer', () { + final data = LineChartData( + titlesData: const FlTitlesData( + leftTitles: AxisTitles( + sideTitles: SideTitles(reservedSize: 20, showTitles: true), + ), + rightTitles: AxisTitles( + sideTitles: SideTitles(reservedSize: 464, showTitles: true), + ), + topTitles: AxisTitles(), + bottomTitles: AxisTitles(), + ), + ); + + final targetData = LineChartData( + titlesData: const FlTitlesData( + leftTitles: AxisTitles( + sideTitles: SideTitles(reservedSize: 8, showTitles: true), + ), + rightTitles: AxisTitles( + sideTitles: SideTitles(reservedSize: 20, showTitles: true), + ), + topTitles: AxisTitles(), + bottomTitles: AxisTitles(), + ), + ); + + const textScaler = TextScaler.linear(4); + + final mockBuildContext = MockBuildContext(); + final renderLineChart = RenderLineChart( + mockBuildContext, + data, + targetData, + textScaler, + null, + canBeScaled: false, + ); + + final mockPainter = MockLineChartPainter(); + final mockPaintingContext = MockPaintingContext(); + final mockCanvas = MockCanvas(); + const mockSize = Size(44, 44); + when(mockPaintingContext.canvas).thenAnswer((realInvocation) => mockCanvas); + renderLineChart + ..mockTestSize = mockSize + ..painter = mockPainter; + + test('test 1 correct data set', () { + expect(renderLineChart.data == data, true); + expect(renderLineChart.data == targetData, false); + expect(renderLineChart.targetData == targetData, true); + expect(renderLineChart.textScaler == textScaler, true); + expect(renderLineChart.paintHolder.data == data, true); + expect(renderLineChart.paintHolder.targetData == targetData, true); + expect(renderLineChart.paintHolder.textScaler == textScaler, true); + }); + + test('test 2 check paint function', () { + renderLineChart.paint(mockPaintingContext, const Offset(10, 10)); + verify(mockCanvas.save()).called(1); + verify(mockCanvas.translate(10, 10)).called(1); + final result = verify(mockPainter.paint(any, captureAny, captureAny)); + expect(result.callCount, 1); + + final canvasWrapper = result.captured[0] as CanvasWrapper; + expect(canvasWrapper.size, const Size(44, 44)); + expect(canvasWrapper.canvas, mockCanvas); + + final paintHolder = result.captured[1] as PaintHolder; + expect(paintHolder.data, data); + expect(paintHolder.targetData, targetData); + expect(paintHolder.textScaler, textScaler); + + verify(mockCanvas.restore()).called(1); + }); + + test('test 3 check getResponseAtLocation function', () { + final results = >[]; + when(mockPainter.handleTouch(captureAny, captureAny, captureAny)) + .thenAnswer((inv) { + results.add({ + 'local_position': inv.positionalArguments[0] as Offset, + 'size': inv.positionalArguments[1] as Size, + 'paint_holder': inv.positionalArguments[2] as PaintHolder, + }); + return MockData.lineTouchResponse1.lineBarSpots; + }); + when(mockPainter.getChartCoordinateFromPixel(any, any, any)) + .thenAnswer((_) => const Offset(10, 10)); + final touchResponse = + renderLineChart.getResponseAtLocation(MockData.offset1); + expect( + touchResponse.lineBarSpots, + MockData.lineTouchResponse1.lineBarSpots, + ); + expect(touchResponse.touchChartCoordinate, const Offset(10, 10)); + expect(results[0]['local_position'] as Offset, MockData.offset1); + expect(results[0]['size'] as Size, mockSize); + final paintHolder = results[0]['paint_holder'] as PaintHolder; + expect(paintHolder.data, data); + expect(paintHolder.targetData, targetData); + expect(paintHolder.textScaler, textScaler); + }); + + test('test 4 check setters', () { + renderLineChart + ..data = targetData + ..targetData = data + ..textScaler = const TextScaler.linear(22); + + expect(renderLineChart.data, targetData); + expect(renderLineChart.targetData, data); + expect(renderLineChart.textScaler, const TextScaler.linear(22)); + }); + + test('passes chart virtual rect to paint holder', () { + final rect1 = Offset.zero & const Size(100, 100); + final renderLineChart = RenderLineChart( + mockBuildContext, + data, + targetData, + textScaler, + null, + canBeScaled: false, + ); + + expect(renderLineChart.chartVirtualRect, isNull); + expect(renderLineChart.paintHolder.chartVirtualRect, isNull); + + renderLineChart.chartVirtualRect = rect1; + + expect(renderLineChart.chartVirtualRect, rect1); + expect(renderLineChart.paintHolder.chartVirtualRect, rect1); + }); + + test('uses canBeScaled', () { + final renderLineChart = RenderLineChart( + mockBuildContext, + data, + targetData, + textScaler, + null, + canBeScaled: false, + ); + + expect(renderLineChart.canBeScaled, false); + + renderLineChart.canBeScaled = true; + + expect(renderLineChart.canBeScaled, true); + }); + }); +} diff --git a/test/chart/line_chart/line_chart_renderer_test.mocks.dart b/test/chart/line_chart/line_chart_renderer_test.mocks.dart new file mode 100644 index 0000000..538abf0 --- /dev/null +++ b/test/chart/line_chart/line_chart_renderer_test.mocks.dart @@ -0,0 +1,1343 @@ +// Mocks generated by Mockito 5.4.5 from annotations +// in fl_chart/test/chart/line_chart/line_chart_renderer_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:typed_data' as _i7; +import 'dart:ui' as _i2; + +import 'package:fl_chart/fl_chart.dart' as _i13; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart' + as _i12; +import 'package:fl_chart/src/chart/line_chart/line_chart_painter.dart' as _i10; +import 'package:fl_chart/src/utils/canvas_wrapper.dart' as _i11; +import 'package:flutter/foundation.dart' as _i5; +import 'package:flutter/gestures.dart' as _i8; +import 'package:flutter/material.dart' as _i6; +import 'package:flutter/rendering.dart' as _i3; +import 'package:flutter/src/rendering/layer.dart' as _i4; +import 'package:flutter/src/widgets/notification_listener.dart' as _i9; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeRect_0 extends _i1.SmartFake implements _i2.Rect { + _FakeRect_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeCanvas_1 extends _i1.SmartFake implements _i2.Canvas { + _FakeCanvas_1(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakePaintingContext_2 extends _i1.SmartFake + implements _i3.PaintingContext { + _FakePaintingContext_2(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeColorFilterLayer_3 extends _i1.SmartFake + implements _i4.ColorFilterLayer { + _FakeColorFilterLayer_3(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeOpacityLayer_4 extends _i1.SmartFake implements _i4.OpacityLayer { + _FakeOpacityLayer_4(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeWidget_5 extends _i1.SmartFake implements _i6.Widget { + _FakeWidget_5(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeInheritedWidget_6 extends _i1.SmartFake + implements _i6.InheritedWidget { + _FakeInheritedWidget_6(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeDiagnosticsNode_7 extends _i1.SmartFake + implements _i5.DiagnosticsNode { + _FakeDiagnosticsNode_7(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({ + _i5.TextTreeConfiguration? parentConfiguration, + _i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info, + }) => super.toString(); +} + +class _FakePath_8 extends _i1.SmartFake implements _i2.Path { + _FakePath_8(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeOffset_9 extends _i1.SmartFake implements _i2.Offset { + _FakeOffset_9(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +/// A class which mocks [Canvas]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCanvas extends _i1.Mock implements _i2.Canvas { + MockCanvas() { + _i1.throwOnMissingStub(this); + } + + @override + void save() => super.noSuchMethod( + Invocation.method(#save, []), + returnValueForMissingStub: null, + ); + + @override + void saveLayer(_i2.Rect? bounds, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#saveLayer, [bounds, paint]), + returnValueForMissingStub: null, + ); + + @override + void restore() => super.noSuchMethod( + Invocation.method(#restore, []), + returnValueForMissingStub: null, + ); + + @override + void restoreToCount(int? count) => super.noSuchMethod( + Invocation.method(#restoreToCount, [count]), + returnValueForMissingStub: null, + ); + + @override + int getSaveCount() => + (super.noSuchMethod(Invocation.method(#getSaveCount, []), returnValue: 0) + as int); + + @override + void translate(double? dx, double? dy) => super.noSuchMethod( + Invocation.method(#translate, [dx, dy]), + returnValueForMissingStub: null, + ); + + @override + void scale(double? sx, [double? sy]) => super.noSuchMethod( + Invocation.method(#scale, [sx, sy]), + returnValueForMissingStub: null, + ); + + @override + void rotate(double? radians) => super.noSuchMethod( + Invocation.method(#rotate, [radians]), + returnValueForMissingStub: null, + ); + + @override + void skew(double? sx, double? sy) => super.noSuchMethod( + Invocation.method(#skew, [sx, sy]), + returnValueForMissingStub: null, + ); + + @override + void transform(_i7.Float64List? matrix4) => super.noSuchMethod( + Invocation.method(#transform, [matrix4]), + returnValueForMissingStub: null, + ); + + @override + _i7.Float64List getTransform() => + (super.noSuchMethod( + Invocation.method(#getTransform, []), + returnValue: _i7.Float64List(0), + ) + as _i7.Float64List); + + @override + void clipRect( + _i2.Rect? rect, { + _i2.ClipOp? clipOp = _i2.ClipOp.intersect, + bool? doAntiAlias = true, + }) => super.noSuchMethod( + Invocation.method( + #clipRect, + [rect], + {#clipOp: clipOp, #doAntiAlias: doAntiAlias}, + ), + returnValueForMissingStub: null, + ); + + @override + void clipRRect(_i2.RRect? rrect, {bool? doAntiAlias = true}) => + super.noSuchMethod( + Invocation.method(#clipRRect, [rrect], {#doAntiAlias: doAntiAlias}), + returnValueForMissingStub: null, + ); + + @override + void clipPath(_i2.Path? path, {bool? doAntiAlias = true}) => + super.noSuchMethod( + Invocation.method(#clipPath, [path], {#doAntiAlias: doAntiAlias}), + returnValueForMissingStub: null, + ); + + @override + _i2.Rect getLocalClipBounds() => + (super.noSuchMethod( + Invocation.method(#getLocalClipBounds, []), + returnValue: _FakeRect_0( + this, + Invocation.method(#getLocalClipBounds, []), + ), + ) + as _i2.Rect); + + @override + _i2.Rect getDestinationClipBounds() => + (super.noSuchMethod( + Invocation.method(#getDestinationClipBounds, []), + returnValue: _FakeRect_0( + this, + Invocation.method(#getDestinationClipBounds, []), + ), + ) + as _i2.Rect); + + @override + void drawColor(_i2.Color? color, _i2.BlendMode? blendMode) => + super.noSuchMethod( + Invocation.method(#drawColor, [color, blendMode]), + returnValueForMissingStub: null, + ); + + @override + void drawLine(_i2.Offset? p1, _i2.Offset? p2, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawLine, [p1, p2, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawPaint(_i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawPaint, [paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRect(_i2.Rect? rect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRect, [rect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRRect(_i2.RRect? rrect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRRect, [rrect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawDRRect(_i2.RRect? outer, _i2.RRect? inner, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawDRRect, [outer, inner, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawOval(_i2.Rect? rect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawOval, [rect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawCircle(_i2.Offset? c, double? radius, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawCircle, [c, radius, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawArc( + _i2.Rect? rect, + double? startAngle, + double? sweepAngle, + bool? useCenter, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawArc, [ + rect, + startAngle, + sweepAngle, + useCenter, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawPath(_i2.Path? path, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawPath, [path, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImage(_i2.Image? image, _i2.Offset? offset, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawImage, [image, offset, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImageRect( + _i2.Image? image, + _i2.Rect? src, + _i2.Rect? dst, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawImageRect, [image, src, dst, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImageNine( + _i2.Image? image, + _i2.Rect? center, + _i2.Rect? dst, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawImageNine, [image, center, dst, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawPicture(_i2.Picture? picture) => super.noSuchMethod( + Invocation.method(#drawPicture, [picture]), + returnValueForMissingStub: null, + ); + + @override + void drawParagraph(_i2.Paragraph? paragraph, _i2.Offset? offset) => + super.noSuchMethod( + Invocation.method(#drawParagraph, [paragraph, offset]), + returnValueForMissingStub: null, + ); + + @override + void drawPoints( + _i2.PointMode? pointMode, + List<_i2.Offset>? points, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawPoints, [pointMode, points, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRawPoints( + _i2.PointMode? pointMode, + _i7.Float32List? points, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawRawPoints, [pointMode, points, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawVertices( + _i2.Vertices? vertices, + _i2.BlendMode? blendMode, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawVertices, [vertices, blendMode, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawAtlas( + _i2.Image? atlas, + List<_i2.RSTransform>? transforms, + List<_i2.Rect>? rects, + List<_i2.Color>? colors, + _i2.BlendMode? blendMode, + _i2.Rect? cullRect, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawAtlas, [ + atlas, + transforms, + rects, + colors, + blendMode, + cullRect, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawRawAtlas( + _i2.Image? atlas, + _i7.Float32List? rstTransforms, + _i7.Float32List? rects, + _i7.Int32List? colors, + _i2.BlendMode? blendMode, + _i2.Rect? cullRect, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawRawAtlas, [ + atlas, + rstTransforms, + rects, + colors, + blendMode, + cullRect, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawShadow( + _i2.Path? path, + _i2.Color? color, + double? elevation, + bool? transparentOccluder, + ) => super.noSuchMethod( + Invocation.method(#drawShadow, [ + path, + color, + elevation, + transparentOccluder, + ]), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [PaintingContext]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPaintingContext extends _i1.Mock implements _i3.PaintingContext { + MockPaintingContext() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.Rect get estimatedBounds => + (super.noSuchMethod( + Invocation.getter(#estimatedBounds), + returnValue: _FakeRect_0(this, Invocation.getter(#estimatedBounds)), + ) + as _i2.Rect); + + @override + _i2.Canvas get canvas => + (super.noSuchMethod( + Invocation.getter(#canvas), + returnValue: _FakeCanvas_1(this, Invocation.getter(#canvas)), + ) + as _i2.Canvas); + + @override + void paintChild(_i3.RenderObject? child, _i2.Offset? offset) => + super.noSuchMethod( + Invocation.method(#paintChild, [child, offset]), + returnValueForMissingStub: null, + ); + + @override + void appendLayer(_i4.Layer? layer) => super.noSuchMethod( + Invocation.method(#appendLayer, [layer]), + returnValueForMissingStub: null, + ); + + @override + _i2.VoidCallback addCompositionCallback(_i4.CompositionCallback? callback) => + (super.noSuchMethod( + Invocation.method(#addCompositionCallback, [callback]), + returnValue: () {}, + ) + as _i2.VoidCallback); + + @override + void stopRecordingIfNeeded() => super.noSuchMethod( + Invocation.method(#stopRecordingIfNeeded, []), + returnValueForMissingStub: null, + ); + + @override + void setIsComplexHint() => super.noSuchMethod( + Invocation.method(#setIsComplexHint, []), + returnValueForMissingStub: null, + ); + + @override + void setWillChangeHint() => super.noSuchMethod( + Invocation.method(#setWillChangeHint, []), + returnValueForMissingStub: null, + ); + + @override + void addLayer(_i4.Layer? layer) => super.noSuchMethod( + Invocation.method(#addLayer, [layer]), + returnValueForMissingStub: null, + ); + + @override + void pushLayer( + _i4.ContainerLayer? childLayer, + _i3.PaintingContextCallback? painter, + _i2.Offset? offset, { + _i2.Rect? childPaintBounds, + }) => super.noSuchMethod( + Invocation.method( + #pushLayer, + [childLayer, painter, offset], + {#childPaintBounds: childPaintBounds}, + ), + returnValueForMissingStub: null, + ); + + @override + _i3.PaintingContext createChildContext( + _i4.ContainerLayer? childLayer, + _i2.Rect? bounds, + ) => + (super.noSuchMethod( + Invocation.method(#createChildContext, [childLayer, bounds]), + returnValue: _FakePaintingContext_2( + this, + Invocation.method(#createChildContext, [childLayer, bounds]), + ), + ) + as _i3.PaintingContext); + + @override + _i4.ClipRectLayer? pushClipRect( + bool? needsCompositing, + _i2.Offset? offset, + _i2.Rect? clipRect, + _i3.PaintingContextCallback? painter, { + _i2.Clip? clipBehavior = _i2.Clip.hardEdge, + _i4.ClipRectLayer? oldLayer, + }) => + (super.noSuchMethod( + Invocation.method( + #pushClipRect, + [needsCompositing, offset, clipRect, painter], + {#clipBehavior: clipBehavior, #oldLayer: oldLayer}, + ), + ) + as _i4.ClipRectLayer?); + + @override + _i4.ClipRRectLayer? pushClipRRect( + bool? needsCompositing, + _i2.Offset? offset, + _i2.Rect? bounds, + _i2.RRect? clipRRect, + _i3.PaintingContextCallback? painter, { + _i2.Clip? clipBehavior = _i2.Clip.antiAlias, + _i4.ClipRRectLayer? oldLayer, + }) => + (super.noSuchMethod( + Invocation.method( + #pushClipRRect, + [needsCompositing, offset, bounds, clipRRect, painter], + {#clipBehavior: clipBehavior, #oldLayer: oldLayer}, + ), + ) + as _i4.ClipRRectLayer?); + + @override + _i4.ClipPathLayer? pushClipPath( + bool? needsCompositing, + _i2.Offset? offset, + _i2.Rect? bounds, + _i2.Path? clipPath, + _i3.PaintingContextCallback? painter, { + _i2.Clip? clipBehavior = _i2.Clip.antiAlias, + _i4.ClipPathLayer? oldLayer, + }) => + (super.noSuchMethod( + Invocation.method( + #pushClipPath, + [needsCompositing, offset, bounds, clipPath, painter], + {#clipBehavior: clipBehavior, #oldLayer: oldLayer}, + ), + ) + as _i4.ClipPathLayer?); + + @override + _i4.ColorFilterLayer pushColorFilter( + _i2.Offset? offset, + _i2.ColorFilter? colorFilter, + _i3.PaintingContextCallback? painter, { + _i4.ColorFilterLayer? oldLayer, + }) => + (super.noSuchMethod( + Invocation.method( + #pushColorFilter, + [offset, colorFilter, painter], + {#oldLayer: oldLayer}, + ), + returnValue: _FakeColorFilterLayer_3( + this, + Invocation.method( + #pushColorFilter, + [offset, colorFilter, painter], + {#oldLayer: oldLayer}, + ), + ), + ) + as _i4.ColorFilterLayer); + + @override + _i4.TransformLayer? pushTransform( + bool? needsCompositing, + _i2.Offset? offset, + _i8.Matrix4? transform, + _i3.PaintingContextCallback? painter, { + _i4.TransformLayer? oldLayer, + }) => + (super.noSuchMethod( + Invocation.method( + #pushTransform, + [needsCompositing, offset, transform, painter], + {#oldLayer: oldLayer}, + ), + ) + as _i4.TransformLayer?); + + @override + _i4.OpacityLayer pushOpacity( + _i2.Offset? offset, + int? alpha, + _i3.PaintingContextCallback? painter, { + _i4.OpacityLayer? oldLayer, + }) => + (super.noSuchMethod( + Invocation.method( + #pushOpacity, + [offset, alpha, painter], + {#oldLayer: oldLayer}, + ), + returnValue: _FakeOpacityLayer_4( + this, + Invocation.method( + #pushOpacity, + [offset, alpha, painter], + {#oldLayer: oldLayer}, + ), + ), + ) + as _i4.OpacityLayer); + + @override + void clipPathAndPaint( + _i2.Path? path, + _i2.Clip? clipBehavior, + _i2.Rect? bounds, + _i2.VoidCallback? painter, + ) => super.noSuchMethod( + Invocation.method(#clipPathAndPaint, [path, clipBehavior, bounds, painter]), + returnValueForMissingStub: null, + ); + + @override + void clipRRectAndPaint( + _i2.RRect? rrect, + _i2.Clip? clipBehavior, + _i2.Rect? bounds, + _i2.VoidCallback? painter, + ) => super.noSuchMethod( + Invocation.method(#clipRRectAndPaint, [ + rrect, + clipBehavior, + bounds, + painter, + ]), + returnValueForMissingStub: null, + ); + + @override + void clipRectAndPaint( + _i2.Rect? rect, + _i2.Clip? clipBehavior, + _i2.Rect? bounds, + _i2.VoidCallback? painter, + ) => super.noSuchMethod( + Invocation.method(#clipRectAndPaint, [rect, clipBehavior, bounds, painter]), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [BuildContext]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockBuildContext extends _i1.Mock implements _i6.BuildContext { + MockBuildContext() { + _i1.throwOnMissingStub(this); + } + + @override + _i6.Widget get widget => + (super.noSuchMethod( + Invocation.getter(#widget), + returnValue: _FakeWidget_5(this, Invocation.getter(#widget)), + ) + as _i6.Widget); + + @override + bool get mounted => + (super.noSuchMethod(Invocation.getter(#mounted), returnValue: false) + as bool); + + @override + bool get debugDoingBuild => + (super.noSuchMethod( + Invocation.getter(#debugDoingBuild), + returnValue: false, + ) + as bool); + + @override + _i6.InheritedWidget dependOnInheritedElement( + _i6.InheritedElement? ancestor, { + Object? aspect, + }) => + (super.noSuchMethod( + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + returnValue: _FakeInheritedWidget_6( + this, + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + ), + ) + as _i6.InheritedWidget); + + @override + void visitAncestorElements(_i6.ConditionalElementVisitor? visitor) => + super.noSuchMethod( + Invocation.method(#visitAncestorElements, [visitor]), + returnValueForMissingStub: null, + ); + + @override + void visitChildElements(_i6.ElementVisitor? visitor) => super.noSuchMethod( + Invocation.method(#visitChildElements, [visitor]), + returnValueForMissingStub: null, + ); + + @override + void dispatchNotification(_i9.Notification? notification) => + super.noSuchMethod( + Invocation.method(#dispatchNotification, [notification]), + returnValueForMissingStub: null, + ); + + @override + _i5.DiagnosticsNode describeElement( + String? name, { + _i5.DiagnosticsTreeStyle? style = _i5.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method(#describeElement, [name], {#style: style}), + returnValue: _FakeDiagnosticsNode_7( + this, + Invocation.method(#describeElement, [name], {#style: style}), + ), + ) + as _i5.DiagnosticsNode); + + @override + _i5.DiagnosticsNode describeWidget( + String? name, { + _i5.DiagnosticsTreeStyle? style = _i5.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method(#describeWidget, [name], {#style: style}), + returnValue: _FakeDiagnosticsNode_7( + this, + Invocation.method(#describeWidget, [name], {#style: style}), + ), + ) + as _i5.DiagnosticsNode); + + @override + List<_i5.DiagnosticsNode> describeMissingAncestor({ + required Type? expectedAncestorType, + }) => + (super.noSuchMethod( + Invocation.method(#describeMissingAncestor, [], { + #expectedAncestorType: expectedAncestorType, + }), + returnValue: <_i5.DiagnosticsNode>[], + ) + as List<_i5.DiagnosticsNode>); + + @override + _i5.DiagnosticsNode describeOwnershipChain(String? name) => + (super.noSuchMethod( + Invocation.method(#describeOwnershipChain, [name]), + returnValue: _FakeDiagnosticsNode_7( + this, + Invocation.method(#describeOwnershipChain, [name]), + ), + ) + as _i5.DiagnosticsNode); +} + +/// A class which mocks [LineChartPainter]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockLineChartPainter extends _i1.Mock implements _i10.LineChartPainter { + MockLineChartPainter() { + _i1.throwOnMissingStub(this); + } + + @override + void paint( + _i6.BuildContext? context, + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.LineChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#paint, [context, canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + void clipToBorder( + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.LineChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#clipToBorder, [canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawBarLine( + _i11.CanvasWrapper? canvasWrapper, + _i13.LineChartBarData? barData, + _i12.PaintHolder<_i13.LineChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawBarLine, [canvasWrapper, barData, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawBetweenBarsArea( + _i11.CanvasWrapper? canvasWrapper, + _i13.LineChartData? data, + _i13.BetweenBarsData? betweenBarsData, + _i12.PaintHolder<_i13.LineChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawBetweenBarsArea, [ + canvasWrapper, + data, + betweenBarsData, + holder, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawDots( + _i11.CanvasWrapper? canvasWrapper, + _i13.LineChartBarData? barData, + _i12.PaintHolder<_i13.LineChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawDots, [canvasWrapper, barData, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawErrorIndicatorData( + _i11.CanvasWrapper? canvasWrapper, + _i13.LineChartBarData? barData, + _i12.PaintHolder<_i13.LineChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawErrorIndicatorData, [ + canvasWrapper, + barData, + holder, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawTouchedSpotsIndicator( + _i11.CanvasWrapper? canvasWrapper, + List<_i10.LineIndexDrawingInfo>? lineIndexDrawingInfo, + _i12.PaintHolder<_i13.LineChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawTouchedSpotsIndicator, [ + canvasWrapper, + lineIndexDrawingInfo, + holder, + ]), + returnValueForMissingStub: null, + ); + + @override + _i2.Path generateBarPath( + _i2.Size? viewSize, + _i13.LineChartBarData? barData, + List<_i13.FlSpot>? barSpots, + _i12.PaintHolder<_i13.LineChartData>? holder, { + _i2.Path? appendToPath, + }) => + (super.noSuchMethod( + Invocation.method( + #generateBarPath, + [viewSize, barData, barSpots, holder], + {#appendToPath: appendToPath}, + ), + returnValue: _FakePath_8( + this, + Invocation.method( + #generateBarPath, + [viewSize, barData, barSpots, holder], + {#appendToPath: appendToPath}, + ), + ), + ) + as _i2.Path); + + @override + _i2.Path generateNormalBarPath( + _i2.Size? viewSize, + _i13.LineChartBarData? barData, + List<_i13.FlSpot>? barSpots, + _i12.PaintHolder<_i13.LineChartData>? holder, { + _i2.Path? appendToPath, + }) => + (super.noSuchMethod( + Invocation.method( + #generateNormalBarPath, + [viewSize, barData, barSpots, holder], + {#appendToPath: appendToPath}, + ), + returnValue: _FakePath_8( + this, + Invocation.method( + #generateNormalBarPath, + [viewSize, barData, barSpots, holder], + {#appendToPath: appendToPath}, + ), + ), + ) + as _i2.Path); + + @override + _i2.Path generateStepBarPath( + _i2.Size? viewSize, + _i13.LineChartBarData? barData, + List<_i13.FlSpot>? barSpots, + _i12.PaintHolder<_i13.LineChartData>? holder, { + _i2.Path? appendToPath, + }) => + (super.noSuchMethod( + Invocation.method( + #generateStepBarPath, + [viewSize, barData, barSpots, holder], + {#appendToPath: appendToPath}, + ), + returnValue: _FakePath_8( + this, + Invocation.method( + #generateStepBarPath, + [viewSize, barData, barSpots, holder], + {#appendToPath: appendToPath}, + ), + ), + ) + as _i2.Path); + + @override + _i2.Path generateBelowBarPath( + _i2.Size? viewSize, + _i13.LineChartBarData? barData, + _i2.Path? barPath, + List<_i13.FlSpot>? barSpots, + _i12.PaintHolder<_i13.LineChartData>? holder, { + bool? fillCompletely = false, + }) => + (super.noSuchMethod( + Invocation.method( + #generateBelowBarPath, + [viewSize, barData, barPath, barSpots, holder], + {#fillCompletely: fillCompletely}, + ), + returnValue: _FakePath_8( + this, + Invocation.method( + #generateBelowBarPath, + [viewSize, barData, barPath, barSpots, holder], + {#fillCompletely: fillCompletely}, + ), + ), + ) + as _i2.Path); + + @override + _i2.Path generateAboveBarPath( + _i2.Size? viewSize, + _i13.LineChartBarData? barData, + _i2.Path? barPath, + List<_i13.FlSpot>? barSpots, + _i12.PaintHolder<_i13.LineChartData>? holder, { + bool? fillCompletely = false, + }) => + (super.noSuchMethod( + Invocation.method( + #generateAboveBarPath, + [viewSize, barData, barPath, barSpots, holder], + {#fillCompletely: fillCompletely}, + ), + returnValue: _FakePath_8( + this, + Invocation.method( + #generateAboveBarPath, + [viewSize, barData, barPath, barSpots, holder], + {#fillCompletely: fillCompletely}, + ), + ), + ) + as _i2.Path); + + @override + void drawBelowBar( + _i11.CanvasWrapper? canvasWrapper, + _i2.Path? belowBarPath, + _i2.Path? filledAboveBarPath, + _i12.PaintHolder<_i13.LineChartData>? holder, + _i13.LineChartBarData? barData, + ) => super.noSuchMethod( + Invocation.method(#drawBelowBar, [ + canvasWrapper, + belowBarPath, + filledAboveBarPath, + holder, + barData, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawAboveBar( + _i11.CanvasWrapper? canvasWrapper, + _i2.Path? aboveBarPath, + _i2.Path? filledBelowBarPath, + _i12.PaintHolder<_i13.LineChartData>? holder, + _i13.LineChartBarData? barData, + ) => super.noSuchMethod( + Invocation.method(#drawAboveBar, [ + canvasWrapper, + aboveBarPath, + filledBelowBarPath, + holder, + barData, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawBetweenBar( + _i11.CanvasWrapper? canvasWrapper, + _i2.Path? barPath, + _i13.BetweenBarsData? betweenBarsData, + _i2.Rect? aroundRect, + _i12.PaintHolder<_i13.LineChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawBetweenBar, [ + canvasWrapper, + barPath, + betweenBarsData, + aroundRect, + holder, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawBarShadow( + _i11.CanvasWrapper? canvasWrapper, + _i2.Path? barPath, + _i13.LineChartBarData? barData, + ) => super.noSuchMethod( + Invocation.method(#drawBarShadow, [canvasWrapper, barPath, barData]), + returnValueForMissingStub: null, + ); + + @override + void drawBar( + _i11.CanvasWrapper? canvasWrapper, + _i2.Path? barPath, + _i13.LineChartBarData? barData, + _i12.PaintHolder<_i13.LineChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawBar, [canvasWrapper, barPath, barData, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawTouchTooltip( + _i6.BuildContext? context, + _i11.CanvasWrapper? canvasWrapper, + _i13.LineTouchTooltipData? tooltipData, + _i13.FlSpot? showOnSpot, + _i13.ShowingTooltipIndicators? showingTooltipSpots, + _i12.PaintHolder<_i13.LineChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawTouchTooltip, [ + context, + canvasWrapper, + tooltipData, + showOnSpot, + showingTooltipSpots, + holder, + ]), + returnValueForMissingStub: null, + ); + + @override + double getBarLineXLength( + _i13.LineChartBarData? barData, + _i2.Size? chartUsableSize, + _i12.PaintHolder<_i13.LineChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#getBarLineXLength, [ + barData, + chartUsableSize, + holder, + ]), + returnValue: 0.0, + ) + as double); + + @override + List<_i13.TouchLineBarSpot>? handleTouch( + _i2.Offset? localPosition, + _i2.Size? size, + _i12.PaintHolder<_i13.LineChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#handleTouch, [localPosition, size, holder]), + ) + as List<_i13.TouchLineBarSpot>?); + + @override + _i13.TouchLineBarSpot? getNearestTouchedSpot( + _i2.Size? viewSize, + _i2.Offset? touchedPoint, + _i13.LineChartBarData? barData, + int? barDataPosition, + _i12.PaintHolder<_i13.LineChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#getNearestTouchedSpot, [ + viewSize, + touchedPoint, + barData, + barDataPosition, + holder, + ]), + ) + as _i13.TouchLineBarSpot?); + + @override + void drawGrid( + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.LineChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawGrid, [canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawBackground( + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.LineChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawBackground, [canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawRangeAnnotation( + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.LineChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawRangeAnnotation, [canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawExtraLines( + _i6.BuildContext? context, + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.LineChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawExtraLines, [context, canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawHorizontalLines( + _i6.BuildContext? context, + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.LineChartData>? holder, + _i2.Size? viewSize, + ) => super.noSuchMethod( + Invocation.method(#drawHorizontalLines, [ + context, + canvasWrapper, + holder, + viewSize, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawVerticalLines( + _i6.BuildContext? context, + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.LineChartData>? holder, + _i2.Size? viewSize, + ) => super.noSuchMethod( + Invocation.method(#drawVerticalLines, [ + context, + canvasWrapper, + holder, + viewSize, + ]), + returnValueForMissingStub: null, + ); + + @override + double getPixelX( + double? spotX, + _i2.Size? viewSize, + _i12.PaintHolder<_i13.LineChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#getPixelX, [spotX, viewSize, holder]), + returnValue: 0.0, + ) + as double); + + @override + double getPixelY( + double? spotY, + _i2.Size? viewSize, + _i12.PaintHolder<_i13.LineChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#getPixelY, [spotY, viewSize, holder]), + returnValue: 0.0, + ) + as double); + + @override + double getXForPixel( + double? pixelX, + _i2.Size? viewSize, + _i12.PaintHolder<_i13.LineChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#getXForPixel, [pixelX, viewSize, holder]), + returnValue: 0.0, + ) + as double); + + @override + double getYForPixel( + double? pixelY, + _i2.Size? viewSize, + _i12.PaintHolder<_i13.LineChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#getYForPixel, [pixelY, viewSize, holder]), + returnValue: 0.0, + ) + as double); + + @override + _i2.Offset getChartCoordinateFromPixel( + _i2.Offset? pixelOffset, + _i2.Size? viewSize, + _i12.PaintHolder<_i13.LineChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#getChartCoordinateFromPixel, [ + pixelOffset, + viewSize, + holder, + ]), + returnValue: _FakeOffset_9( + this, + Invocation.method(#getChartCoordinateFromPixel, [ + pixelOffset, + viewSize, + holder, + ]), + ), + ) + as _i2.Offset); + + @override + double getTooltipLeft( + double? dx, + double? tooltipWidth, + _i13.FLHorizontalAlignment? tooltipHorizontalAlignment, + double? tooltipHorizontalOffset, + ) => + (super.noSuchMethod( + Invocation.method(#getTooltipLeft, [ + dx, + tooltipWidth, + tooltipHorizontalAlignment, + tooltipHorizontalOffset, + ]), + returnValue: 0.0, + ) + as double); +} diff --git a/test/chart/line_chart/line_chart_test.dart b/test/chart/line_chart/line_chart_test.dart new file mode 100644 index 0000000..48a34d3 --- /dev/null +++ b/test/chart/line_chart/line_chart_test.dart @@ -0,0 +1,830 @@ +import 'package:fl_chart/src/chart/base/axis_chart/axis_chart_scaffold_widget.dart'; +import 'package:fl_chart/src/chart/base/axis_chart/scale_axis.dart'; +import 'package:fl_chart/src/chart/base/axis_chart/transformation_config.dart'; +import 'package:fl_chart/src/chart/line_chart/line_chart.dart'; +import 'package:fl_chart/src/chart/line_chart/line_chart_data.dart'; +import 'package:fl_chart/src/chart/line_chart/line_chart_renderer.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + Widget createTestWidget({ + required LineChart chart, + }) { + return MaterialApp( + home: chart, + ); + } + + group('LineChart', () { + testWidgets('has correct default values', (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: LineChart( + LineChartData(), + ), + ), + ); + + final lineChart = tester.widget(find.byType(LineChart)); + expect(lineChart.transformationConfig, const FlTransformationConfig()); + }); + + testWidgets('passes interaction parameters to AxisChartScaffoldWidget', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: LineChart( + LineChartData(), + ), + ), + ); + + final axisChartScaffoldWidget = tester.widget( + find.byType(AxisChartScaffoldWidget), + ); + + expect( + axisChartScaffoldWidget.transformationConfig, + const FlTransformationConfig(), + ); + + await tester.pumpAndSettle(); + + final transformationConfig = FlTransformationConfig( + scaleAxis: FlScaleAxis.free, + trackpadScrollCausesScale: true, + maxScale: 10, + minScale: 1.5, + transformationController: TransformationController(), + ); + await tester.pumpWidget( + createTestWidget( + chart: LineChart( + LineChartData(), + transformationConfig: transformationConfig, + ), + ), + ); + + final axisChartScaffoldWidget1 = tester.widget( + find.byType(AxisChartScaffoldWidget), + ); + + expect( + axisChartScaffoldWidget1.transformationConfig, + transformationConfig, + ); + }); + + for (final scaleAxis in FlScaleAxis.scalingEnabledAxis) { + testWidgets('passes canBeScaled true for $scaleAxis', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: LineChart( + LineChartData(), + transformationConfig: FlTransformationConfig( + scaleAxis: scaleAxis, + ), + ), + ), + ); + + final lineChartLeaf = tester.widget( + find.byType(LineChartLeaf), + ); + expect(lineChartLeaf.canBeScaled, true); + }); + } + + testWidgets('passes canBeScaled false for FlScaleAxis.none', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: LineChart( + LineChartData(), + // This is for test + // ignore: avoid_redundant_argument_values + transformationConfig: const FlTransformationConfig( + // This is for test + // ignore: avoid_redundant_argument_values + scaleAxis: FlScaleAxis.none, + ), + ), + ), + ); + + final lineChartLeaf = tester.widget( + find.byType(LineChartLeaf), + ); + expect(lineChartLeaf.canBeScaled, false); + }); + + group('touch gesture', () { + testWidgets('does not scale with FlScaleAxis.none', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: LineChart( + LineChartData(), + ), + ), + ); + + final lineChartCenterOffset = tester.getCenter( + find.byType(LineChartLeaf), + ); + final scaleStart1 = lineChartCenterOffset; + final scaleStart2 = lineChartCenterOffset; + final scaleEnd1 = lineChartCenterOffset + const Offset(100, 100); + final scaleEnd2 = lineChartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await tester.pump(); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await tester.pump(); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final lineChartLeaf = tester.widget( + find.byType(LineChartLeaf), + ); + + expect(lineChartLeaf.chartVirtualRect, isNull); + }); + + testWidgets('scales freely with FlScaleAxis.free', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: LineChart( + LineChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.free, + ), + ), + ), + ); + + final lineChartCenterOffset = tester.getCenter( + find.byType(LineChartLeaf), + ); + final scaleStart1 = lineChartCenterOffset; + final scaleStart2 = lineChartCenterOffset; + final scaleEnd1 = lineChartCenterOffset + const Offset(100, 100); + final scaleEnd2 = lineChartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await tester.pump(); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await tester.pump(); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final lineChartLeaf = tester.widget( + find.byType(LineChartLeaf), + ); + final renderBox = tester.renderObject( + find.byType(LineChartLeaf), + ); + final chartVirtualRect = lineChartLeaf.chartVirtualRect!; + + expect(chartVirtualRect.size, greaterThan(renderBox.size)); + expect(chartVirtualRect.left, isNegative); + expect(chartVirtualRect.top, isNegative); + }); + + testWidgets('scales horizontally with FlScaleAxis.horizontal', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: LineChart( + LineChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.horizontal, + ), + ), + ), + ); + + final chartCenterOffset = tester.getCenter(find.byType(LineChartLeaf)); + final scaleStart1 = chartCenterOffset; + final scaleStart2 = chartCenterOffset; + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await tester.pump(); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await tester.pump(); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final lineChartLeaf = tester.widget( + find.byType(LineChartLeaf), + ); + final renderBox = tester.renderObject( + find.byType(LineChartLeaf), + ); + final chartVirtualRect = lineChartLeaf.chartVirtualRect!; + + expect(chartVirtualRect.size.height, renderBox.size.height); + expect(chartVirtualRect.size.width, greaterThan(renderBox.size.width)); + expect(chartVirtualRect.left, isNegative); + expect(chartVirtualRect.top, 0); + }); + + testWidgets('scales vertically with FlScaleAxis.vertical', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: LineChart( + LineChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.vertical, + ), + ), + ), + ); + + final chartCenterOffset = tester.getCenter(find.byType(LineChartLeaf)); + final scaleStart1 = chartCenterOffset; + final scaleStart2 = chartCenterOffset; + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await tester.pump(); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await tester.pump(); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final lineChartLeaf = tester.widget( + find.byType(LineChartLeaf), + ); + final renderBox = tester.renderObject( + find.byType(LineChartLeaf), + ); + final chartVirtualRect = lineChartLeaf.chartVirtualRect!; + + expect( + chartVirtualRect.size.height, + greaterThan(renderBox.size.height), + ); + expect(chartVirtualRect.size.width, renderBox.size.width); + expect(chartVirtualRect.left, 0); + expect(chartVirtualRect.top, isNegative); + }); + + group('pans', () { + testWidgets('only horizontally with FlScaleAxis.horizontal', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: LineChart( + LineChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.horizontal, + ), + ), + ), + ); + + final chartCenterOffset = tester.getCenter( + find.byType(LineChartLeaf), + ); + final scaleStart1 = chartCenterOffset; + final scaleStart2 = chartCenterOffset; + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final lineChartLeafBeforePan = tester.widget( + find.byType(LineChartLeaf), + ); + final chartVirtualRectBeforePan = + lineChartLeafBeforePan.chartVirtualRect!; + expect(chartVirtualRectBeforePan.top, 0); + + const panOffset = Offset(100, 100); + await tester.dragFrom(chartCenterOffset, panOffset); + await tester.pumpAndSettle(); + + final lineChartLeafAfterPan = tester.widget( + find.byType(LineChartLeaf), + ); + final chartVirtualRectAfterPan = + lineChartLeafAfterPan.chartVirtualRect!; + + expect(chartVirtualRectBeforePan.size, chartVirtualRectAfterPan.size); + expect( + chartVirtualRectAfterPan.left, + greaterThan(chartVirtualRectBeforePan.left), + ); + expect(chartVirtualRectAfterPan.top, 0); + }); + + testWidgets('only vertically with FlScaleAxis.vertical', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: LineChart( + LineChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.vertical, + ), + ), + ), + ); + + final chartCenterOffset = tester.getCenter( + find.byType(LineChartLeaf), + ); + final scaleStart1 = chartCenterOffset; + final scaleStart2 = chartCenterOffset; + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final lineChartLeafBeforePan = tester.widget( + find.byType(LineChartLeaf), + ); + final chartVirtualRectBeforePan = + lineChartLeafBeforePan.chartVirtualRect!; + expect(chartVirtualRectBeforePan.left, 0); + + const panOffset = Offset(100, 100); + await tester.dragFrom(chartCenterOffset, panOffset); + await tester.pumpAndSettle(); + + final lineChartLeafAfterPan = tester.widget( + find.byType(LineChartLeaf), + ); + final chartVirtualRectAfterPan = + lineChartLeafAfterPan.chartVirtualRect!; + + expect(chartVirtualRectAfterPan.left, 0); + expect( + chartVirtualRectAfterPan.top, + greaterThan(chartVirtualRectBeforePan.top), + ); + }); + + testWidgets('freely with FlScaleAxis.free', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: LineChart( + LineChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.free, + ), + ), + ), + ); + + final chartCenterOffset = tester.getCenter( + find.byType(LineChartLeaf), + ); + final scaleStart1 = chartCenterOffset; + final scaleStart2 = chartCenterOffset; + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final lineChartLeafBeforePan = tester.widget( + find.byType(LineChartLeaf), + ); + final chartVirtualRectBeforePan = + lineChartLeafBeforePan.chartVirtualRect!; + expect(chartVirtualRectBeforePan.left, isNegative); + expect(chartVirtualRectBeforePan.top, isNegative); + + const panOffset = Offset(100, 100); + await tester.dragFrom(chartCenterOffset, panOffset); + await tester.pumpAndSettle(); + + final lineChartLeafAfterPan = tester.widget( + find.byType(LineChartLeaf), + ); + final chartVirtualRectAfterPan = + lineChartLeafAfterPan.chartVirtualRect!; + + expect( + chartVirtualRectAfterPan.left, + greaterThan(chartVirtualRectBeforePan.left), + ); + expect( + chartVirtualRectAfterPan.top, + greaterThan(chartVirtualRectBeforePan.top), + ); + }); + }); + }); + + group('trackpad scroll', () { + group('pans', () { + testWidgets('only horizontally with FlScaleAxis.horizontal', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: LineChart( + LineChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.horizontal, + ), + ), + ), + ); + + final chartCenterOffset = tester.getCenter( + find.byType(LineChartLeaf), + ); + final scaleStart1 = chartCenterOffset; + final scaleStart2 = chartCenterOffset; + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await tester.pump(); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await tester.pump(); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final lineChartLeafBeforePan = tester.widget( + find.byType(LineChartLeaf), + ); + + final chartVirtualRectBeforePan = + lineChartLeafBeforePan.chartVirtualRect!; + expect(chartVirtualRectBeforePan.top, 0); + + final pointer = TestPointer(1, PointerDeviceKind.trackpad); + const leftAndUp = Offset(-100, -100); + await tester.sendEventToBinding(pointer.hover(chartCenterOffset)); + await tester.pump(); + await tester.sendEventToBinding(pointer.scroll(leftAndUp)); + await tester.pump(); + + final lineChartLeafAfterPan = tester.widget( + find.byType(LineChartLeaf), + ); + final chartVirtualRectAfterPan = + lineChartLeafAfterPan.chartVirtualRect!; + + expect( + chartVirtualRectAfterPan.left, + greaterThan(chartVirtualRectBeforePan.left), + ); + expect(chartVirtualRectAfterPan.top, 0); + }); + + testWidgets('vertically with FlScaleAxis.vertical', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: LineChart( + LineChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.vertical, + ), + ), + ), + ); + + final chartCenterOffset = tester.getCenter( + find.byType(LineChartLeaf), + ); + final scaleStart1 = chartCenterOffset; + final scaleStart2 = chartCenterOffset; + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await tester.pump(); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await tester.pump(); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final lineChartLeafBeforePan = tester.widget( + find.byType(LineChartLeaf), + ); + + final chartVirtualRectBeforePan = + lineChartLeafBeforePan.chartVirtualRect!; + expect(chartVirtualRectBeforePan.left, 0); + + final pointer = TestPointer(1, PointerDeviceKind.trackpad); + const leftAndUp = Offset(-100, -100); + await tester.sendEventToBinding(pointer.hover(chartCenterOffset)); + await tester.pump(); + await tester.sendEventToBinding(pointer.scroll(leftAndUp)); + await tester.pump(); + + final lineChartLeafAfterPan = tester.widget( + find.byType(LineChartLeaf), + ); + final chartVirtualRectAfterPan = + lineChartLeafAfterPan.chartVirtualRect!; + + expect(chartVirtualRectAfterPan.left, 0); + expect( + chartVirtualRectAfterPan.top, + greaterThan(chartVirtualRectBeforePan.top), + ); + }); + + testWidgets('freely with FlScaleAxis.free', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: LineChart( + LineChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.free, + ), + ), + ), + ); + + final chartCenterOffset = tester.getCenter( + find.byType(LineChartLeaf), + ); + final scaleStart1 = chartCenterOffset; + final scaleStart2 = chartCenterOffset; + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await tester.pump(); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await tester.pump(); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final lineChartLeafBeforePan = tester.widget( + find.byType(LineChartLeaf), + ); + + final chartVirtualRectBeforePan = + lineChartLeafBeforePan.chartVirtualRect!; + + final pointer = TestPointer(1, PointerDeviceKind.trackpad); + const leftAndUp = Offset(-100, -100); + await tester.sendEventToBinding(pointer.hover(chartCenterOffset)); + await tester.pump(); + await tester.sendEventToBinding(pointer.scroll(leftAndUp)); + await tester.pump(); + + final lineChartLeafAfterPan = tester.widget( + find.byType(LineChartLeaf), + ); + final chartVirtualRectAfterPan = + lineChartLeafAfterPan.chartVirtualRect!; + + expect( + chartVirtualRectAfterPan.left, + greaterThan(chartVirtualRectBeforePan.left), + ); + expect( + chartVirtualRectAfterPan.top, + greaterThan(chartVirtualRectBeforePan.top), + ); + }); + }); + + testWidgets( + 'does not scale with FlScaleAxis.none when ' + 'trackpadScrollCausesScale is true', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: LineChart( + LineChartData(), + transformationConfig: const FlTransformationConfig( + // This is for test + // ignore: avoid_redundant_argument_values + scaleAxis: FlScaleAxis.none, + trackpadScrollCausesScale: true, + ), + ), + ), + ); + + final pointer = TestPointer(1, PointerDeviceKind.trackpad); + final chartCenterOffset = tester.getCenter( + find.byType(LineChartLeaf), + ); + const scrollAmount = Offset(0, -100); + + await tester.sendEventToBinding(pointer.hover(chartCenterOffset)); + await tester.pump(); + await tester.sendEventToBinding(pointer.scroll(scrollAmount)); + await tester.pump(); + + final lineChartLeaf = tester.widget( + find.byType(LineChartLeaf), + ); + expect(lineChartLeaf.chartVirtualRect, null); + }, + ); + + for (final scaleAxis in FlScaleAxis.scalingEnabledAxis) { + testWidgets( + 'does not scale when trackpadScrollCausesScale is false ' + 'for $scaleAxis', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: LineChart( + LineChartData(), + transformationConfig: FlTransformationConfig( + scaleAxis: scaleAxis, + // This is for test + // ignore: avoid_redundant_argument_values + trackpadScrollCausesScale: false, + ), + ), + ), + ); + + final pointer = TestPointer(1, PointerDeviceKind.trackpad); + final chartCenterOffset = tester.getCenter( + find.byType(LineChartLeaf), + ); + const scrollAmount = Offset(0, -100); + + await tester.sendEventToBinding(pointer.hover(chartCenterOffset)); + await tester.pump(); + await tester.sendEventToBinding(pointer.scroll(scrollAmount)); + await tester.pump(); + + final lineChartLeaf = tester.widget( + find.byType(LineChartLeaf), + ); + expect(lineChartLeaf.chartVirtualRect, null); + }, + ); + } + + testWidgets('scales horizontally with FlScaleAxis.horizontal', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: LineChart( + LineChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.horizontal, + trackpadScrollCausesScale: true, + ), + ), + ), + ); + + final pointer = TestPointer(1, PointerDeviceKind.trackpad); + final chartCenterOffset = tester.getCenter(find.byType(LineChartLeaf)); + const scrollAmount = Offset(0, -100); + + await tester.sendEventToBinding(pointer.hover(chartCenterOffset)); + await tester.pump(); + await tester.sendEventToBinding(pointer.scroll(scrollAmount)); + await tester.pump(); + + final lineChartLeaf = tester.widget( + find.byType(LineChartLeaf), + ); + final renderBox = tester.renderObject( + find.byType(LineChartLeaf), + ); + final chartVirtualRect = lineChartLeaf.chartVirtualRect!; + + expect(chartVirtualRect.size.height, renderBox.size.height); + expect(chartVirtualRect.size.width, greaterThan(renderBox.size.width)); + expect(chartVirtualRect.left, isNegative); + expect(chartVirtualRect.top, 0); + }); + + testWidgets('scales vertically with FlScaleAxis.vertical', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: LineChart( + LineChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.vertical, + trackpadScrollCausesScale: true, + ), + ), + ), + ); + + final pointer = TestPointer(1, PointerDeviceKind.trackpad); + final chartCenterOffset = tester.getCenter(find.byType(LineChartLeaf)); + const scrollAmount = Offset(0, -100); + + await tester.sendEventToBinding(pointer.hover(chartCenterOffset)); + await tester.pump(); + await tester.sendEventToBinding(pointer.scroll(scrollAmount)); + await tester.pump(); + + final lineChartLeaf = tester.widget( + find.byType(LineChartLeaf), + ); + final renderBox = tester.renderObject( + find.byType(LineChartLeaf), + ); + final chartVirtualRect = lineChartLeaf.chartVirtualRect!; + + expect( + chartVirtualRect.size.height, + greaterThan(renderBox.size.height), + ); + expect(chartVirtualRect.size.width, renderBox.size.width); + expect(chartVirtualRect.left, 0); + expect(chartVirtualRect.top, isNegative); + }); + + testWidgets('scales freely with FlScaleAxis.free', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: LineChart( + LineChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.free, + trackpadScrollCausesScale: true, + ), + ), + ), + ); + + final pointer = TestPointer(1, PointerDeviceKind.trackpad); + final chartCenterOffset = tester.getCenter(find.byType(LineChartLeaf)); + const scrollAmount = Offset(0, -100); + + await tester.sendEventToBinding(pointer.hover(chartCenterOffset)); + await tester.pump(); + await tester.sendEventToBinding(pointer.scroll(scrollAmount)); + await tester.pump(); + + final lineChartLeaf = + tester.widget(find.byType(LineChartLeaf)); + final renderBox = tester.renderObject( + find.byType(LineChartLeaf), + ); + final chartVirtualRect = lineChartLeaf.chartVirtualRect!; + + expect(chartVirtualRect.size, greaterThan(renderBox.size)); + expect(chartVirtualRect.left, isNegative); + expect(chartVirtualRect.top, isNegative); + }); + }); + }); +} diff --git a/test/chart/pie_chart/pie_chart_data_test.dart b/test/chart/pie_chart/pie_chart_data_test.dart new file mode 100644 index 0000000..c39349b --- /dev/null +++ b/test/chart/pie_chart/pie_chart_data_test.dart @@ -0,0 +1,168 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../data_pool.dart'; + +void main() { + group('PieChart data equality check', () { + test('PieChartData equality test', () { + expect(pieChartData1 == pieChartData1Clone, true); + + expect( + pieChartData1 == + pieChartData1Clone.copyWith( + borderData: FlBorderData( + show: false, + border: Border.all(), + ), + ), + true, + ); + + expect( + pieChartData1 == + pieChartData1Clone.copyWith( + borderData: FlBorderData( + show: true, + border: Border.all(), + ), + ), + false, + ); + + expect( + pieChartData1 == + pieChartData1Clone.copyWith( + startDegreeOffset: 33, + ), + false, + ); + + expect( + pieChartData1 == + PieChartData( + borderData: FlBorderData( + show: false, + border: Border.all(), + ), + startDegreeOffset: 0, + centerSpaceColor: Colors.white, + centerSpaceRadius: 12, + pieTouchData: PieTouchData( + enabled: false, + ), + sectionsSpace: 44, + ), + false, + ); + + expect( + pieChartData1 == + pieChartData1Clone.copyWith( + sections: [], + ), + false, + ); + + expect( + pieChartData1 == + pieChartData1Clone.copyWith( + sections: [ + PieChartSectionData(value: 12, color: Colors.red), + PieChartSectionData(value: 22, color: Colors.green), + ], + ), + true, + ); + + expect( + pieChartData1 == + pieChartData1Clone.copyWith( + sections: [ + PieChartSectionData(value: 12, color: Colors.red), + PieChartSectionData( + value: 22, + color: Colors.green.withValues(alpha: 0.99), + ), + ], + ), + false, + ); + + expect( + pieChartData1 == + pieChartData1Clone.copyWith( + sections: [ + PieChartSectionData(value: 22, color: Colors.green), + PieChartSectionData(value: 12, color: Colors.red), + ], + ), + false, + ); + + expect( + pieChartData1 == + pieChartData1Clone.copyWith( + centerSpaceColor: Colors.cyan, + ), + false, + ); + + expect( + pieChartData1 == + pieChartData1Clone.copyWith( + centerSpaceRadius: 44, + ), + false, + ); + + expect( + pieChartData1 == + pieChartData1Clone.copyWith( + pieTouchData: PieTouchData(), + ), + false, + ); + + expect( + pieChartData1 == + pieChartData1Clone.copyWith( + sectionsSpace: 44.000001, + ), + false, + ); + + expect( + pieChartData1 == + pieChartData1Clone.copyWith( + titleSunbeamLayout: true, + ), + false, + ); + }); + + test('PieTouchData equality test', () { + final sample1 = PieTouchData( + touchCallback: (event, response) {}, + enabled: true, + ); + final sample2 = PieTouchData( + enabled: true, + ); + + expect(sample1 == sample2, false); + + final disabled = PieTouchData( + enabled: false, + ); + expect(sample1 == disabled, false); + + final zeroLongPressDuration = PieTouchData( + enabled: true, + longPressDuration: Duration.zero, + ); + expect(sample1 == zeroLongPressDuration, false); + }); + }); +} diff --git a/test/chart/pie_chart/pie_chart_helper_test.dart b/test/chart/pie_chart/pie_chart_helper_test.dart new file mode 100644 index 0000000..db53380 --- /dev/null +++ b/test/chart/pie_chart/pie_chart_helper_test.dart @@ -0,0 +1,33 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/pie_chart/pie_chart_helper.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('Test List.toWidgets()', () { + final widgets1 = [ + PieChartSectionData(value: 1), + PieChartSectionData(value: 2), + PieChartSectionData(value: 3), + ].toWidgets(); + expect(widgets1, List.empty()); + + final widgets2 = [ + PieChartSectionData(value: 1), + PieChartSectionData(value: 2, badgeWidget: const Text('asdf')), + PieChartSectionData(value: 3), + ].toWidgets(); + expect(widgets2[0] is Container, true); + expect(widgets2[1] is Text, true); + expect(widgets2[2] is Container, true); + + final widgets3 = [ + PieChartSectionData(value: 1, badgeWidget: const Text('1')), + PieChartSectionData(value: 2, badgeWidget: const Text('2')), + PieChartSectionData(value: 3, badgeWidget: const Text('3')), + ].toWidgets(); + expect((widgets3[0] as Text).data, '1'); + expect((widgets3[1] as Text).data, '2'); + expect((widgets3[2] as Text).data, '3'); + }); +} diff --git a/test/chart/pie_chart/pie_chart_painter_test.dart b/test/chart/pie_chart/pie_chart_painter_test.dart new file mode 100644 index 0000000..2bfa5c8 --- /dev/null +++ b/test/chart/pie_chart/pie_chart_painter_test.dart @@ -0,0 +1,1266 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart'; +import 'package:fl_chart/src/chart/base/line.dart'; +import 'package:fl_chart/src/chart/pie_chart/pie_chart_painter.dart'; +import 'package:fl_chart/src/utils/canvas_wrapper.dart'; +import 'package:fl_chart/src/utils/utils.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/physics.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import '../../helper_methods.dart'; +import '../data_pool.dart'; +import 'pie_chart_painter_test.mocks.dart'; + +@GenerateMocks([Canvas, CanvasWrapper, BuildContext, Utils]) +void main() { + const tolerance = 0.001; + group('paint()', () { + test('test 1', () { + final utilsMainInstance = Utils(); + const viewSize = Size(400, 400); + final data = PieChartData( + sections: [ + PieChartSectionData( + value: 10, + ), + PieChartSectionData( + value: 20, + ), + PieChartSectionData( + value: 30, + ), + ], + ); + + final pieChartPainter = PieChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.getThemeAwareTextStyle(any, any)) + .thenAnswer((realInvocation) => textStyle1); + when(mockUtils.radians(any)).thenAnswer((realInvocation) => 12); + + final mockBuildContext = MockBuildContext(); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + pieChartPainter.paint( + mockBuildContext, + mockCanvasWrapper, + holder, + ); + + verify(mockCanvasWrapper.drawPath(any, any)).called(3); + Utils.changeInstance(utilsMainInstance); + }); + }); + + group('calculateSectionsAngle()', () { + test('test 1', () { + final sections = [ + PieChartSectionData(value: 10), + PieChartSectionData(value: 20), + PieChartSectionData(value: 30), + PieChartSectionData(value: 40), + ]; + expect( + PieChartPainter().calculateSectionsAngle(sections, 100), + [36, 72, 108, 144], + ); + }); + + test('test 2', () { + final sections = [ + PieChartSectionData(value: 10), + PieChartSectionData(value: 10), + PieChartSectionData(value: 10), + PieChartSectionData(value: 10), + ]; + expect( + PieChartPainter().calculateSectionsAngle(sections, 40), + [90, 90, 90, 90], + ); + }); + }); + + group('drawCenterSpace()', () { + test('test 1', () { + const viewSize = Size(200, 200); + + final data = PieChartData( + centerSpaceColor: MockData.color1, + ); + + final barChartPainter = PieChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + barChartPainter.drawCenterSpace(mockCanvasWrapper, 10, holder); + + final result = verify( + mockCanvasWrapper.drawCircle(const Offset(100, 100), 10, captureAny), + ); + expect(result.callCount, 1); + expect( + (result.captured.first as Paint).color, + isSameColorAs(MockData.color1), + ); + }); + }); + + group('drawTexts()', () { + test('test 1', () { + final utilsMainInstance = Utils(); + const viewSize = Size(200, 200); + + final data = PieChartData( + sections: List.generate(2, (i) { + return PieChartSectionData( + value: 10, + title: '$i%', + ); + }), + titleSunbeamLayout: true, + ); + + final pieChartPainter = PieChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + + final mockBuildContext = MockBuildContext(); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.getThemeAwareTextStyle(any, any)) + .thenAnswer((realInvocation) => textStyle1); + when(mockUtils.radians(any)).thenAnswer((realInvocation) => 12); + + final centerRadius = pieChartPainter.calculateCenterRadius( + viewSize, + holder, + ); + + pieChartPainter.drawTexts( + mockBuildContext, + mockCanvasWrapper, + holder, + centerRadius, + ); + + final results = verifyInOrder([ + mockCanvasWrapper.drawText(any, any, captureAny), + mockCanvasWrapper.drawText(any, any, captureAny), + ]); + + expect(results[0].captured.single, -90); + expect(results[1].captured.single, 90); + + Utils.changeInstance(utilsMainInstance); + }); + }); + + group('drawSections()', () { + test('test 1', () { + const viewSize = Size(200, 200); + + const radius = 30.0; + const centerSpace = 10.0; + final sections = [ + PieChartSectionData( + color: MockData.color2, + radius: radius, + value: 10, + borderSide: const BorderSide( + color: MockData.color3, + width: 3, + ), + ), + ]; + final data = PieChartData( + sections: sections, + ); + + final barChartPainter = PieChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + barChartPainter.drawSections(mockCanvasWrapper, [360], 10, holder); + + final rect = Rect.fromCircle( + center: viewSize.center(Offset.zero), + radius: radius + centerSpace, + ); + final results = verifyInOrder([ + mockCanvasWrapper.saveLayer( + rect, + any, + ), + mockCanvasWrapper.drawCircle( + const Offset(100, 100), + 10 + 30, + captureAny, + ), + mockCanvasWrapper.drawCircle( + const Offset(100, 100), + 10, + captureAny, + ), + mockCanvasWrapper.restore(), + ]); + final result = results[1]; + expect(result.callCount, 1); + expect( + (result.captured.single as Paint).color, + isSameColorAs(MockData.color2), + ); + expect((result.captured.single as Paint).style, PaintingStyle.fill); + + final result2 = verify( + mockCanvasWrapper.drawCircle( + const Offset(100, 100), + 10 + (3 / 2), + captureAny, + ), + ); + expect(result2.callCount, 1); + expect( + (result2.captured.single as Paint).color, + isSameColorAs(MockData.color3), + ); + expect((result2.captured.single as Paint).strokeWidth, 3); + expect((result2.captured.single as Paint).style, PaintingStyle.stroke); + }); + + test('test 2', () { + const viewSize = Size(200, 200); + + final data = PieChartData( + centerSpaceColor: MockData.color1, + sectionsSpace: 10, + sections: [ + PieChartSectionData(color: MockData.color1, value: 1), + PieChartSectionData(color: MockData.color2, value: 2), + PieChartSectionData(color: MockData.color3, value: 3), + PieChartSectionData(color: MockData.color4, value: 4), + ], + ); + + final barChartPainter = PieChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + final results = >[]; + when(mockCanvasWrapper.drawPath(captureAny, captureAny)) + .thenAnswer((inv) { + final paint = inv.positionalArguments[1] as Paint; + results.add({ + 'path': inv.positionalArguments[0] as Path, + 'paint_color': paint.color, + 'paint_style': paint.style, + }); + }); + + barChartPainter.drawSections( + mockCanvasWrapper, + [36, 72, 108, 144], + 10, + holder, + ); + verifyNever(mockCanvasWrapper.drawCircle(any, any, any)); + + expect(results.length, 4); + + final path0 = barChartPainter.generateSectionPath( + data.sections[0], + 10, + 0, + 36, + const Offset(100, 100), + 10, + ); + expect( + HelperMethods.equalsPaths(results[0]['path'] as Path, path0), + true, + ); + expect( + results[0]['paint_color'] as Color, + isSameColorAs(MockData.color1), + ); + expect(results[0]['paint_style'] as PaintingStyle, PaintingStyle.fill); + + final path1 = barChartPainter.generateSectionPath( + data.sections[1], + 10, + 36, + 72, + const Offset(100, 100), + 10, + ); + expect( + HelperMethods.equalsPaths(results[1]['path'] as Path, path1), + true, + ); + expect( + results[1]['paint_color'] as Color, + isSameColorAs(MockData.color2), + ); + expect(results[1]['paint_style'] as PaintingStyle, PaintingStyle.fill); + + final path2 = barChartPainter.generateSectionPath( + data.sections[2], + 10, + 108, + 108, + const Offset(100, 100), + 10, + ); + expect( + HelperMethods.equalsPaths(results[2]['path'] as Path, path2), + true, + ); + expect( + results[2]['paint_color'] as Color, + isSameColorAs(MockData.color3), + ); + expect(results[2]['paint_style'] as PaintingStyle, PaintingStyle.fill); + + final path3 = barChartPainter.generateSectionPath( + data.sections[3], + 10, + 216, + 144, + const Offset(100, 100), + 10, + ); + expect( + HelperMethods.equalsPaths(results[3]['path'] as Path, path3), + true, + ); + expect( + results[3]['paint_color'] as Color, + isSameColorAs(MockData.color4), + ); + expect(results[3]['paint_style'] as PaintingStyle, PaintingStyle.fill); + }); + }); + + group('generateSectionPath()', () { + test('test 1', () { + final data = PieChartData( + centerSpaceColor: MockData.color1, + sectionsSpace: 10, + sections: [ + PieChartSectionData(color: MockData.color1, value: 1), + PieChartSectionData(color: MockData.color2, value: 2), + PieChartSectionData(color: MockData.color3, value: 3), + PieChartSectionData(color: MockData.color4, value: 4), + ], + ); + final barChartPainter = PieChartPainter(); + + final path0 = barChartPainter.generateSectionPath( + data.sections[0], + 10, + 0, + 36, + const Offset(100, 100), + 10, + ); + final path0Length = path0 + .computeMetrics() + .toList() + .map((e) => e.length) + .reduce((a, b) => a + b); + expect(path0Length, 90.08028411865234); + + final path1 = barChartPainter.generateSectionPath( + data.sections[1], + 10, + 36, + 72, + const Offset(100, 100), + 10, + ); + final path1Length = path1 + .computeMetrics() + .toList() + .map((e) => e.length) + .reduce((a, b) => a + b); + expect(path1Length, 136.93048095703125); + + final path2 = barChartPainter.generateSectionPath( + data.sections[2], + 10, + 108, + 108, + const Offset(100, 100), + 10, + ); + final path2Length = path2 + .computeMetrics() + .toList() + .map((e) => e.length) + .reduce((a, b) => a + b); + expect(path2Length, closeTo(174.6013, tolerance)); + + final path3 = barChartPainter.generateSectionPath( + data.sections[3], + 10, + 216, + 144, + const Offset(100, 100), + 10, + ); + final path3Length = path3 + .computeMetrics() + .toList() + .map((e) => e.length) + .reduce((a, b) => a + b); + expect(path3Length, 212.1544189453125); + }); + + test('test 2', () { + final data = PieChartData( + centerSpaceColor: MockData.color1, + sectionsSpace: 0, + sections: [ + PieChartSectionData(color: MockData.color1, value: 1), + PieChartSectionData(color: MockData.color2, value: 2), + PieChartSectionData(color: MockData.color3, value: 3), + PieChartSectionData(color: MockData.color4, value: 4), + ], + ); + final barChartPainter = PieChartPainter(); + + final path0 = barChartPainter.generateSectionPath( + data.sections[0], + 0, + 0, + 36, + const Offset(100, 100), + 10, + ); + final path0Length = path0 + .computeMetrics() + .toList() + .map((e) => e.length) + .reduce((a, b) => a + b); + expect(path0Length, 117.56398010253906); + + final path1 = barChartPainter.generateSectionPath( + data.sections[1], + 0, + 36, + 72, + const Offset(100, 100), + 10, + ); + final path1Length = path1 + .computeMetrics() + .toList() + .map((e) => e.length) + .reduce((a, b) => a + b); + expect(path1Length, 155.1278076171875); + + final path2 = barChartPainter.generateSectionPath( + data.sections[2], + 0, + 108, + 108, + const Offset(100, 100), + 10, + ); + final path2Length = path2 + .computeMetrics() + .toList() + .map((e) => e.length) + .reduce((a, b) => a + b); + expect(path2Length, closeTo(192.8401, tolerance)); + + final path3 = barChartPainter.generateSectionPath( + data.sections[3], + 0, + 216, + 144, + const Offset(100, 100), + 10, + ); + final path3Length = path3 + .computeMetrics() + .toList() + .map((e) => e.length) + .reduce((a, b) => a + b); + expect(nearEqual(path3Length, 230.37237548828125, 0.0001), true); + }); + + test('test 3', () { + final data = PieChartData( + centerSpaceColor: MockData.color1, + sectionsSpace: 0, + sections: [ + PieChartSectionData(color: MockData.color1, value: 1), + PieChartSectionData(color: MockData.color2, value: 2), + PieChartSectionData(color: MockData.color3, value: 3), + PieChartSectionData(color: MockData.color4, value: 4), + ], + ); + final barChartPainter = PieChartPainter(); + + final path0 = barChartPainter.generateSectionPath( + data.sections[0], + 0, + 0, + 36, + const Offset(100, 100), + 3, + ); + final path0Length = path0 + .computeMetrics() + .toList() + .map((e) => e.length) + .reduce((a, b) => a + b); + expect(path0Length, 108.80243682861328); + + final path1 = barChartPainter.generateSectionPath( + data.sections[1], + 0, + 36, + 72, + const Offset(100, 100), + 4, + ); + final path1Length = path1 + .computeMetrics() + .toList() + .map((e) => e.length) + .reduce((a, b) => a + b); + expect(path1Length, 140.05465698242188); + + final path2 = barChartPainter.generateSectionPath( + data.sections[2], + 0, + 108, + 108, + const Offset(100, 100), + 5, + ); + final path2Length = path2 + .computeMetrics() + .toList() + .map((e) => e.length) + .reduce((a, b) => a + b); + expect(path2Length, 173.86875915527344); + + final path3 = barChartPainter.generateSectionPath( + data.sections[3], + 0, + 216, + 144, + const Offset(100, 100), + 6, + ); + final path3Length = path3 + .computeMetrics() + .toList() + .map((e) => e.length) + .reduce((a, b) => a + b); + expect(path3Length, 210.1807098388672); + }); + }); + + group('createRectPathAroundLine()', () { + test('test 1', () { + final barChartPainter = PieChartPainter(); + final path0 = barChartPainter.createRectPathAroundLine( + const Line(Offset.zero, Offset(10, 0)), + 4, + ); + final path0Length = path0 + .computeMetrics() + .toList() + .map((e) => e.length) + .reduce((a, b) => a + b); + expect(path0Length, 32.0); + + final path1 = barChartPainter.createRectPathAroundLine( + const Line(Offset(32, 11), Offset(12, 5)), + 66, + ); + final path1Length = path1 + .computeMetrics() + .toList() + .map((e) => e.length) + .reduce((a, b) => a + b); + expect(path1Length, 239.76123046875); + }); + }); + + group('drawSection()', () { + test('test 1', () { + const viewSize = Size(200, 200); + final data = PieChartData( + centerSpaceColor: MockData.color1, + sectionsSpace: 10, + sections: [ + PieChartSectionData(color: MockData.color1, value: 1), + PieChartSectionData(color: MockData.color2, value: 2), + PieChartSectionData(color: MockData.color3, value: 3), + PieChartSectionData(color: MockData.color4, value: 4), + ], + ); + final barChartPainter = PieChartPainter(); + + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final results = >[]; + when(mockCanvasWrapper.drawPath(captureAny, captureAny)) + .thenAnswer((inv) { + final paint = inv.positionalArguments[1] as Paint; + results.add({ + 'path': inv.positionalArguments[0] as Path, + 'paint_color': paint.color, + 'paint_style': paint.style, + }); + }); + + barChartPainter + ..drawSection( + data.sections[0], + MockData.path1, + mockCanvasWrapper, + ) + ..drawSection( + data.sections[1], + MockData.path2, + mockCanvasWrapper, + ) + ..drawSection( + data.sections[2], + MockData.path3, + mockCanvasWrapper, + ) + ..drawSection( + data.sections[3], + MockData.path4, + mockCanvasWrapper, + ); + + expect(results.length, 4); + + expect(results[0]['path'] as Path, MockData.path1); + expect( + results[0]['paint_color'] as Color, + isSameColorAs(MockData.color1), + ); + expect(results[0]['paint_style'] as PaintingStyle, PaintingStyle.fill); + + expect(results[1]['path'] as Path, MockData.path2); + expect( + results[1]['paint_color'] as Color, + isSameColorAs(MockData.color2), + ); + expect(results[1]['paint_style'] as PaintingStyle, PaintingStyle.fill); + + expect(results[2]['path'] as Path, MockData.path3); + expect( + results[2]['paint_color'] as Color, + isSameColorAs(MockData.color3), + ); + expect(results[2]['paint_style'] as PaintingStyle, PaintingStyle.fill); + + expect(results[3]['path'] as Path, MockData.path4); + expect( + results[3]['paint_color'] as Color, + isSameColorAs(MockData.color4), + ); + expect(results[3]['paint_style'] as PaintingStyle, PaintingStyle.fill); + }); + }); + + group('drawSectionStroke()', () { + test('test 1', () { + const viewSize = Size(200, 200); + final data = PieChartData( + centerSpaceColor: MockData.color1, + sectionsSpace: 10, + sections: [ + PieChartSectionData(color: MockData.color1, value: 1), + PieChartSectionData(color: MockData.color2, value: 2), + PieChartSectionData(color: MockData.color3, value: 3), + PieChartSectionData(color: MockData.color4, value: 4), + ], + ); + final barChartPainter = PieChartPainter(); + + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final results = >[]; + when(mockCanvasWrapper.drawPath(captureAny, captureAny)) + .thenAnswer((inv) { + final paint = inv.positionalArguments[1] as Paint; + results.add({ + 'path': inv.positionalArguments[0] as Path, + 'paint_color': paint.color, + 'paint_style': paint.style, + }); + }); + + barChartPainter + ..drawSectionStroke( + data.sections[0], + MockData.path1, + mockCanvasWrapper, + viewSize, + ) + ..drawSectionStroke( + data.sections[1], + MockData.path2, + mockCanvasWrapper, + viewSize, + ) + ..drawSectionStroke( + data.sections[2], + MockData.path3, + mockCanvasWrapper, + viewSize, + ) + ..drawSectionStroke( + data.sections[3], + MockData.path4, + mockCanvasWrapper, + viewSize, + ); + + verifyNever(mockCanvasWrapper.saveLayer(any, any)); + verifyNever(mockCanvasWrapper.clipPath(any)); + verifyNever(mockCanvasWrapper.drawPath(any, any)); + verifyNever(mockCanvasWrapper.restore()); + }); + + test('test 2', () { + const viewSize = Size(200, 200); + final data = PieChartData( + centerSpaceColor: MockData.color1, + sectionsSpace: 10, + sections: [ + PieChartSectionData( + color: MockData.color1, + value: 1, + borderSide: MockData.borderSide1, + ), + PieChartSectionData( + color: MockData.color2, + value: 2, + borderSide: MockData.borderSide2, + ), + PieChartSectionData( + color: MockData.color3, + value: 3, + borderSide: MockData.borderSide3, + ), + PieChartSectionData( + color: MockData.color4, + value: 4, + borderSide: MockData.borderSide4, + ), + ], + ); + final barChartPainter = PieChartPainter(); + + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final clipPathResults = >[]; + when(mockCanvasWrapper.clipPath(captureAny)).thenAnswer((inv) { + clipPathResults.add({ + 'path': inv.positionalArguments[0] as Path, + }); + }); + + final drawPathResults = >[]; + when(mockCanvasWrapper.drawPath(captureAny, captureAny)) + .thenAnswer((inv) { + final paint = inv.positionalArguments[1] as Paint; + drawPathResults.add({ + 'path': inv.positionalArguments[0] as Path, + 'paint_color': paint.color, + 'paint_style': paint.style, + 'paint_stroke_width': paint.strokeWidth, + }); + }); + + barChartPainter + ..drawSectionStroke( + data.sections[0], + MockData.path1, + mockCanvasWrapper, + viewSize, + ) + ..drawSectionStroke( + data.sections[1], + MockData.path2, + mockCanvasWrapper, + viewSize, + ) + ..drawSectionStroke( + data.sections[2], + MockData.path3, + mockCanvasWrapper, + viewSize, + ) + ..drawSectionStroke( + data.sections[3], + MockData.path4, + mockCanvasWrapper, + viewSize, + ); + + verify( + mockCanvasWrapper.saveLayer( + Rect.fromLTWH(0, 0, viewSize.width, viewSize.height), + any, + ), + ).called(4); + expect(clipPathResults.length, 4); + expect(clipPathResults[0]['path'], MockData.path1); + expect(clipPathResults[1]['path'], MockData.path2); + expect(clipPathResults[2]['path'], MockData.path3); + expect(clipPathResults[3]['path'], MockData.path4); + + expect(drawPathResults.length, 4); + + expect(drawPathResults[0]['path'], MockData.path1); + expect( + drawPathResults[0]['paint_color'], + isSameColorAs(MockData.color1), + ); + expect(drawPathResults[0]['paint_style'], PaintingStyle.stroke); + expect( + drawPathResults[0]['paint_stroke_width'], + MockData.borderSide1.width * 2, + ); + + expect(drawPathResults[1]['path'], MockData.path2); + expect( + drawPathResults[1]['paint_color'], + isSameColorAs(MockData.color2), + ); + expect(drawPathResults[1]['paint_style'], PaintingStyle.stroke); + expect( + drawPathResults[1]['paint_stroke_width'], + MockData.borderSide2.width * 2, + ); + + expect(drawPathResults[2]['path'], MockData.path3); + expect( + drawPathResults[2]['paint_color'], + isSameColorAs(MockData.color3), + ); + expect(drawPathResults[2]['paint_style'], PaintingStyle.stroke); + expect( + drawPathResults[2]['paint_stroke_width'], + MockData.borderSide3.width * 2, + ); + + expect(drawPathResults[3]['path'], MockData.path4); + expect( + drawPathResults[3]['paint_color'], + isSameColorAs(MockData.color4), + ); + expect(drawPathResults[3]['paint_style'], PaintingStyle.stroke); + expect( + drawPathResults[3]['paint_stroke_width'], + MockData.borderSide4.width * 2, + ); + + verify(mockCanvasWrapper.restore()).called(4); + }); + }); + + group('calculateCenterRadius()', () { + test('test 1', () { + const viewSize = Size(400, 200); + final sections = [ + PieChartSectionData( + color: MockData.color1, + value: 1, + borderSide: MockData.borderSide1, + showTitle: true, + titleStyle: MockData.textStyle1, + radius: 11, + ), + PieChartSectionData( + color: MockData.color2, + value: 2, + borderSide: MockData.borderSide2, + showTitle: true, + titleStyle: MockData.textStyle2, + radius: 22, + title: '22-22', + ), + PieChartSectionData( + color: MockData.color3, + value: 3, + borderSide: MockData.borderSide3, + showTitle: false, + titleStyle: MockData.textStyle3, + radius: 33, + ), + PieChartSectionData( + color: MockData.color4, + value: 4, + borderSide: MockData.borderSide4, + showTitle: true, + titleStyle: MockData.textStyle4, + radius: 44, + ), + ]; + final barChartPainter = PieChartPainter(); + + final data1 = PieChartData(sections: sections, centerSpaceRadius: 15); + final result1 = barChartPainter.calculateCenterRadius( + viewSize, + PaintHolder(data1, data1, TextScaler.noScaling), + ); + expect(result1, 15); + + final data2 = PieChartData(sections: sections); + final result2 = barChartPainter.calculateCenterRadius( + viewSize, + PaintHolder(data2, data2, TextScaler.noScaling), + ); + expect(result2, 56); + }); + }); + + group('handleTouch()', () { + test('test 2', () { + const viewSize = Size(200, 200); + final data = PieChartData( + centerSpaceColor: MockData.color1, + sectionsSpace: 10, + sections: [ + PieChartSectionData( + color: MockData.color1, + value: 1, + borderSide: MockData.borderSide1, + radius: 10, + ), + PieChartSectionData( + color: MockData.color2, + value: 2, + borderSide: MockData.borderSide2, + radius: 20, + ), + PieChartSectionData( + color: MockData.color3, + value: 3, + borderSide: MockData.borderSide3, + radius: 30, + ), + PieChartSectionData( + color: MockData.color4, + value: 4, + borderSide: MockData.borderSide4, + radius: 40, + ), + ], + ); + final barChartPainter = PieChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + + expect( + barChartPainter + .handleTouch(const Offset(191, 110), viewSize, holder) + .touchedSectionIndex, + -1, + ); + expect( + barChartPainter + .handleTouch(const Offset(156, 110), viewSize, holder) + .touchedSectionIndex, + -1, + ); + expect( + barChartPainter + .handleTouch(const Offset(107, 190), viewSize, holder) + .touchedSectionIndex, + -1, + ); + expect( + barChartPainter + .handleTouch(const Offset(90, 156), viewSize, holder) + .touchedSectionIndex, + -1, + ); + expect( + barChartPainter + .handleTouch(const Offset(53, 131), viewSize, holder) + .touchedSectionIndex, + -1, + ); + expect( + barChartPainter + .handleTouch(const Offset(53, 131), viewSize, holder) + .touchedSectionIndex, + -1, + ); + expect( + barChartPainter + .handleTouch(const Offset(43, 94), viewSize, holder) + .touchedSectionIndex, + -1, + ); + expect( + barChartPainter + .handleTouch(const Offset(36, 57), viewSize, holder) + .touchedSectionIndex, + -1, + ); + expect( + barChartPainter + .handleTouch(const Offset(36, 57), viewSize, holder) + .touchedSectionIndex, + -1, + ); + expect( + barChartPainter + .handleTouch(const Offset(65, 4.3), viewSize, holder) + .touchedSectionIndex, + -1, + ); + expect( + barChartPainter + .handleTouch(const Offset(7, 108), viewSize, holder) + .touchedSectionIndex, + -1, + ); + + expect( + barChartPainter + .handleTouch(const Offset(159.76, 135.56), viewSize, holder) + .touchedSectionIndex, + 0, + ); + expect( + barChartPainter + .handleTouch(const Offset(169.35, 108.4), viewSize, holder) + .touchedSectionIndex, + 0, + ); + expect( + barChartPainter + .handleTouch(const Offset(162.32, 109.37), viewSize, holder) + .touchedSectionIndex, + 0, + ); + + expect( + barChartPainter + .handleTouch(const Offset(146.67, 144.94), viewSize, holder) + .touchedSectionIndex, + 1, + ); + expect( + barChartPainter + .handleTouch(const Offset(121.06, 160.38), viewSize, holder) + .touchedSectionIndex, + 1, + ); + expect( + barChartPainter + .handleTouch(const Offset(89.66, 163.60), viewSize, holder) + .touchedSectionIndex, + 1, + ); + expect( + barChartPainter + .handleTouch(const Offset(85.04, 177.85), viewSize, holder) + .touchedSectionIndex, + 1, + ); + + expect( + barChartPainter + .handleTouch(const Offset(75.2, 158.4), viewSize, holder) + .touchedSectionIndex, + 2, + ); + expect( + barChartPainter + .handleTouch(const Offset(66.2, 177), viewSize, holder) + .touchedSectionIndex, + 2, + ); + expect( + barChartPainter + .handleTouch(const Offset(40.3, 124.8), viewSize, holder) + .touchedSectionIndex, + 2, + ); + expect( + barChartPainter + .handleTouch(const Offset(19.1, 131), viewSize, holder) + .touchedSectionIndex, + 2, + ); + expect( + barChartPainter + .handleTouch(const Offset(19.1, 131), viewSize, holder) + .touchedSectionIndex, + 2, + ); + expect( + barChartPainter + .handleTouch(const Offset(17.7, 83.7), viewSize, holder) + .touchedSectionIndex, + 2, + ); + expect( + barChartPainter + .handleTouch(const Offset(27.8, 59.4), viewSize, holder) + .touchedSectionIndex, + 2, + ); + expect( + barChartPainter + .handleTouch(const Offset(44.1, 75.2), viewSize, holder) + .touchedSectionIndex, + 2, + ); + + expect( + barChartPainter + .handleTouch(const Offset(56.1, 55.6), viewSize, holder) + .touchedSectionIndex, + 3, + ); + expect( + barChartPainter + .handleTouch(const Offset(42.1, 46.3), viewSize, holder) + .touchedSectionIndex, + 3, + ); + expect( + barChartPainter + .handleTouch(const Offset(30.9, 38.4), viewSize, holder) + .touchedSectionIndex, + 3, + ); + expect( + barChartPainter + .handleTouch(const Offset(55.3, 17.8), viewSize, holder) + .touchedSectionIndex, + 3, + ); + expect( + barChartPainter + .handleTouch(const Offset(81.2, 39.8), viewSize, holder) + .touchedSectionIndex, + 3, + ); + expect( + barChartPainter + .handleTouch(const Offset(100.5, 4.1), viewSize, holder) + .touchedSectionIndex, + 3, + ); + expect( + barChartPainter + .handleTouch(const Offset(126.7, 40.6), viewSize, holder) + .touchedSectionIndex, + 3, + ); + expect( + barChartPainter + .handleTouch(const Offset(181.8, 51.3), viewSize, holder) + .touchedSectionIndex, + 3, + ); + expect( + barChartPainter + .handleTouch(const Offset(174.5, 40.2), viewSize, holder) + .touchedSectionIndex, + 3, + ); + expect( + barChartPainter + .handleTouch(const Offset(164.5, 91.4), viewSize, holder) + .touchedSectionIndex, + 3, + ); + }); + }); + + group('getBadgeOffsets()', () { + test('test 1', () { + const viewSize = Size(200, 200); + final data = PieChartData( + centerSpaceColor: MockData.color1, + sectionsSpace: 10, + sections: [ + PieChartSectionData( + color: MockData.color1, + value: 1, + borderSide: MockData.borderSide1, + ), + PieChartSectionData( + color: MockData.color2, + value: 2, + borderSide: MockData.borderSide2, + ), + PieChartSectionData( + color: MockData.color3, + value: 3, + borderSide: MockData.borderSide3, + ), + PieChartSectionData( + color: MockData.color4, + value: 4, + borderSide: MockData.borderSide4, + ), + ], + ); + final barChartPainter = PieChartPainter(); + final holder = PaintHolder( + data, + data, + TextScaler.noScaling, + ); + + final result = barChartPainter.getBadgeOffsets(viewSize, holder); + expect( + result, + { + 0: const Offset(176.0845213036123, 124.7213595499958), + 1: const Offset(124.7213595499958, 176.0845213036123), + 2: const Offset(23.915478696387723, 124.7213595499958), + 3: const Offset(124.72135954999578, 23.91547869638771), + }, + ); + }); + }); +} diff --git a/test/chart/pie_chart/pie_chart_painter_test.mocks.dart b/test/chart/pie_chart/pie_chart_painter_test.mocks.dart new file mode 100644 index 0000000..c57c2f1 --- /dev/null +++ b/test/chart/pie_chart/pie_chart_painter_test.mocks.dart @@ -0,0 +1,924 @@ +// Mocks generated by Mockito 5.4.5 from annotations +// in fl_chart/test/chart/pie_chart/pie_chart_painter_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:typed_data' as _i5; +import 'dart:ui' as _i2; + +import 'package:fl_chart/fl_chart.dart' as _i7; +import 'package:fl_chart/src/utils/canvas_wrapper.dart' as _i6; +import 'package:fl_chart/src/utils/utils.dart' as _i8; +import 'package:flutter/cupertino.dart' as _i3; +import 'package:flutter/foundation.dart' as _i4; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i9; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeRect_0 extends _i1.SmartFake implements _i2.Rect { + _FakeRect_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeCanvas_1 extends _i1.SmartFake implements _i2.Canvas { + _FakeCanvas_1(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeSize_2 extends _i1.SmartFake implements _i2.Size { + _FakeSize_2(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeWidget_3 extends _i1.SmartFake implements _i3.Widget { + _FakeWidget_3(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeInheritedWidget_4 extends _i1.SmartFake + implements _i3.InheritedWidget { + _FakeInheritedWidget_4(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeDiagnosticsNode_5 extends _i1.SmartFake + implements _i3.DiagnosticsNode { + _FakeDiagnosticsNode_5(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({ + _i4.TextTreeConfiguration? parentConfiguration, + _i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info, + }) => super.toString(); +} + +class _FakeOffset_6 extends _i1.SmartFake implements _i2.Offset { + _FakeOffset_6(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeBorderSide_7 extends _i1.SmartFake implements _i3.BorderSide { + _FakeBorderSide_7(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeTextStyle_8 extends _i1.SmartFake implements _i3.TextStyle { + _FakeTextStyle_8(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +/// A class which mocks [Canvas]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCanvas extends _i1.Mock implements _i2.Canvas { + MockCanvas() { + _i1.throwOnMissingStub(this); + } + + @override + void save() => super.noSuchMethod( + Invocation.method(#save, []), + returnValueForMissingStub: null, + ); + + @override + void saveLayer(_i2.Rect? bounds, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#saveLayer, [bounds, paint]), + returnValueForMissingStub: null, + ); + + @override + void restore() => super.noSuchMethod( + Invocation.method(#restore, []), + returnValueForMissingStub: null, + ); + + @override + void restoreToCount(int? count) => super.noSuchMethod( + Invocation.method(#restoreToCount, [count]), + returnValueForMissingStub: null, + ); + + @override + int getSaveCount() => + (super.noSuchMethod(Invocation.method(#getSaveCount, []), returnValue: 0) + as int); + + @override + void translate(double? dx, double? dy) => super.noSuchMethod( + Invocation.method(#translate, [dx, dy]), + returnValueForMissingStub: null, + ); + + @override + void scale(double? sx, [double? sy]) => super.noSuchMethod( + Invocation.method(#scale, [sx, sy]), + returnValueForMissingStub: null, + ); + + @override + void rotate(double? radians) => super.noSuchMethod( + Invocation.method(#rotate, [radians]), + returnValueForMissingStub: null, + ); + + @override + void skew(double? sx, double? sy) => super.noSuchMethod( + Invocation.method(#skew, [sx, sy]), + returnValueForMissingStub: null, + ); + + @override + void transform(_i5.Float64List? matrix4) => super.noSuchMethod( + Invocation.method(#transform, [matrix4]), + returnValueForMissingStub: null, + ); + + @override + _i5.Float64List getTransform() => + (super.noSuchMethod( + Invocation.method(#getTransform, []), + returnValue: _i5.Float64List(0), + ) + as _i5.Float64List); + + @override + void clipRect( + _i2.Rect? rect, { + _i2.ClipOp? clipOp = _i2.ClipOp.intersect, + bool? doAntiAlias = true, + }) => super.noSuchMethod( + Invocation.method( + #clipRect, + [rect], + {#clipOp: clipOp, #doAntiAlias: doAntiAlias}, + ), + returnValueForMissingStub: null, + ); + + @override + void clipRRect(_i2.RRect? rrect, {bool? doAntiAlias = true}) => + super.noSuchMethod( + Invocation.method(#clipRRect, [rrect], {#doAntiAlias: doAntiAlias}), + returnValueForMissingStub: null, + ); + + @override + void clipPath(_i2.Path? path, {bool? doAntiAlias = true}) => + super.noSuchMethod( + Invocation.method(#clipPath, [path], {#doAntiAlias: doAntiAlias}), + returnValueForMissingStub: null, + ); + + @override + _i2.Rect getLocalClipBounds() => + (super.noSuchMethod( + Invocation.method(#getLocalClipBounds, []), + returnValue: _FakeRect_0( + this, + Invocation.method(#getLocalClipBounds, []), + ), + ) + as _i2.Rect); + + @override + _i2.Rect getDestinationClipBounds() => + (super.noSuchMethod( + Invocation.method(#getDestinationClipBounds, []), + returnValue: _FakeRect_0( + this, + Invocation.method(#getDestinationClipBounds, []), + ), + ) + as _i2.Rect); + + @override + void drawColor(_i2.Color? color, _i2.BlendMode? blendMode) => + super.noSuchMethod( + Invocation.method(#drawColor, [color, blendMode]), + returnValueForMissingStub: null, + ); + + @override + void drawLine(_i2.Offset? p1, _i2.Offset? p2, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawLine, [p1, p2, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawPaint(_i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawPaint, [paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRect(_i2.Rect? rect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRect, [rect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRRect(_i2.RRect? rrect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRRect, [rrect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawDRRect(_i2.RRect? outer, _i2.RRect? inner, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawDRRect, [outer, inner, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawOval(_i2.Rect? rect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawOval, [rect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawCircle(_i2.Offset? c, double? radius, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawCircle, [c, radius, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawArc( + _i2.Rect? rect, + double? startAngle, + double? sweepAngle, + bool? useCenter, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawArc, [ + rect, + startAngle, + sweepAngle, + useCenter, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawPath(_i2.Path? path, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawPath, [path, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImage(_i2.Image? image, _i2.Offset? offset, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawImage, [image, offset, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImageRect( + _i2.Image? image, + _i2.Rect? src, + _i2.Rect? dst, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawImageRect, [image, src, dst, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImageNine( + _i2.Image? image, + _i2.Rect? center, + _i2.Rect? dst, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawImageNine, [image, center, dst, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawPicture(_i2.Picture? picture) => super.noSuchMethod( + Invocation.method(#drawPicture, [picture]), + returnValueForMissingStub: null, + ); + + @override + void drawParagraph(_i2.Paragraph? paragraph, _i2.Offset? offset) => + super.noSuchMethod( + Invocation.method(#drawParagraph, [paragraph, offset]), + returnValueForMissingStub: null, + ); + + @override + void drawPoints( + _i2.PointMode? pointMode, + List<_i2.Offset>? points, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawPoints, [pointMode, points, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRawPoints( + _i2.PointMode? pointMode, + _i5.Float32List? points, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawRawPoints, [pointMode, points, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawVertices( + _i2.Vertices? vertices, + _i2.BlendMode? blendMode, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawVertices, [vertices, blendMode, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawAtlas( + _i2.Image? atlas, + List<_i2.RSTransform>? transforms, + List<_i2.Rect>? rects, + List<_i2.Color>? colors, + _i2.BlendMode? blendMode, + _i2.Rect? cullRect, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawAtlas, [ + atlas, + transforms, + rects, + colors, + blendMode, + cullRect, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawRawAtlas( + _i2.Image? atlas, + _i5.Float32List? rstTransforms, + _i5.Float32List? rects, + _i5.Int32List? colors, + _i2.BlendMode? blendMode, + _i2.Rect? cullRect, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawRawAtlas, [ + atlas, + rstTransforms, + rects, + colors, + blendMode, + cullRect, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawShadow( + _i2.Path? path, + _i2.Color? color, + double? elevation, + bool? transparentOccluder, + ) => super.noSuchMethod( + Invocation.method(#drawShadow, [ + path, + color, + elevation, + transparentOccluder, + ]), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [CanvasWrapper]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCanvasWrapper extends _i1.Mock implements _i6.CanvasWrapper { + MockCanvasWrapper() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.Canvas get canvas => + (super.noSuchMethod( + Invocation.getter(#canvas), + returnValue: _FakeCanvas_1(this, Invocation.getter(#canvas)), + ) + as _i2.Canvas); + + @override + _i2.Size get size => + (super.noSuchMethod( + Invocation.getter(#size), + returnValue: _FakeSize_2(this, Invocation.getter(#size)), + ) + as _i2.Size); + + @override + void drawRRect(_i2.RRect? rrect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRRect, [rrect, paint]), + returnValueForMissingStub: null, + ); + + @override + void save() => super.noSuchMethod( + Invocation.method(#save, []), + returnValueForMissingStub: null, + ); + + @override + void restore() => super.noSuchMethod( + Invocation.method(#restore, []), + returnValueForMissingStub: null, + ); + + @override + void clipRect( + _i2.Rect? rect, { + _i2.ClipOp? clipOp = _i2.ClipOp.intersect, + bool? doAntiAlias = true, + }) => super.noSuchMethod( + Invocation.method( + #clipRect, + [rect], + {#clipOp: clipOp, #doAntiAlias: doAntiAlias}, + ), + returnValueForMissingStub: null, + ); + + @override + void translate(double? dx, double? dy) => super.noSuchMethod( + Invocation.method(#translate, [dx, dy]), + returnValueForMissingStub: null, + ); + + @override + void rotate(double? radius) => super.noSuchMethod( + Invocation.method(#rotate, [radius]), + returnValueForMissingStub: null, + ); + + @override + void drawPath(_i2.Path? path, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawPath, [path, paint]), + returnValueForMissingStub: null, + ); + + @override + void saveLayer(_i2.Rect? bounds, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#saveLayer, [bounds, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawPicture(_i2.Picture? picture) => super.noSuchMethod( + Invocation.method(#drawPicture, [picture]), + returnValueForMissingStub: null, + ); + + @override + void drawImage(_i2.Image? image, _i2.Offset? offset, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawImage, [image, offset, paint]), + returnValueForMissingStub: null, + ); + + @override + void clipPath(_i2.Path? path, {bool? doAntiAlias = true}) => + super.noSuchMethod( + Invocation.method(#clipPath, [path], {#doAntiAlias: doAntiAlias}), + returnValueForMissingStub: null, + ); + + @override + void drawRect(_i2.Rect? rect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRect, [rect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawLine(_i2.Offset? p1, _i2.Offset? p2, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawLine, [p1, p2, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawCircle(_i2.Offset? center, double? radius, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawCircle, [center, radius, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawArc( + _i2.Rect? rect, + double? startAngle, + double? sweepAngle, + bool? useCenter, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawArc, [ + rect, + startAngle, + sweepAngle, + useCenter, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawText( + _i3.TextPainter? tp, + _i2.Offset? offset, [ + double? rotateAngle, + ]) => super.noSuchMethod( + Invocation.method(#drawText, [tp, offset, rotateAngle]), + returnValueForMissingStub: null, + ); + + @override + void drawVerticalText(_i3.TextPainter? tp, _i2.Offset? offset) => + super.noSuchMethod( + Invocation.method(#drawVerticalText, [tp, offset]), + returnValueForMissingStub: null, + ); + + @override + void drawDot( + _i7.FlDotPainter? painter, + _i7.FlSpot? spot, + _i2.Offset? offset, + ) => super.noSuchMethod( + Invocation.method(#drawDot, [painter, spot, offset]), + returnValueForMissingStub: null, + ); + + @override + void drawErrorIndicator( + _i7.FlSpotErrorRangePainter? painter, + _i7.FlSpot? origin, + _i2.Offset? offset, + _i2.Rect? errorRelativeRect, + _i7.AxisChartData? axisData, + ) => super.noSuchMethod( + Invocation.method(#drawErrorIndicator, [ + painter, + origin, + offset, + errorRelativeRect, + axisData, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawRotated({ + required _i2.Size? size, + _i2.Offset? rotationOffset = _i2.Offset.zero, + _i2.Offset? drawOffset = _i2.Offset.zero, + required double? angle, + required _i6.DrawCallback? drawCallback, + }) => super.noSuchMethod( + Invocation.method(#drawRotated, [], { + #size: size, + #rotationOffset: rotationOffset, + #drawOffset: drawOffset, + #angle: angle, + #drawCallback: drawCallback, + }), + returnValueForMissingStub: null, + ); + + @override + void drawDashedLine( + _i2.Offset? from, + _i2.Offset? to, + _i2.Paint? painter, + List? dashArray, + ) => super.noSuchMethod( + Invocation.method(#drawDashedLine, [from, to, painter, dashArray]), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [BuildContext]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockBuildContext extends _i1.Mock implements _i3.BuildContext { + MockBuildContext() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Widget get widget => + (super.noSuchMethod( + Invocation.getter(#widget), + returnValue: _FakeWidget_3(this, Invocation.getter(#widget)), + ) + as _i3.Widget); + + @override + bool get mounted => + (super.noSuchMethod(Invocation.getter(#mounted), returnValue: false) + as bool); + + @override + bool get debugDoingBuild => + (super.noSuchMethod( + Invocation.getter(#debugDoingBuild), + returnValue: false, + ) + as bool); + + @override + _i3.InheritedWidget dependOnInheritedElement( + _i3.InheritedElement? ancestor, { + Object? aspect, + }) => + (super.noSuchMethod( + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + returnValue: _FakeInheritedWidget_4( + this, + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + ), + ) + as _i3.InheritedWidget); + + @override + void visitAncestorElements(_i3.ConditionalElementVisitor? visitor) => + super.noSuchMethod( + Invocation.method(#visitAncestorElements, [visitor]), + returnValueForMissingStub: null, + ); + + @override + void visitChildElements(_i3.ElementVisitor? visitor) => super.noSuchMethod( + Invocation.method(#visitChildElements, [visitor]), + returnValueForMissingStub: null, + ); + + @override + void dispatchNotification(_i3.Notification? notification) => + super.noSuchMethod( + Invocation.method(#dispatchNotification, [notification]), + returnValueForMissingStub: null, + ); + + @override + _i3.DiagnosticsNode describeElement( + String? name, { + _i4.DiagnosticsTreeStyle? style = _i4.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method(#describeElement, [name], {#style: style}), + returnValue: _FakeDiagnosticsNode_5( + this, + Invocation.method(#describeElement, [name], {#style: style}), + ), + ) + as _i3.DiagnosticsNode); + + @override + _i3.DiagnosticsNode describeWidget( + String? name, { + _i4.DiagnosticsTreeStyle? style = _i4.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method(#describeWidget, [name], {#style: style}), + returnValue: _FakeDiagnosticsNode_5( + this, + Invocation.method(#describeWidget, [name], {#style: style}), + ), + ) + as _i3.DiagnosticsNode); + + @override + List<_i3.DiagnosticsNode> describeMissingAncestor({ + required Type? expectedAncestorType, + }) => + (super.noSuchMethod( + Invocation.method(#describeMissingAncestor, [], { + #expectedAncestorType: expectedAncestorType, + }), + returnValue: <_i3.DiagnosticsNode>[], + ) + as List<_i3.DiagnosticsNode>); + + @override + _i3.DiagnosticsNode describeOwnershipChain(String? name) => + (super.noSuchMethod( + Invocation.method(#describeOwnershipChain, [name]), + returnValue: _FakeDiagnosticsNode_5( + this, + Invocation.method(#describeOwnershipChain, [name]), + ), + ) + as _i3.DiagnosticsNode); +} + +/// A class which mocks [Utils]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockUtils extends _i1.Mock implements _i8.Utils { + MockUtils() { + _i1.throwOnMissingStub(this); + } + + @override + double radians(double? degrees) => + (super.noSuchMethod( + Invocation.method(#radians, [degrees]), + returnValue: 0.0, + ) + as double); + + @override + double degrees(double? radians) => + (super.noSuchMethod( + Invocation.method(#degrees, [radians]), + returnValue: 0.0, + ) + as double); + + @override + double translateRotatedPosition(double? size, double? degree) => + (super.noSuchMethod( + Invocation.method(#translateRotatedPosition, [size, degree]), + returnValue: 0.0, + ) + as double); + + @override + _i2.Offset calculateRotationOffset(_i2.Size? size, double? degree) => + (super.noSuchMethod( + Invocation.method(#calculateRotationOffset, [size, degree]), + returnValue: _FakeOffset_6( + this, + Invocation.method(#calculateRotationOffset, [size, degree]), + ), + ) + as _i2.Offset); + + @override + _i3.BorderRadius? normalizeBorderRadius( + _i3.BorderRadius? borderRadius, + double? width, + ) => + (super.noSuchMethod( + Invocation.method(#normalizeBorderRadius, [borderRadius, width]), + ) + as _i3.BorderRadius?); + + @override + _i3.BorderSide normalizeBorderSide( + _i3.BorderSide? borderSide, + double? width, + ) => + (super.noSuchMethod( + Invocation.method(#normalizeBorderSide, [borderSide, width]), + returnValue: _FakeBorderSide_7( + this, + Invocation.method(#normalizeBorderSide, [borderSide, width]), + ), + ) + as _i3.BorderSide); + + @override + double getEfficientInterval( + double? axisViewSize, + double? diffInAxis, { + double? pixelPerInterval = 40.0, + }) => + (super.noSuchMethod( + Invocation.method( + #getEfficientInterval, + [axisViewSize, diffInAxis], + {#pixelPerInterval: pixelPerInterval}, + ), + returnValue: 0.0, + ) + as double); + + @override + double roundInterval(double? input) => + (super.noSuchMethod( + Invocation.method(#roundInterval, [input]), + returnValue: 0.0, + ) + as double); + + @override + int getFractionDigits(double? value) => + (super.noSuchMethod( + Invocation.method(#getFractionDigits, [value]), + returnValue: 0, + ) + as int); + + @override + String formatNumber(double? axisMin, double? axisMax, double? axisValue) => + (super.noSuchMethod( + Invocation.method(#formatNumber, [axisMin, axisMax, axisValue]), + returnValue: _i9.dummyValue( + this, + Invocation.method(#formatNumber, [axisMin, axisMax, axisValue]), + ), + ) + as String); + + @override + _i3.TextStyle getThemeAwareTextStyle( + _i3.BuildContext? context, + _i3.TextStyle? providedStyle, + ) => + (super.noSuchMethod( + Invocation.method(#getThemeAwareTextStyle, [ + context, + providedStyle, + ]), + returnValue: _FakeTextStyle_8( + this, + Invocation.method(#getThemeAwareTextStyle, [ + context, + providedStyle, + ]), + ), + ) + as _i3.TextStyle); + + @override + double getBestInitialIntervalValue( + double? min, + double? max, + double? interval, { + double? baseline = 0.0, + }) => + (super.noSuchMethod( + Invocation.method( + #getBestInitialIntervalValue, + [min, max, interval], + {#baseline: baseline}, + ), + returnValue: 0.0, + ) + as double); + + @override + double convertRadiusToSigma(double? radius) => + (super.noSuchMethod( + Invocation.method(#convertRadiusToSigma, [radius]), + returnValue: 0.0, + ) + as double); +} diff --git a/test/chart/pie_chart/pie_chart_renderer_test.dart b/test/chart/pie_chart/pie_chart_renderer_test.dart new file mode 100644 index 0000000..51c696e --- /dev/null +++ b/test/chart/pie_chart/pie_chart_renderer_test.dart @@ -0,0 +1,102 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart'; +import 'package:fl_chart/src/chart/pie_chart/pie_chart_painter.dart'; +import 'package:fl_chart/src/chart/pie_chart/pie_chart_renderer.dart'; +import 'package:fl_chart/src/utils/canvas_wrapper.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import '../data_pool.dart'; +import 'pie_chart_renderer_test.mocks.dart'; + +@GenerateMocks([Canvas, PaintingContext, BuildContext, PieChartPainter]) +void main() { + group('PieChartRenderer', () { + final data = PieChartData(); + + final targetData = PieChartData(centerSpaceRadius: 12); + + const textScaler = TextScaler.linear(4); + + final mockBuildContext = MockBuildContext(); + final renderBarChart = RenderPieChart( + mockBuildContext, + data, + targetData, + textScaler, + ); + + final mockPainter = MockPieChartPainter(); + final mockPaintingContext = MockPaintingContext(); + final mockCanvas = MockCanvas(); + const mockSize = Size(44, 44); + when(mockPaintingContext.canvas).thenAnswer((realInvocation) => mockCanvas); + renderBarChart + ..mockTestSize = mockSize + ..painter = mockPainter; + + test('test 1 correct data set', () { + expect(renderBarChart.data == data, true); + expect(renderBarChart.data == targetData, false); + expect(renderBarChart.targetData == targetData, true); + expect(renderBarChart.textScaler == textScaler, true); + expect(renderBarChart.paintHolder.data == data, true); + expect(renderBarChart.paintHolder.targetData == targetData, true); + expect(renderBarChart.paintHolder.textScaler == textScaler, true); + }); + + test('test 2 check paint function', () { + renderBarChart.paint(mockPaintingContext, const Offset(10, 10)); + verify(mockCanvas.save()).called(1); + verify(mockCanvas.translate(10, 10)).called(1); + final result = verify(mockPainter.paint(any, captureAny, captureAny)); + expect(result.callCount, 1); + + final canvasWrapper = result.captured[0] as CanvasWrapper; + expect(canvasWrapper.size, const Size(44, 44)); + expect(canvasWrapper.canvas, mockCanvas); + + final paintHolder = result.captured[1] as PaintHolder; + expect(paintHolder.data, data); + expect(paintHolder.targetData, targetData); + expect(paintHolder.textScaler, textScaler); + + verify(mockCanvas.restore()).called(1); + }); + + test('test 3 check getResponseAtLocation function', () { + final results = >[]; + when(mockPainter.handleTouch(captureAny, captureAny, captureAny)) + .thenAnswer((inv) { + results.add({ + 'local_position': inv.positionalArguments[0] as Offset, + 'size': inv.positionalArguments[1] as Size, + 'paint_holder': inv.positionalArguments[2] as PaintHolder, + }); + return MockData.pieTouchedSection1; + }); + final touchResponse = + renderBarChart.getResponseAtLocation(MockData.offset1); + expect(touchResponse.touchedSection, MockData.pieTouchedSection1); + expect(results[0]['local_position'] as Offset, MockData.offset1); + expect(results[0]['size'] as Size, mockSize); + final paintHolder = results[0]['paint_holder'] as PaintHolder; + expect(paintHolder.data, data); + expect(paintHolder.targetData, targetData); + expect(paintHolder.textScaler, textScaler); + }); + + test('test 4 check setters', () { + renderBarChart + ..data = targetData + ..targetData = data + ..textScaler = const TextScaler.linear(22); + + expect(renderBarChart.data, targetData); + expect(renderBarChart.targetData, data); + expect(renderBarChart.textScaler, const TextScaler.linear(22)); + }); + }); +} diff --git a/test/chart/pie_chart/pie_chart_renderer_test.mocks.dart b/test/chart/pie_chart/pie_chart_renderer_test.mocks.dart new file mode 100644 index 0000000..e08f28a --- /dev/null +++ b/test/chart/pie_chart/pie_chart_renderer_test.mocks.dart @@ -0,0 +1,1009 @@ +// Mocks generated by Mockito 5.4.5 from annotations +// in fl_chart/test/chart/pie_chart/pie_chart_renderer_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:typed_data' as _i8; +import 'dart:ui' as _i2; + +import 'package:fl_chart/fl_chart.dart' as _i7; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart' + as _i13; +import 'package:fl_chart/src/chart/base/line.dart' as _i14; +import 'package:fl_chart/src/chart/pie_chart/pie_chart_painter.dart' as _i11; +import 'package:fl_chart/src/utils/canvas_wrapper.dart' as _i12; +import 'package:flutter/foundation.dart' as _i5; +import 'package:flutter/gestures.dart' as _i9; +import 'package:flutter/material.dart' as _i6; +import 'package:flutter/rendering.dart' as _i3; +import 'package:flutter/src/rendering/layer.dart' as _i4; +import 'package:flutter/src/widgets/notification_listener.dart' as _i10; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeRect_0 extends _i1.SmartFake implements _i2.Rect { + _FakeRect_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeCanvas_1 extends _i1.SmartFake implements _i2.Canvas { + _FakeCanvas_1(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakePaintingContext_2 extends _i1.SmartFake + implements _i3.PaintingContext { + _FakePaintingContext_2(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeColorFilterLayer_3 extends _i1.SmartFake + implements _i4.ColorFilterLayer { + _FakeColorFilterLayer_3(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeOpacityLayer_4 extends _i1.SmartFake implements _i4.OpacityLayer { + _FakeOpacityLayer_4(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeWidget_5 extends _i1.SmartFake implements _i6.Widget { + _FakeWidget_5(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeInheritedWidget_6 extends _i1.SmartFake + implements _i6.InheritedWidget { + _FakeInheritedWidget_6(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeDiagnosticsNode_7 extends _i1.SmartFake + implements _i5.DiagnosticsNode { + _FakeDiagnosticsNode_7(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({ + _i5.TextTreeConfiguration? parentConfiguration, + _i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info, + }) => super.toString(); +} + +class _FakePath_8 extends _i1.SmartFake implements _i2.Path { + _FakePath_8(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakePieTouchedSection_9 extends _i1.SmartFake + implements _i7.PieTouchedSection { + _FakePieTouchedSection_9(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +/// A class which mocks [Canvas]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCanvas extends _i1.Mock implements _i2.Canvas { + MockCanvas() { + _i1.throwOnMissingStub(this); + } + + @override + void save() => super.noSuchMethod( + Invocation.method(#save, []), + returnValueForMissingStub: null, + ); + + @override + void saveLayer(_i2.Rect? bounds, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#saveLayer, [bounds, paint]), + returnValueForMissingStub: null, + ); + + @override + void restore() => super.noSuchMethod( + Invocation.method(#restore, []), + returnValueForMissingStub: null, + ); + + @override + void restoreToCount(int? count) => super.noSuchMethod( + Invocation.method(#restoreToCount, [count]), + returnValueForMissingStub: null, + ); + + @override + int getSaveCount() => + (super.noSuchMethod(Invocation.method(#getSaveCount, []), returnValue: 0) + as int); + + @override + void translate(double? dx, double? dy) => super.noSuchMethod( + Invocation.method(#translate, [dx, dy]), + returnValueForMissingStub: null, + ); + + @override + void scale(double? sx, [double? sy]) => super.noSuchMethod( + Invocation.method(#scale, [sx, sy]), + returnValueForMissingStub: null, + ); + + @override + void rotate(double? radians) => super.noSuchMethod( + Invocation.method(#rotate, [radians]), + returnValueForMissingStub: null, + ); + + @override + void skew(double? sx, double? sy) => super.noSuchMethod( + Invocation.method(#skew, [sx, sy]), + returnValueForMissingStub: null, + ); + + @override + void transform(_i8.Float64List? matrix4) => super.noSuchMethod( + Invocation.method(#transform, [matrix4]), + returnValueForMissingStub: null, + ); + + @override + _i8.Float64List getTransform() => + (super.noSuchMethod( + Invocation.method(#getTransform, []), + returnValue: _i8.Float64List(0), + ) + as _i8.Float64List); + + @override + void clipRect( + _i2.Rect? rect, { + _i2.ClipOp? clipOp = _i2.ClipOp.intersect, + bool? doAntiAlias = true, + }) => super.noSuchMethod( + Invocation.method( + #clipRect, + [rect], + {#clipOp: clipOp, #doAntiAlias: doAntiAlias}, + ), + returnValueForMissingStub: null, + ); + + @override + void clipRRect(_i2.RRect? rrect, {bool? doAntiAlias = true}) => + super.noSuchMethod( + Invocation.method(#clipRRect, [rrect], {#doAntiAlias: doAntiAlias}), + returnValueForMissingStub: null, + ); + + @override + void clipPath(_i2.Path? path, {bool? doAntiAlias = true}) => + super.noSuchMethod( + Invocation.method(#clipPath, [path], {#doAntiAlias: doAntiAlias}), + returnValueForMissingStub: null, + ); + + @override + _i2.Rect getLocalClipBounds() => + (super.noSuchMethod( + Invocation.method(#getLocalClipBounds, []), + returnValue: _FakeRect_0( + this, + Invocation.method(#getLocalClipBounds, []), + ), + ) + as _i2.Rect); + + @override + _i2.Rect getDestinationClipBounds() => + (super.noSuchMethod( + Invocation.method(#getDestinationClipBounds, []), + returnValue: _FakeRect_0( + this, + Invocation.method(#getDestinationClipBounds, []), + ), + ) + as _i2.Rect); + + @override + void drawColor(_i2.Color? color, _i2.BlendMode? blendMode) => + super.noSuchMethod( + Invocation.method(#drawColor, [color, blendMode]), + returnValueForMissingStub: null, + ); + + @override + void drawLine(_i2.Offset? p1, _i2.Offset? p2, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawLine, [p1, p2, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawPaint(_i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawPaint, [paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRect(_i2.Rect? rect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRect, [rect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRRect(_i2.RRect? rrect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRRect, [rrect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawDRRect(_i2.RRect? outer, _i2.RRect? inner, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawDRRect, [outer, inner, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawOval(_i2.Rect? rect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawOval, [rect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawCircle(_i2.Offset? c, double? radius, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawCircle, [c, radius, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawArc( + _i2.Rect? rect, + double? startAngle, + double? sweepAngle, + bool? useCenter, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawArc, [ + rect, + startAngle, + sweepAngle, + useCenter, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawPath(_i2.Path? path, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawPath, [path, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImage(_i2.Image? image, _i2.Offset? offset, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawImage, [image, offset, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImageRect( + _i2.Image? image, + _i2.Rect? src, + _i2.Rect? dst, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawImageRect, [image, src, dst, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImageNine( + _i2.Image? image, + _i2.Rect? center, + _i2.Rect? dst, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawImageNine, [image, center, dst, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawPicture(_i2.Picture? picture) => super.noSuchMethod( + Invocation.method(#drawPicture, [picture]), + returnValueForMissingStub: null, + ); + + @override + void drawParagraph(_i2.Paragraph? paragraph, _i2.Offset? offset) => + super.noSuchMethod( + Invocation.method(#drawParagraph, [paragraph, offset]), + returnValueForMissingStub: null, + ); + + @override + void drawPoints( + _i2.PointMode? pointMode, + List<_i2.Offset>? points, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawPoints, [pointMode, points, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRawPoints( + _i2.PointMode? pointMode, + _i8.Float32List? points, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawRawPoints, [pointMode, points, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawVertices( + _i2.Vertices? vertices, + _i2.BlendMode? blendMode, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawVertices, [vertices, blendMode, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawAtlas( + _i2.Image? atlas, + List<_i2.RSTransform>? transforms, + List<_i2.Rect>? rects, + List<_i2.Color>? colors, + _i2.BlendMode? blendMode, + _i2.Rect? cullRect, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawAtlas, [ + atlas, + transforms, + rects, + colors, + blendMode, + cullRect, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawRawAtlas( + _i2.Image? atlas, + _i8.Float32List? rstTransforms, + _i8.Float32List? rects, + _i8.Int32List? colors, + _i2.BlendMode? blendMode, + _i2.Rect? cullRect, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawRawAtlas, [ + atlas, + rstTransforms, + rects, + colors, + blendMode, + cullRect, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawShadow( + _i2.Path? path, + _i2.Color? color, + double? elevation, + bool? transparentOccluder, + ) => super.noSuchMethod( + Invocation.method(#drawShadow, [ + path, + color, + elevation, + transparentOccluder, + ]), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [PaintingContext]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPaintingContext extends _i1.Mock implements _i3.PaintingContext { + MockPaintingContext() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.Rect get estimatedBounds => + (super.noSuchMethod( + Invocation.getter(#estimatedBounds), + returnValue: _FakeRect_0(this, Invocation.getter(#estimatedBounds)), + ) + as _i2.Rect); + + @override + _i2.Canvas get canvas => + (super.noSuchMethod( + Invocation.getter(#canvas), + returnValue: _FakeCanvas_1(this, Invocation.getter(#canvas)), + ) + as _i2.Canvas); + + @override + void paintChild(_i3.RenderObject? child, _i2.Offset? offset) => + super.noSuchMethod( + Invocation.method(#paintChild, [child, offset]), + returnValueForMissingStub: null, + ); + + @override + void appendLayer(_i4.Layer? layer) => super.noSuchMethod( + Invocation.method(#appendLayer, [layer]), + returnValueForMissingStub: null, + ); + + @override + _i2.VoidCallback addCompositionCallback(_i4.CompositionCallback? callback) => + (super.noSuchMethod( + Invocation.method(#addCompositionCallback, [callback]), + returnValue: () {}, + ) + as _i2.VoidCallback); + + @override + void stopRecordingIfNeeded() => super.noSuchMethod( + Invocation.method(#stopRecordingIfNeeded, []), + returnValueForMissingStub: null, + ); + + @override + void setIsComplexHint() => super.noSuchMethod( + Invocation.method(#setIsComplexHint, []), + returnValueForMissingStub: null, + ); + + @override + void setWillChangeHint() => super.noSuchMethod( + Invocation.method(#setWillChangeHint, []), + returnValueForMissingStub: null, + ); + + @override + void addLayer(_i4.Layer? layer) => super.noSuchMethod( + Invocation.method(#addLayer, [layer]), + returnValueForMissingStub: null, + ); + + @override + void pushLayer( + _i4.ContainerLayer? childLayer, + _i3.PaintingContextCallback? painter, + _i2.Offset? offset, { + _i2.Rect? childPaintBounds, + }) => super.noSuchMethod( + Invocation.method( + #pushLayer, + [childLayer, painter, offset], + {#childPaintBounds: childPaintBounds}, + ), + returnValueForMissingStub: null, + ); + + @override + _i3.PaintingContext createChildContext( + _i4.ContainerLayer? childLayer, + _i2.Rect? bounds, + ) => + (super.noSuchMethod( + Invocation.method(#createChildContext, [childLayer, bounds]), + returnValue: _FakePaintingContext_2( + this, + Invocation.method(#createChildContext, [childLayer, bounds]), + ), + ) + as _i3.PaintingContext); + + @override + _i4.ClipRectLayer? pushClipRect( + bool? needsCompositing, + _i2.Offset? offset, + _i2.Rect? clipRect, + _i3.PaintingContextCallback? painter, { + _i2.Clip? clipBehavior = _i2.Clip.hardEdge, + _i4.ClipRectLayer? oldLayer, + }) => + (super.noSuchMethod( + Invocation.method( + #pushClipRect, + [needsCompositing, offset, clipRect, painter], + {#clipBehavior: clipBehavior, #oldLayer: oldLayer}, + ), + ) + as _i4.ClipRectLayer?); + + @override + _i4.ClipRRectLayer? pushClipRRect( + bool? needsCompositing, + _i2.Offset? offset, + _i2.Rect? bounds, + _i2.RRect? clipRRect, + _i3.PaintingContextCallback? painter, { + _i2.Clip? clipBehavior = _i2.Clip.antiAlias, + _i4.ClipRRectLayer? oldLayer, + }) => + (super.noSuchMethod( + Invocation.method( + #pushClipRRect, + [needsCompositing, offset, bounds, clipRRect, painter], + {#clipBehavior: clipBehavior, #oldLayer: oldLayer}, + ), + ) + as _i4.ClipRRectLayer?); + + @override + _i4.ClipPathLayer? pushClipPath( + bool? needsCompositing, + _i2.Offset? offset, + _i2.Rect? bounds, + _i2.Path? clipPath, + _i3.PaintingContextCallback? painter, { + _i2.Clip? clipBehavior = _i2.Clip.antiAlias, + _i4.ClipPathLayer? oldLayer, + }) => + (super.noSuchMethod( + Invocation.method( + #pushClipPath, + [needsCompositing, offset, bounds, clipPath, painter], + {#clipBehavior: clipBehavior, #oldLayer: oldLayer}, + ), + ) + as _i4.ClipPathLayer?); + + @override + _i4.ColorFilterLayer pushColorFilter( + _i2.Offset? offset, + _i2.ColorFilter? colorFilter, + _i3.PaintingContextCallback? painter, { + _i4.ColorFilterLayer? oldLayer, + }) => + (super.noSuchMethod( + Invocation.method( + #pushColorFilter, + [offset, colorFilter, painter], + {#oldLayer: oldLayer}, + ), + returnValue: _FakeColorFilterLayer_3( + this, + Invocation.method( + #pushColorFilter, + [offset, colorFilter, painter], + {#oldLayer: oldLayer}, + ), + ), + ) + as _i4.ColorFilterLayer); + + @override + _i4.TransformLayer? pushTransform( + bool? needsCompositing, + _i2.Offset? offset, + _i9.Matrix4? transform, + _i3.PaintingContextCallback? painter, { + _i4.TransformLayer? oldLayer, + }) => + (super.noSuchMethod( + Invocation.method( + #pushTransform, + [needsCompositing, offset, transform, painter], + {#oldLayer: oldLayer}, + ), + ) + as _i4.TransformLayer?); + + @override + _i4.OpacityLayer pushOpacity( + _i2.Offset? offset, + int? alpha, + _i3.PaintingContextCallback? painter, { + _i4.OpacityLayer? oldLayer, + }) => + (super.noSuchMethod( + Invocation.method( + #pushOpacity, + [offset, alpha, painter], + {#oldLayer: oldLayer}, + ), + returnValue: _FakeOpacityLayer_4( + this, + Invocation.method( + #pushOpacity, + [offset, alpha, painter], + {#oldLayer: oldLayer}, + ), + ), + ) + as _i4.OpacityLayer); + + @override + void clipPathAndPaint( + _i2.Path? path, + _i2.Clip? clipBehavior, + _i2.Rect? bounds, + _i2.VoidCallback? painter, + ) => super.noSuchMethod( + Invocation.method(#clipPathAndPaint, [path, clipBehavior, bounds, painter]), + returnValueForMissingStub: null, + ); + + @override + void clipRRectAndPaint( + _i2.RRect? rrect, + _i2.Clip? clipBehavior, + _i2.Rect? bounds, + _i2.VoidCallback? painter, + ) => super.noSuchMethod( + Invocation.method(#clipRRectAndPaint, [ + rrect, + clipBehavior, + bounds, + painter, + ]), + returnValueForMissingStub: null, + ); + + @override + void clipRectAndPaint( + _i2.Rect? rect, + _i2.Clip? clipBehavior, + _i2.Rect? bounds, + _i2.VoidCallback? painter, + ) => super.noSuchMethod( + Invocation.method(#clipRectAndPaint, [rect, clipBehavior, bounds, painter]), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [BuildContext]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockBuildContext extends _i1.Mock implements _i6.BuildContext { + MockBuildContext() { + _i1.throwOnMissingStub(this); + } + + @override + _i6.Widget get widget => + (super.noSuchMethod( + Invocation.getter(#widget), + returnValue: _FakeWidget_5(this, Invocation.getter(#widget)), + ) + as _i6.Widget); + + @override + bool get mounted => + (super.noSuchMethod(Invocation.getter(#mounted), returnValue: false) + as bool); + + @override + bool get debugDoingBuild => + (super.noSuchMethod( + Invocation.getter(#debugDoingBuild), + returnValue: false, + ) + as bool); + + @override + _i6.InheritedWidget dependOnInheritedElement( + _i6.InheritedElement? ancestor, { + Object? aspect, + }) => + (super.noSuchMethod( + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + returnValue: _FakeInheritedWidget_6( + this, + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + ), + ) + as _i6.InheritedWidget); + + @override + void visitAncestorElements(_i6.ConditionalElementVisitor? visitor) => + super.noSuchMethod( + Invocation.method(#visitAncestorElements, [visitor]), + returnValueForMissingStub: null, + ); + + @override + void visitChildElements(_i6.ElementVisitor? visitor) => super.noSuchMethod( + Invocation.method(#visitChildElements, [visitor]), + returnValueForMissingStub: null, + ); + + @override + void dispatchNotification(_i10.Notification? notification) => + super.noSuchMethod( + Invocation.method(#dispatchNotification, [notification]), + returnValueForMissingStub: null, + ); + + @override + _i5.DiagnosticsNode describeElement( + String? name, { + _i5.DiagnosticsTreeStyle? style = _i5.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method(#describeElement, [name], {#style: style}), + returnValue: _FakeDiagnosticsNode_7( + this, + Invocation.method(#describeElement, [name], {#style: style}), + ), + ) + as _i5.DiagnosticsNode); + + @override + _i5.DiagnosticsNode describeWidget( + String? name, { + _i5.DiagnosticsTreeStyle? style = _i5.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method(#describeWidget, [name], {#style: style}), + returnValue: _FakeDiagnosticsNode_7( + this, + Invocation.method(#describeWidget, [name], {#style: style}), + ), + ) + as _i5.DiagnosticsNode); + + @override + List<_i5.DiagnosticsNode> describeMissingAncestor({ + required Type? expectedAncestorType, + }) => + (super.noSuchMethod( + Invocation.method(#describeMissingAncestor, [], { + #expectedAncestorType: expectedAncestorType, + }), + returnValue: <_i5.DiagnosticsNode>[], + ) + as List<_i5.DiagnosticsNode>); + + @override + _i5.DiagnosticsNode describeOwnershipChain(String? name) => + (super.noSuchMethod( + Invocation.method(#describeOwnershipChain, [name]), + returnValue: _FakeDiagnosticsNode_7( + this, + Invocation.method(#describeOwnershipChain, [name]), + ), + ) + as _i5.DiagnosticsNode); +} + +/// A class which mocks [PieChartPainter]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPieChartPainter extends _i1.Mock implements _i11.PieChartPainter { + MockPieChartPainter() { + _i1.throwOnMissingStub(this); + } + + @override + void paint( + _i6.BuildContext? context, + _i12.CanvasWrapper? canvasWrapper, + _i13.PaintHolder<_i7.PieChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#paint, [context, canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + List calculateSectionsAngle( + List<_i7.PieChartSectionData>? sections, + double? sumValue, + ) => + (super.noSuchMethod( + Invocation.method(#calculateSectionsAngle, [sections, sumValue]), + returnValue: [], + ) + as List); + + @override + void drawCenterSpace( + _i12.CanvasWrapper? canvasWrapper, + double? centerRadius, + _i13.PaintHolder<_i7.PieChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawCenterSpace, [canvasWrapper, centerRadius, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawSections( + _i12.CanvasWrapper? canvasWrapper, + List? sectionsAngle, + double? centerRadius, + _i13.PaintHolder<_i7.PieChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawSections, [ + canvasWrapper, + sectionsAngle, + centerRadius, + holder, + ]), + returnValueForMissingStub: null, + ); + + @override + _i2.Path generateSectionPath( + _i7.PieChartSectionData? section, + double? sectionSpace, + double? tempAngle, + double? sectionDegree, + _i2.Offset? center, + double? centerRadius, + ) => + (super.noSuchMethod( + Invocation.method(#generateSectionPath, [ + section, + sectionSpace, + tempAngle, + sectionDegree, + center, + centerRadius, + ]), + returnValue: _FakePath_8( + this, + Invocation.method(#generateSectionPath, [ + section, + sectionSpace, + tempAngle, + sectionDegree, + center, + centerRadius, + ]), + ), + ) + as _i2.Path); + + @override + _i2.Path createRectPathAroundLine(_i14.Line? line, double? width) => + (super.noSuchMethod( + Invocation.method(#createRectPathAroundLine, [line, width]), + returnValue: _FakePath_8( + this, + Invocation.method(#createRectPathAroundLine, [line, width]), + ), + ) + as _i2.Path); + + @override + void drawSection( + _i7.PieChartSectionData? section, + _i2.Path? sectionPath, + _i12.CanvasWrapper? canvasWrapper, + ) => super.noSuchMethod( + Invocation.method(#drawSection, [section, sectionPath, canvasWrapper]), + returnValueForMissingStub: null, + ); + + @override + void drawSectionStroke( + _i7.PieChartSectionData? section, + _i2.Path? sectionPath, + _i12.CanvasWrapper? canvasWrapper, + _i2.Size? viewSize, + ) => super.noSuchMethod( + Invocation.method(#drawSectionStroke, [ + section, + sectionPath, + canvasWrapper, + viewSize, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawTexts( + _i6.BuildContext? context, + _i12.CanvasWrapper? canvasWrapper, + _i13.PaintHolder<_i7.PieChartData>? holder, + double? centerRadius, + ) => super.noSuchMethod( + Invocation.method(#drawTexts, [ + context, + canvasWrapper, + holder, + centerRadius, + ]), + returnValueForMissingStub: null, + ); + + @override + double calculateCenterRadius( + _i2.Size? viewSize, + _i13.PaintHolder<_i7.PieChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#calculateCenterRadius, [viewSize, holder]), + returnValue: 0.0, + ) + as double); + + @override + _i7.PieTouchedSection handleTouch( + _i2.Offset? localPosition, + _i2.Size? viewSize, + _i13.PaintHolder<_i7.PieChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#handleTouch, [localPosition, viewSize, holder]), + returnValue: _FakePieTouchedSection_9( + this, + Invocation.method(#handleTouch, [ + localPosition, + viewSize, + holder, + ]), + ), + ) + as _i7.PieTouchedSection); + + @override + Map getBadgeOffsets( + _i2.Size? viewSize, + _i13.PaintHolder<_i7.PieChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#getBadgeOffsets, [viewSize, holder]), + returnValue: {}, + ) + as Map); +} diff --git a/test/chart/radar_chart/radar_chart_data_test.dart b/test/chart/radar_chart/radar_chart_data_test.dart new file mode 100644 index 0000000..a45e22e --- /dev/null +++ b/test/chart/radar_chart/radar_chart_data_test.dart @@ -0,0 +1,273 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../data_pool.dart'; + +void main() { + group('RadarChart Data equality check', () { + test('RadarChartData equality test', () { + /// object equality test + expect(radarChartData1 == radarChartData1Clone, true); + + expect( + radarChartData1 == + radarChartData1Clone.copyWith(dataSets: [radarDataSet2]), + false, + ); + + expect( + radarChartData1 == + radarChartData1Clone.copyWith(radarBackgroundColor: Colors.black), + false, + ); + + expect( + radarChartData1 == + radarChartData1Clone.copyWith( + borderData: FlBorderData( + show: true, + border: Border.all(color: Colors.green), + ), + ), + true, + ); + + expect( + radarChartData1 == + radarChartData1Clone.copyWith( + borderData: FlBorderData( + show: false, + border: Border.all(), + ), + ), + false, + ); + + expect( + radarChartData1 == + radarChartData1Clone.copyWith( + radarBorderData: const BorderSide( + width: 200, + color: Colors.red, + ), + ), + false, + ); + + expect( + radarChartData1 == + radarChartData1Clone.copyWith( + radarShape: RadarShape.polygon, + ), + false, + ); + + expect( + radarChartData1 == + radarChartData1Clone.copyWith(radarTouchData: radarTouchData2), + false, + ); + + expect( + radarChartData1 == + radarChartData1Clone.copyWith( + gridBorderData: const BorderSide( + color: Colors.black54, + width: 2.1, + ), + ), + false, + ); + + expect( + radarChartData1 == radarChartData1Clone.copyWith(tickCount: 8), + false, + ); + + expect( + radarChartData1 == + radarChartData1Clone.copyWith(ticksTextStyle: const TextStyle()), + false, + ); + + expect( + radarChartData1 == + radarChartData1Clone.copyWith( + ticksTextStyle: radarChartData2.ticksTextStyle, + ), + false, + ); + + expect( + radarChartData1 == + radarChartData1Clone.copyWith( + tickBorderData: radarChartData2.tickBorderData, + ), + false, + ); + + expect( + radarChartData1 == + radarChartData1Clone.copyWith(titlePositionPercentageOffset: 0.2), + true, + ); + + expect( + radarChartData1 == + radarChartData1Clone.copyWith( + titlePositionPercentageOffset: + radarChartData2.titlePositionPercentageOffset, + ), + false, + ); + + expect( + radarChartData1 == + radarChartData1Clone.copyWith(titleTextStyle: const TextStyle()), + false, + ); + + expect( + radarChartData1 == + radarChartData1Clone.copyWith( + titleTextStyle: radarChartData2.titleTextStyle, + ), + false, + ); + + expect( + radarChartData1 == + radarChartData1Clone.copyWith( + titleTextStyle: radarChartData2.titleTextStyle, + ), + false, + ); + }); + + test('RadarDataSet equality test', () { + expect(radarDataSet1 == radarDataSet1Clone, true); + + expect(radarDataSet1 == radarDataSet2, false); + + expect( + radarDataSet1 == + radarDataSet1Clone.copyWith( + dataEntries: const [ + RadarEntry(value: 5), + RadarEntry(value: 5), + RadarEntry(value: 5), + ], + ), + false, + ); + + expect( + radarDataSet1 == radarDataSet1Clone.copyWith(fillColor: Colors.grey), + true, + ); + + expect( + radarDataSet1 == radarDataSet1Clone.copyWith(fillColor: Colors.pink), + false, + ); + + expect( + radarDataSet1 == radarDataSet1Clone.copyWith(borderColor: Colors.blue), + true, + ); + + expect( + radarDataSet1 == radarDataSet1Clone.copyWith(borderColor: Colors.pink), + false, + ); + + expect( + radarDataSet1 == radarDataSet1Clone.copyWith(borderWidth: 3), + true, + ); + + expect( + radarDataSet1 == radarDataSet1Clone.copyWith(borderWidth: 3.2), + false, + ); + + expect( + radarDataSet1 == radarDataSet1Clone.copyWith(borderWidth: 3.00002), + false, + ); + + expect( + radarDataSet1 == radarDataSet1Clone.copyWith(entryRadius: 3), + true, + ); + + expect( + radarDataSet1 == radarDataSet1Clone.copyWith(entryRadius: 3.2), + false, + ); + + expect( + radarDataSet1 == radarDataSet1Clone.copyWith(entryRadius: 3.002), + false, + ); + }); + + test('RadarTouchData equality test', () { + expect(radarTouchData1 == radarTouchData1Clone, true); + + expect(radarTouchData1 == radarTouchData2, false); + + expect( + radarTouchData1 == + RadarTouchData( + enabled: false, + touchCallback: radarTouchCallback, + touchSpotThreshold: 12, + ), + false, + ); + + expect( + radarTouchData1 == + RadarTouchData( + enabled: true, + touchCallback: radarTouchCallback, + touchSpotThreshold: 2, + ), + false, + ); + + expect( + radarTouchData1 == + RadarTouchData( + enabled: true, + touchCallback: (event, value) {}, + touchSpotThreshold: 12, + ), + false, + ); + + expect( + radarTouchData1 == + RadarTouchData( + enabled: true, + touchCallback: radarTouchCallback, + touchSpotThreshold: 12, + longPressDuration: Duration.zero, + ), + false, + ); + }); + + test('RadarTouchedSpot equality test', () { + expect(radarTouchedSpot1 == radarTouchedSpotClone1, true); + expect(radarTouchedSpot1 == radarTouchedSpot2, false); + expect(radarTouchedSpot1 == radarTouchedSpot3, false); + expect(radarTouchedSpot1 == radarTouchedSpot4, false); + expect(radarTouchedSpot1 == radarTouchedSpot5, false); + expect(radarTouchedSpot1 == radarTouchedSpot6, false); + expect(radarTouchedSpot1 == radarTouchedSpot7, false); + }); + }); +} diff --git a/test/chart/radar_chart/radar_chart_painter_test.dart b/test/chart/radar_chart/radar_chart_painter_test.dart new file mode 100644 index 0000000..640354b --- /dev/null +++ b/test/chart/radar_chart/radar_chart_painter_test.dart @@ -0,0 +1,1412 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart'; +import 'package:fl_chart/src/chart/radar_chart/radar_chart_painter.dart'; +import 'package:fl_chart/src/utils/canvas_wrapper.dart'; +import 'package:fl_chart/src/utils/utils.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import '../data_pool.dart'; +import 'radar_chart_painter_test.mocks.dart'; + +@GenerateMocks([Canvas, CanvasWrapper, BuildContext, Utils]) +void main() { + final utilsMainInstance = Utils(); + group('paint()', () { + test('test 1', () { + const viewSize = Size(400, 400); + final data = RadarChartData( + dataSets: [ + RadarDataSet( + dataEntries: const [ + RadarEntry(value: 12), + RadarEntry(value: 11), + RadarEntry(value: 10), + ], + ), + RadarDataSet( + dataEntries: const [ + RadarEntry(value: 2), + RadarEntry(value: 2), + RadarEntry(value: 2), + ], + ), + RadarDataSet( + dataEntries: const [ + RadarEntry(value: 4), + RadarEntry(value: 4), + RadarEntry(value: 4), + ], + ), + ], + ); + + final radarPainter = RadarChartPainter(); + final holder = PaintHolder( + data, + data, + TextScaler.noScaling, + ); + + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.getThemeAwareTextStyle(any, any)) + .thenAnswer((realInvocation) => textStyle1); + when(mockUtils.calculateRotationOffset(any, any)) + .thenAnswer((realInvocation) => Offset.zero); + when(mockUtils.convertRadiusToSigma(any)) + .thenAnswer((realInvocation) => 4.0); + when(mockUtils.getEfficientInterval(any, any)) + .thenAnswer((realInvocation) => 1.0); + when(mockUtils.getBestInitialIntervalValue(any, any, any)) + .thenAnswer((realInvocation) => 1.0); + when(mockUtils.normalizeBorderRadius(any, any)) + .thenAnswer((realInvocation) => BorderRadius.zero); + when(mockUtils.normalizeBorderSide(any, any)).thenAnswer( + (realInvocation) => const BorderSide(color: MockData.color0), + ); + + final mockBuildContext = MockBuildContext(); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + radarPainter.paint( + mockBuildContext, + mockCanvasWrapper, + holder, + ); + + verify(mockCanvasWrapper.drawCircle(any, any, any)).called(12); + verify(mockCanvasWrapper.drawLine(any, any, any)).called(3); + Utils.changeInstance(utilsMainInstance); + }); + }); + + group('drawTicks()', () { + test('test 1', () { + const viewSize = Size(400, 300); + + final data = RadarChartData( + dataSets: [ + RadarDataSet( + dataEntries: [ + const RadarEntry(value: 1), + const RadarEntry(value: 2), + const RadarEntry(value: 3), + ], + ), + RadarDataSet( + dataEntries: [ + const RadarEntry(value: 3), + const RadarEntry(value: 1), + const RadarEntry(value: 2), + ], + ), + RadarDataSet( + dataEntries: [ + const RadarEntry(value: 2), + const RadarEntry(value: 3), + const RadarEntry(value: 1), + ], + ), + ], + radarBorderData: const BorderSide(color: MockData.color6, width: 33), + tickBorderData: const BorderSide(color: MockData.color5, width: 55), + radarBackgroundColor: MockData.color2, + ); + + final radarChartPainter = RadarChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final mockUtils = MockUtils(); + when(mockUtils.getThemeAwareTextStyle(any, any)) + .thenReturn(MockData.textStyle1); + Utils.changeInstance(mockUtils); + + final mockContext = MockBuildContext(); + + final drawCircleResults = >[]; + when(mockCanvasWrapper.drawCircle(captureAny, captureAny, captureAny)) + .thenAnswer((inv) { + drawCircleResults.add({ + 'offset': inv.positionalArguments[0] as Offset, + 'radius': inv.positionalArguments[1] as double, + 'paint_color': (inv.positionalArguments[2] as Paint).color, + 'paint_style': (inv.positionalArguments[2] as Paint).style, + 'paint_stroke': (inv.positionalArguments[2] as Paint).strokeWidth, + }); + }); + + radarChartPainter.drawTicks(mockContext, mockCanvasWrapper, holder); + + expect(drawCircleResults.length, 3); + + // Background circle + expect(drawCircleResults[0]['offset'], const Offset(200, 150)); + expect(drawCircleResults[0]['radius'], 120); + expect( + drawCircleResults[0]['paint_color'], + isSameColorAs(MockData.color2), + ); + expect(drawCircleResults[0]['paint_style'], PaintingStyle.fill); + + // Border circle + expect(drawCircleResults[1]['offset'], const Offset(200, 150)); + expect(drawCircleResults[1]['radius'], 120); + expect( + drawCircleResults[1]['paint_color'], + isSameColorAs(MockData.color6), + ); + expect(drawCircleResults[1]['paint_stroke'], 33); + expect(drawCircleResults[1]['paint_style'], PaintingStyle.stroke); + + // First Tick + expect(drawCircleResults[2]['offset'], const Offset(200, 150)); + expect(drawCircleResults[2]['radius'], 60); + expect( + drawCircleResults[2]['paint_color'], + isSameColorAs(MockData.color5), + ); + expect(drawCircleResults[2]['paint_stroke'], 55); + expect(drawCircleResults[2]['paint_style'], PaintingStyle.stroke); + + final result = verify(mockCanvasWrapper.drawText(captureAny, captureAny)); + expect(result.callCount, 1); + final tp = result.captured[0] as TextPainter; + expect((tp.text as TextSpan?)!.text, '1.0'); + expect((tp.text as TextSpan?)!.style, MockData.textStyle1); + expect(result.captured[1] as Offset, const Offset(205, 76)); + }); + + test('test 2', () { + const viewSize = Size(400, 300); + + final data = RadarChartData( + dataSets: [ + RadarDataSet( + dataEntries: [ + const RadarEntry(value: 1), + const RadarEntry(value: 2), + const RadarEntry(value: 3), + ], + ), + RadarDataSet( + dataEntries: [ + const RadarEntry(value: 3), + const RadarEntry(value: 1), + const RadarEntry(value: 2), + ], + ), + RadarDataSet( + dataEntries: [ + const RadarEntry(value: 2), + const RadarEntry(value: 3), + const RadarEntry(value: 1), + ], + ), + ], + radarBorderData: const BorderSide(color: MockData.color6, width: 33), + radarShape: RadarShape.polygon, + tickBorderData: const BorderSide(color: MockData.color5, width: 55), + radarBackgroundColor: MockData.color2, + ); + + final radarChartPainter = RadarChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final mockUtils = MockUtils(); + when(mockUtils.getThemeAwareTextStyle(any, any)) + .thenReturn(MockData.textStyle1); + Utils.changeInstance(mockUtils); + + final mockContext = MockBuildContext(); + + final drawPathResult = >[]; + when(mockCanvasWrapper.drawPath(captureAny, captureAny)) + .thenAnswer((inv) { + drawPathResult.add({ + 'path': inv.positionalArguments[0] as Path, + 'paint_color': (inv.positionalArguments[1] as Paint).color, + 'paint_stroke': (inv.positionalArguments[1] as Paint).strokeWidth, + 'paint_style': (inv.positionalArguments[1] as Paint).style, + }); + }); + + radarChartPainter.drawTicks(mockContext, mockCanvasWrapper, holder); + + expect(drawPathResult.length, 3); + + // Background circle + expect( + drawPathResult[0]['paint_color'], + isSameColorAs(MockData.color2), + ); + expect(drawPathResult[0]['paint_stroke'], 0); + expect(drawPathResult[0]['paint_style'], PaintingStyle.fill); + + // Border circle + expect( + drawPathResult[1]['paint_color'], + isSameColorAs(MockData.color6), + ); + expect(drawPathResult[1]['paint_stroke'], 33); + expect(drawPathResult[1]['paint_style'], PaintingStyle.stroke); + + // First Tick + expect( + drawPathResult[2]['paint_color'], + isSameColorAs(MockData.color5), + ); + expect(drawPathResult[2]['paint_stroke'], 55); + expect(drawPathResult[2]['paint_style'], PaintingStyle.stroke); + + final result = verify(mockCanvasWrapper.drawText(captureAny, captureAny)); + expect(result.callCount, 1); + final tp = result.captured[0] as TextPainter; + expect((tp.text as TextSpan?)!.text, '1.0'); + expect((tp.text as TextSpan?)!.style, MockData.textStyle1); + expect(result.captured[1] as Offset, const Offset(205, 76)); + }); + }); + + group('drawGrids()', () { + test('test 1', () { + const viewSize = Size(400, 300); + + final data = RadarChartData( + dataSets: [ + RadarDataSet( + dataEntries: [ + const RadarEntry(value: 1), + const RadarEntry(value: 2), + const RadarEntry(value: 3), + ], + ), + RadarDataSet( + dataEntries: [ + const RadarEntry(value: 3), + const RadarEntry(value: 1), + const RadarEntry(value: 2), + ], + ), + RadarDataSet( + dataEntries: [ + const RadarEntry(value: 2), + const RadarEntry(value: 3), + const RadarEntry(value: 1), + ], + ), + ], + radarBorderData: const BorderSide(color: MockData.color6, width: 33), + tickBorderData: const BorderSide(color: MockData.color5, width: 55), + gridBorderData: const BorderSide(color: MockData.color3, width: 3), + radarBackgroundColor: MockData.color2, + ); + + final radarChartPainter = RadarChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final mockUtils = MockUtils(); + when(mockUtils.getThemeAwareTextStyle(any, any)) + .thenReturn(MockData.textStyle1); + Utils.changeInstance(mockUtils); + + final drawLineResults = >[]; + when(mockCanvasWrapper.drawLine(captureAny, captureAny, captureAny)) + .thenAnswer((inv) { + drawLineResults.add({ + 'offset_from': inv.positionalArguments[0] as Offset, + 'offset_to': inv.positionalArguments[1] as Offset, + 'paint_color': (inv.positionalArguments[2] as Paint).color, + 'paint_style': (inv.positionalArguments[2] as Paint).style, + 'paint_stroke': (inv.positionalArguments[2] as Paint).strokeWidth, + }); + }); + + radarChartPainter.drawGrids(mockCanvasWrapper, holder); + expect(drawLineResults.length, 3); + + expect(drawLineResults[0]['offset_from'], const Offset(200, 150)); + expect(drawLineResults[0]['offset_to'], const Offset(200, 30)); + expect( + drawLineResults[0]['paint_color'], + isSameColorAs(MockData.color3), + ); + expect(drawLineResults[0]['paint_style'], PaintingStyle.stroke); + expect(drawLineResults[0]['paint_stroke'], 3); + + expect(drawLineResults[1]['offset_from'], const Offset(200, 150)); + expect( + drawLineResults[1]['offset_to'], + const Offset(303.92304845413264, 209.99999999999997), + ); + expect( + drawLineResults[1]['paint_color'], + isSameColorAs(MockData.color3), + ); + expect(drawLineResults[1]['paint_style'], PaintingStyle.stroke); + expect(drawLineResults[1]['paint_stroke'], 3); + + expect(drawLineResults[2]['offset_from'], const Offset(200, 150)); + expect( + drawLineResults[2]['offset_to'], + const Offset(96.07695154586739, 210.00000000000006), + ); + expect( + drawLineResults[2]['paint_color'], + isSameColorAs(MockData.color3), + ); + expect(drawLineResults[2]['paint_style'], PaintingStyle.stroke); + expect(drawLineResults[2]['paint_stroke'], 3); + }); + }); + + group('drawGrids()', () { + test('test 1', () { + const viewSize = Size(400, 300); + + final data = RadarChartData( + dataSets: [ + RadarDataSet( + dataEntries: [ + const RadarEntry(value: 1), + const RadarEntry(value: 2), + const RadarEntry(value: 3), + ], + ), + RadarDataSet( + dataEntries: [ + const RadarEntry(value: 3), + const RadarEntry(value: 1), + const RadarEntry(value: 2), + ], + ), + RadarDataSet( + dataEntries: [ + const RadarEntry(value: 2), + const RadarEntry(value: 3), + const RadarEntry(value: 1), + ], + ), + ], + titleTextStyle: MockData.textStyle4, + radarBorderData: const BorderSide(color: MockData.color6, width: 33), + tickBorderData: const BorderSide(color: MockData.color5, width: 55), + gridBorderData: const BorderSide(color: MockData.color3, width: 3), + radarBackgroundColor: MockData.color2, + ); + + final radarChartPainter = RadarChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final mockUtils = MockUtils(); + when(mockUtils.getThemeAwareTextStyle(any, any)).thenAnswer( + (realInvocation) => realInvocation.positionalArguments[1] as TextStyle, + ); + Utils.changeInstance(mockUtils); + + final mockContext = MockBuildContext(); + + radarChartPainter.drawTitles(mockContext, mockCanvasWrapper, holder); + + verifyNever(mockCanvasWrapper.drawText(any, any)); + }); + + test('test 2', () { + const viewSize = Size(400, 300); + + final data = RadarChartData( + dataSets: [ + RadarDataSet( + dataEntries: [ + const RadarEntry(value: 1), + const RadarEntry(value: 2), + const RadarEntry(value: 3), + ], + ), + RadarDataSet( + dataEntries: [ + const RadarEntry(value: 3), + const RadarEntry(value: 1), + const RadarEntry(value: 2), + ], + ), + RadarDataSet( + dataEntries: [ + const RadarEntry(value: 2), + const RadarEntry(value: 3), + const RadarEntry(value: 1), + ], + ), + ], + getTitle: (index, angle) { + return RadarChartTitle(text: '$index$index', angle: angle); + }, + titleTextStyle: MockData.textStyle4, + radarBorderData: const BorderSide(color: MockData.color6, width: 33), + tickBorderData: const BorderSide(color: MockData.color5, width: 55), + gridBorderData: const BorderSide(color: MockData.color3, width: 3), + radarBackgroundColor: MockData.color2, + ); + + final radarChartPainter = RadarChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final mockUtils = MockUtils(); + when(mockUtils.getThemeAwareTextStyle(any, any)).thenAnswer( + (realInvocation) => realInvocation.positionalArguments[1] as TextStyle, + ); + when(mockUtils.degrees(captureAny)).thenAnswer((inv) { + return utilsMainInstance + .degrees(inv.positionalArguments.first as double); + }); + Utils.changeInstance(mockUtils); + + final mockContext = MockBuildContext(); + + final results = >[]; + when( + mockCanvasWrapper.drawRotated( + size: anyNamed('size'), + rotationOffset: anyNamed('rotationOffset'), + drawOffset: anyNamed('drawOffset'), + angle: anyNamed('angle'), + drawCallback: captureAnyNamed('drawCallback'), + ), + ).thenAnswer((inv) { + (inv.namedArguments[const Symbol('drawCallback')] as void Function())(); + }); + when(mockCanvasWrapper.drawText(captureAny, captureAny, captureAny)) + .thenAnswer((inv) { + results.add({ + 'tp_text': + ((inv.positionalArguments[0] as TextPainter).text as TextSpan?)! + .text, + 'tp_style': + ((inv.positionalArguments[0] as TextPainter).text as TextSpan?)! + .style, + }); + }); + + radarChartPainter.drawTitles(mockContext, mockCanvasWrapper, holder); + expect(results.length, 3); + + expect(results[0]['tp_text'] as String, '00'); + expect(results[0]['tp_style'] as TextStyle, MockData.textStyle4); + + expect(results[1]['tp_text'] as String, '11'); + expect(results[1]['tp_style'] as TextStyle, MockData.textStyle4); + + expect(results[2]['tp_text'] as String, '22'); + expect(results[2]['tp_style'] as TextStyle, MockData.textStyle4); + }); + }); + + group('drawDataSets()', () { + test('test 1', () { + const viewSize = Size(400, 300); + + final data = RadarChartData( + dataSets: [ + RadarDataSet( + dataEntries: [ + const RadarEntry(value: 1), + const RadarEntry(value: 2), + const RadarEntry(value: 3), + ], + fillColor: MockData.color1, + borderColor: MockData.color3, + borderWidth: 3, + ), + RadarDataSet( + dataEntries: [ + const RadarEntry(value: 3), + const RadarEntry(value: 1), + const RadarEntry(value: 2), + ], + fillColor: MockData.color2, + borderColor: MockData.color2, + borderWidth: 2, + ), + RadarDataSet( + dataEntries: [ + const RadarEntry(value: 2), + const RadarEntry(value: 3), + const RadarEntry(value: 1), + ], + fillColor: MockData.color3, + borderColor: MockData.color1, + borderWidth: 1, + ), + RadarDataSet( + dataEntries: [ + const RadarEntry(value: 2), + const RadarEntry(value: 3), + const RadarEntry(value: 1), + ], + fillColor: MockData.color1, + fillGradient: MockData.gradient1, + borderColor: MockData.color1, + borderWidth: 1, + ), + ], + getTitle: (index, angle) { + return RadarChartTitle(text: '$index$index', angle: angle); + }, + titleTextStyle: MockData.textStyle4, + radarBorderData: const BorderSide(color: MockData.color6, width: 33), + tickBorderData: const BorderSide(color: MockData.color5, width: 55), + gridBorderData: const BorderSide(color: MockData.color3, width: 3), + radarBackgroundColor: MockData.color2, + ); + + final radarChartPainter = RadarChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final mockUtils = MockUtils(); + when(mockUtils.getThemeAwareTextStyle(any, any)).thenAnswer( + (realInvocation) => realInvocation.positionalArguments[1] as TextStyle, + ); + Utils.changeInstance(mockUtils); + + final drawCircleResults = >[]; + when(mockCanvasWrapper.drawCircle(captureAny, captureAny, captureAny)) + .thenAnswer((inv) { + drawCircleResults.add({ + 'offset': inv.positionalArguments[0] as Offset, + 'radius': inv.positionalArguments[1] as double, + 'paint': inv.positionalArguments[2] as Paint, + }); + }); + + final drawPathResults = >[]; + when(mockCanvasWrapper.drawPath(captureAny, captureAny)) + .thenAnswer((inv) { + drawPathResults.add({ + 'path': inv.positionalArguments[0] as Path, + 'paint_color': (inv.positionalArguments[1] as Paint).color, + 'paint_shader': (inv.positionalArguments[1] as Paint).shader, + 'paint_stroke': (inv.positionalArguments[1] as Paint).strokeWidth, + 'paint_style': (inv.positionalArguments[1] as Paint).style, + }); + }); + + radarChartPainter.drawDataSets(mockCanvasWrapper, holder); + expect(drawCircleResults.length, 12); + + expect( + drawCircleResults[0]['offset'] as Offset, + const Offset(200, 90), + ); + expect(drawCircleResults[0]['radius'] as double, 5); + + expect( + drawCircleResults[1]['offset'] as Offset, + const Offset(277.9422863405995, 195), + ); + expect(drawCircleResults[1]['radius'] as double, 5); + + expect( + drawCircleResults[2]['offset'] as Offset, + const Offset(96.07695154586739, 210.00000000000006), + ); + expect(drawCircleResults[2]['radius'] as double, 5); + + expect( + drawCircleResults[3]['offset'] as Offset, + const Offset(200, 30), + ); + expect(drawCircleResults[3]['radius'] as double, 5); + + expect( + drawCircleResults[4]['offset'] as Offset, + const Offset(251.96152422706632, 180), + ); + expect(drawCircleResults[4]['radius'] as double, 5); + + expect( + drawCircleResults[5]['offset'] as Offset, + const Offset(122.05771365940053, 195.00000000000003), + ); + expect(drawCircleResults[5]['radius'] as double, 5); + + expect( + drawCircleResults[6]['offset'] as Offset, + const Offset(200, 60), + ); + expect(drawCircleResults[6]['radius'] as double, 5); + + expect( + drawCircleResults[7]['offset'] as Offset, + const Offset(303.92304845413264, 209.99999999999997), + ); + expect(drawCircleResults[7]['radius'] as double, 5); + + expect( + drawCircleResults[8]['offset'] as Offset, + const Offset(148.03847577293368, 180.00000000000003), + ); + expect(drawCircleResults[8]['radius'] as double, 5); + + expect(drawPathResults.length, 8); + + expect( + drawPathResults[0]['paint_color'], + isSameColorAs(MockData.color1), + ); + expect(drawPathResults[0]['paint_style'], PaintingStyle.fill); + + expect( + drawPathResults[1]['paint_color'], + isSameColorAs(MockData.color3), + ); + expect(drawPathResults[1]['paint_stroke'], 3); + expect(drawPathResults[1]['paint_style'], PaintingStyle.stroke); + + expect( + drawPathResults[2]['paint_color'], + isSameColorAs(MockData.color2), + ); + expect(drawPathResults[2]['paint_style'], PaintingStyle.fill); + + expect( + drawPathResults[3]['paint_color'], + isSameColorAs(MockData.color2), + ); + expect(drawPathResults[3]['paint_stroke'], 2); + expect(drawPathResults[3]['paint_style'], PaintingStyle.stroke); + + expect( + drawPathResults[4]['paint_color'], + isSameColorAs(MockData.color3), + ); + expect(drawPathResults[4]['paint_style'], PaintingStyle.fill); + + expect( + drawPathResults[5]['paint_color'], + isSameColorAs(MockData.color1), + ); + expect(drawPathResults[5]['paint_stroke'], 1); + expect(drawPathResults[5]['paint_style'], PaintingStyle.stroke); + + expect(drawPathResults[6]['paint_shader'], isNotNull); + expect(drawPathResults[6]['paint_shader'], isA()); + expect(drawPathResults[6]['paint_style'], PaintingStyle.fill); + + expect(drawPathResults[7]['paint_color'], isSameColorAs(MockData.color1)); + expect(drawPathResults[7]['paint_stroke'], 1); + expect(drawPathResults[7]['paint_style'], PaintingStyle.stroke); + }); + }); + + group('drawTitles()', () { + test('rotated titles', () { + const viewSize = Size(400, 300); + + final data = RadarChartData( + dataSets: [ + RadarDataSet( + dataEntries: [ + const RadarEntry(value: 1), + const RadarEntry(value: 2), + const RadarEntry(value: 3), + ], + ), + RadarDataSet( + dataEntries: [ + const RadarEntry(value: 3), + const RadarEntry(value: 1), + const RadarEntry(value: 2), + ], + ), + RadarDataSet( + dataEntries: [ + const RadarEntry(value: 2), + const RadarEntry(value: 3), + const RadarEntry(value: 1), + ], + ), + ], + radarBorderData: const BorderSide(color: MockData.color6, width: 33), + tickBorderData: const BorderSide(color: MockData.color5, width: 55), + radarBackgroundColor: MockData.color2, + getTitle: (index, angle) { + return RadarChartTitle(text: '$index-$angle', angle: angle); + }, + ); + + final radarChartPainter = RadarChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final mockUtils = MockUtils(); + when(mockUtils.getThemeAwareTextStyle(any, any)) + .thenReturn(MockData.textStyle1); + when(mockUtils.degrees(captureAny)).thenAnswer((inv) { + return utilsMainInstance + .degrees(inv.positionalArguments.first as double); + }); + Utils.changeInstance(mockUtils); + + final mockContext = MockBuildContext(); + + final drawRotatedResults = >[]; + final drawTextResults = >[]; + when( + mockCanvasWrapper.drawRotated( + size: anyNamed('size'), + rotationOffset: anyNamed('rotationOffset'), + drawOffset: anyNamed('drawOffset'), + angle: captureAnyNamed('angle'), + drawCallback: captureAnyNamed('drawCallback'), + ), + ).thenAnswer((inv) { + drawRotatedResults.add({ + 'angle': inv.namedArguments[const Symbol('angle')], + }); + (inv.namedArguments[const Symbol('drawCallback')] as void Function())(); + }); + when(mockCanvasWrapper.drawText(captureAny, captureAny, captureAny)) + .thenAnswer((inv) { + drawTextResults.add({ + 'text': + (inv.positionalArguments[0] as TextPainter).text?.toPlainText(), + 'angle': inv.positionalArguments[2] as double, + }); + }); + + radarChartPainter.drawTitles(mockContext, mockCanvasWrapper, holder); + + expect(drawRotatedResults.length, 3); + expect(drawTextResults.length, 3); + + // Titles + const angle = 360.0 / 3; + for (var i = 0; i < drawTextResults.length; i++) { + expect(drawRotatedResults[i]['angle'], closeTo(angle * i, 0.001)); + expect(drawTextResults[i]['text'], startsWith('$i')); + expect(drawTextResults[i]['angle'], 0); + } + }); + test('horizontal titles by default', () { + const viewSize = Size(400, 300); + + final data = RadarChartData( + dataSets: [ + RadarDataSet( + dataEntries: [ + const RadarEntry(value: 1), + const RadarEntry(value: 2), + const RadarEntry(value: 3), + ], + ), + RadarDataSet( + dataEntries: [ + const RadarEntry(value: 3), + const RadarEntry(value: 1), + const RadarEntry(value: 2), + ], + ), + RadarDataSet( + dataEntries: [ + const RadarEntry(value: 2), + const RadarEntry(value: 3), + const RadarEntry(value: 1), + ], + ), + ], + radarBorderData: const BorderSide(color: MockData.color6, width: 33), + tickBorderData: const BorderSide(color: MockData.color5, width: 55), + radarBackgroundColor: MockData.color2, + getTitle: (index, angle) { + return RadarChartTitle(text: '$index-$angle'); + }, + ); + + final radarChartPainter = RadarChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final mockUtils = MockUtils(); + when(mockUtils.getThemeAwareTextStyle(any, any)) + .thenReturn(MockData.textStyle1); + when(mockUtils.degrees(captureAny)).thenAnswer((inv) { + return utilsMainInstance + .degrees(inv.positionalArguments.first as double); + }); + Utils.changeInstance(mockUtils); + + final mockContext = MockBuildContext(); + + final drawRotatedResults = >[]; + final drawTextResults = >[]; + when( + mockCanvasWrapper.drawRotated( + size: anyNamed('size'), + rotationOffset: anyNamed('rotationOffset'), + drawOffset: anyNamed('drawOffset'), + angle: captureAnyNamed('angle'), + drawCallback: captureAnyNamed('drawCallback'), + ), + ).thenAnswer((inv) { + drawRotatedResults.add({ + 'angle': inv.namedArguments[const Symbol('angle')], + }); + (inv.namedArguments[const Symbol('drawCallback')] as void Function())(); + }); + when(mockCanvasWrapper.drawText(captureAny, captureAny, captureAny)) + .thenAnswer((inv) { + drawTextResults.add({ + 'text': + (inv.positionalArguments[0] as TextPainter).text?.toPlainText(), + 'angle': inv.positionalArguments[2] as double, + }); + }); + + radarChartPainter.drawTitles(mockContext, mockCanvasWrapper, holder); + + expect(drawRotatedResults.length, 3); + expect(drawTextResults.length, 3); + + // Titles + const angle = 360.0 / 3; + for (var i = 0; i < drawTextResults.length; i++) { + expect(drawRotatedResults[i]['angle'], closeTo(angle * i, 0.001)); + expect(drawTextResults[i]['text'], startsWith('$i')); + expect(drawTextResults[i]['angle'], closeTo(-angle * i, 0.001)); + } + }); + }); + + group('handleTouch()', () { + test('test 1', () { + const viewSize = Size(400, 300); + final data = RadarChartData( + dataSets: [ + RadarDataSet( + dataEntries: [ + const RadarEntry(value: 1), + const RadarEntry(value: 2), + const RadarEntry(value: 3), + ], + ), + RadarDataSet( + dataEntries: [ + const RadarEntry(value: 3), + const RadarEntry(value: 1), + const RadarEntry(value: 2), + ], + ), + RadarDataSet( + dataEntries: [ + const RadarEntry(value: 2), + const RadarEntry(value: 3), + const RadarEntry(value: 1), + ], + ), + ], + ); + + final radarChartPainter = RadarChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final mockUtils = MockUtils(); + when(mockUtils.getThemeAwareTextStyle(any, any)).thenAnswer( + (realInvocation) => realInvocation.positionalArguments[1] as TextStyle, + ); + Utils.changeInstance(mockUtils); + + final drawCircleResults = >[]; + when(mockCanvasWrapper.drawCircle(captureAny, captureAny, captureAny)) + .thenAnswer((inv) { + drawCircleResults.add({ + 'offset': inv.positionalArguments[0] as Offset, + 'radius': inv.positionalArguments[1] as double, + 'paint': inv.positionalArguments[2] as Paint, + }); + }); + + final drawPathResults = >[]; + when(mockCanvasWrapper.drawPath(captureAny, captureAny)) + .thenAnswer((inv) { + drawPathResults.add({ + 'path': inv.positionalArguments[0] as Path, + 'paint_color': (inv.positionalArguments[1] as Paint).color, + 'paint_stroke': (inv.positionalArguments[1] as Paint).strokeWidth, + 'paint_style': (inv.positionalArguments[1] as Paint).style, + }); + }); + + expect( + radarChartPainter.handleTouch( + const Offset(287.8, 120.3), + viewSize, + holder, + ), + null, + ); + expect( + radarChartPainter.handleTouch( + const Offset(145.1, 125.4), + viewSize, + holder, + ), + null, + ); + expect( + radarChartPainter.handleTouch( + const Offset(175.9, 120.8), + viewSize, + holder, + ), + null, + ); + expect( + radarChartPainter.handleTouch( + const Offset(201.8, 153.7), + viewSize, + holder, + ), + null, + ); + expect( + radarChartPainter.handleTouch( + const Offset(259.5, 116.3), + viewSize, + holder, + ), + null, + ); + expect( + radarChartPainter.handleTouch( + const Offset(266.9, 179.3), + viewSize, + holder, + ), + null, + ); + expect( + radarChartPainter.handleTouch( + const Offset(145, 193.7), + viewSize, + holder, + ), + null, + ); + + final result0 = radarChartPainter.handleTouch( + const Offset(304.9, 212.9), + viewSize, + holder, + ); + expect(result0!.touchedDataSetIndex, 2); + expect(result0.touchedRadarEntryIndex, 1); + + final result1 = radarChartPainter.handleTouch( + const Offset(200, 60), + viewSize, + holder, + ); + expect(result1!.touchedDataSetIndex, 2); + expect(result1.touchedRadarEntryIndex, 0); + + final result2 = radarChartPainter.handleTouch( + const Offset(148, 180), + viewSize, + holder, + ); + expect(result2!.touchedDataSetIndex, 2); + expect(result2.touchedRadarEntryIndex, 2); + + final result3 = radarChartPainter.handleTouch( + const Offset(270.5, 192.3), + viewSize, + holder, + ); + expect(result3!.touchedDataSetIndex, 0); + expect(result3.touchedRadarEntryIndex, 1); + + final result4 = radarChartPainter.handleTouch( + const Offset(98.3, 216.8), + viewSize, + holder, + ); + expect(result4!.touchedDataSetIndex, 0); + expect(result4.touchedRadarEntryIndex, 2); + + final result5 = radarChartPainter.handleTouch( + const Offset(200, 90), + viewSize, + holder, + ); + expect(result5!.touchedDataSetIndex, 0); + expect(result5.touchedRadarEntryIndex, 0); + + final result6 = radarChartPainter.handleTouch( + const Offset(202.6, 33.5), + viewSize, + holder, + ); + expect(result6!.touchedDataSetIndex, 1); + expect(result6.touchedRadarEntryIndex, 0); + + final result7 = radarChartPainter.handleTouch( + const Offset(122.1, 195), + viewSize, + holder, + ); + expect(result7!.touchedDataSetIndex, 1); + expect(result7.touchedRadarEntryIndex, 2); + + final result8 = radarChartPainter.handleTouch( + const Offset(252, 180), + viewSize, + holder, + ); + expect(result8!.touchedDataSetIndex, 1); + expect(result8.touchedRadarEntryIndex, 1); + }); + }); + + group('radarCenterY()', () { + test('test 1', () { + final painter = RadarChartPainter(); + expect(painter.radarCenterY(const Size(200, 400)), 200); + expect(painter.radarCenterY(const Size(2314, 400)), 200); + }); + }); + + group('radarCenterX()', () { + test('test 1', () { + final painter = RadarChartPainter(); + expect(painter.radarCenterX(const Size(400, 200)), 200); + expect(painter.radarCenterX(const Size(400, 2314)), 200); + }); + }); + + group('radarRadius()', () { + test('test 1', () { + final painter = RadarChartPainter(); + expect(painter.radarRadius(const Size(400, 200)), 80); + expect(painter.radarRadius(const Size(400, 2314)), 160); + }); + }); + + group('calculateDataSetsPosition()', () { + test('test 1', () { + const viewSize = Size(400, 300); + + final data = RadarChartData( + dataSets: [ + RadarDataSet( + dataEntries: [ + const RadarEntry(value: 1), + const RadarEntry(value: 2), + const RadarEntry(value: 3), + ], + ), + RadarDataSet( + dataEntries: [ + const RadarEntry(value: 3), + const RadarEntry(value: 1), + const RadarEntry(value: 2), + ], + ), + RadarDataSet( + dataEntries: [ + const RadarEntry(value: 2), + const RadarEntry(value: 3), + const RadarEntry(value: 1), + ], + ), + ], + titleTextStyle: MockData.textStyle4, + radarBorderData: const BorderSide(color: MockData.color6, width: 33), + tickBorderData: const BorderSide(color: MockData.color5, width: 55), + gridBorderData: const BorderSide(color: MockData.color3, width: 3), + radarBackgroundColor: MockData.color2, + ); + + final radarChartPainter = RadarChartPainter(); + final holder = + PaintHolder(data, data, TextScaler.noScaling); + + final result = + radarChartPainter.calculateDataSetsPosition(viewSize, holder); + expect(result.length, 3); + expect( + result[0].entriesOffset, + [ + const Offset(200, 90), + const Offset(277.9422863405995, 195), + const Offset(96.07695154586739, 210.00000000000006), + ], + ); + expect( + result[1].entriesOffset, + [ + const Offset(200, 30), + const Offset(251.96152422706632, 180), + const Offset(122.05771365940053, 195.00000000000003), + ], + ); + expect( + result[2].entriesOffset, + [ + const Offset(200, 60), + const Offset(303.92304845413264, 209.99999999999997), + const Offset(148.03847577293368, 180.00000000000003), + ], + ); + }); + }); + + group('getDefaultChartCenterValue()', () { + final radarChartPainter = RadarChartPainter(); + + test('test 1', () { + expect(radarChartPainter.getDefaultChartCenterValue(), 0); + }); + }); + + group('getChartCenterValue()', () { + final radarChartPainter = RadarChartPainter(); + final dataSet = RadarDataSet( + dataEntries: [ + const RadarEntry(value: 15), + const RadarEntry(value: 20), + const RadarEntry(value: 20), + ], + ); + final dataSetWithSameMaxAndMin = RadarDataSet( + dataEntries: [ + const RadarEntry(value: 10), + const RadarEntry(value: 10), + const RadarEntry(value: 10), + ], + ); + final dataWith1Tick = RadarChartData( + dataSets: [dataSet], + tickCount: 1, + ); + final dataWith2Ticks = RadarChartData( + dataSets: [dataSet], + tickCount: 2, + ); + final dataWith3Ticks = RadarChartData( + dataSets: [dataSet], + tickCount: 3, + ); + final dataWithSameMaxAndMin = RadarChartData( + dataSets: [dataSetWithSameMaxAndMin], + tickCount: 2, + ); + + test('test 1', () { + expect(radarChartPainter.getChartCenterValue(dataWith1Tick), 10); + expect(radarChartPainter.getChartCenterValue(dataWith2Ticks), 12.5); + expect( + radarChartPainter.getChartCenterValue(dataWith3Ticks), + 13.333333333333334, + ); + }); + + test('test 2', () { + expect(radarChartPainter.getChartCenterValue(dataWithSameMaxAndMin), 0); + }); + }); + + group('getScaledPoint()', () { + final radarChartPainter = RadarChartPainter(); + final data = RadarChartData( + dataSets: [ + RadarDataSet( + dataEntries: [ + const RadarEntry(value: 15), + const RadarEntry(value: 20), + const RadarEntry(value: 20), + ], + ), + ], + tickCount: 2, + ); + final dataWithSameMaxAndMin = RadarChartData( + dataSets: [ + RadarDataSet( + dataEntries: [ + const RadarEntry(value: 10), + const RadarEntry(value: 10), + const RadarEntry(value: 10), + ], + ), + ], + tickCount: 2, + ); + const radius = 200.0; + const point1 = RadarEntry(value: 0); + const point2 = RadarEntry(value: 50); + const point3 = RadarEntry(value: 150); + + test('test 1', () { + expect( + radarChartPainter.getScaledPoint(point1, radius, data), + -333.3333333333333, + ); + expect(radarChartPainter.getScaledPoint(point2, radius, data), 1000.0); + expect( + radarChartPainter.getScaledPoint(point3, radius, data), + 3666.6666666666665, + ); + }); + + test('test 2', () { + expect( + radarChartPainter.getScaledPoint( + point1, + radius, + dataWithSameMaxAndMin, + ), + 0.0, + ); + expect( + radarChartPainter.getScaledPoint( + point2, + radius, + dataWithSameMaxAndMin, + ), + 1000.0, + ); + expect( + radarChartPainter.getScaledPoint( + point3, + radius, + dataWithSameMaxAndMin, + ), + 3000.0, + ); + }); + }); + + group('getFirstTickValue()', () { + final radarChartPainter = RadarChartPainter(); + final data = RadarChartData( + dataSets: [ + RadarDataSet( + dataEntries: [ + const RadarEntry(value: 15), + const RadarEntry(value: 20), + const RadarEntry(value: 20), + ], + ), + ], + tickCount: 2, + ); + final dataWithSameMaxAndMin = RadarChartData( + dataSets: [ + RadarDataSet( + dataEntries: [ + const RadarEntry(value: 10), + const RadarEntry(value: 10), + const RadarEntry(value: 10), + ], + ), + ], + tickCount: 2, + ); + + test('test 1', () { + expect(radarChartPainter.getFirstTickValue(data), 15); + }); + + test('test 2', () { + expect( + radarChartPainter.getFirstTickValue(dataWithSameMaxAndMin), + 3.3333333333333335, + ); + }); + }); + + group('getSpaceBetweenTicks()', () { + final radarChartPainter = RadarChartPainter(); + final data = RadarChartData( + dataSets: [ + RadarDataSet( + dataEntries: [ + const RadarEntry(value: 15), + const RadarEntry(value: 20), + const RadarEntry(value: 20), + ], + ), + ], + tickCount: 2, + ); + final dataWithSameMaxAndMin = RadarChartData( + dataSets: [ + RadarDataSet( + dataEntries: [ + const RadarEntry(value: 10), + const RadarEntry(value: 10), + const RadarEntry(value: 10), + ], + ), + ], + tickCount: 2, + ); + + test('test 1', () { + expect(radarChartPainter.getSpaceBetweenTicks(data), 2.5); + }); + + test('test 2', () { + expect( + radarChartPainter.getSpaceBetweenTicks(dataWithSameMaxAndMin), + 3.3333333333333335, + ); + }); + }); +} diff --git a/test/chart/radar_chart/radar_chart_painter_test.mocks.dart b/test/chart/radar_chart/radar_chart_painter_test.mocks.dart new file mode 100644 index 0000000..06dd2ff --- /dev/null +++ b/test/chart/radar_chart/radar_chart_painter_test.mocks.dart @@ -0,0 +1,924 @@ +// Mocks generated by Mockito 5.4.5 from annotations +// in fl_chart/test/chart/radar_chart/radar_chart_painter_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:typed_data' as _i5; +import 'dart:ui' as _i2; + +import 'package:fl_chart/fl_chart.dart' as _i7; +import 'package:fl_chart/src/utils/canvas_wrapper.dart' as _i6; +import 'package:fl_chart/src/utils/utils.dart' as _i8; +import 'package:flutter/cupertino.dart' as _i3; +import 'package:flutter/foundation.dart' as _i4; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i9; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeRect_0 extends _i1.SmartFake implements _i2.Rect { + _FakeRect_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeCanvas_1 extends _i1.SmartFake implements _i2.Canvas { + _FakeCanvas_1(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeSize_2 extends _i1.SmartFake implements _i2.Size { + _FakeSize_2(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeWidget_3 extends _i1.SmartFake implements _i3.Widget { + _FakeWidget_3(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeInheritedWidget_4 extends _i1.SmartFake + implements _i3.InheritedWidget { + _FakeInheritedWidget_4(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeDiagnosticsNode_5 extends _i1.SmartFake + implements _i3.DiagnosticsNode { + _FakeDiagnosticsNode_5(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({ + _i4.TextTreeConfiguration? parentConfiguration, + _i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info, + }) => super.toString(); +} + +class _FakeOffset_6 extends _i1.SmartFake implements _i2.Offset { + _FakeOffset_6(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeBorderSide_7 extends _i1.SmartFake implements _i3.BorderSide { + _FakeBorderSide_7(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeTextStyle_8 extends _i1.SmartFake implements _i3.TextStyle { + _FakeTextStyle_8(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +/// A class which mocks [Canvas]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCanvas extends _i1.Mock implements _i2.Canvas { + MockCanvas() { + _i1.throwOnMissingStub(this); + } + + @override + void save() => super.noSuchMethod( + Invocation.method(#save, []), + returnValueForMissingStub: null, + ); + + @override + void saveLayer(_i2.Rect? bounds, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#saveLayer, [bounds, paint]), + returnValueForMissingStub: null, + ); + + @override + void restore() => super.noSuchMethod( + Invocation.method(#restore, []), + returnValueForMissingStub: null, + ); + + @override + void restoreToCount(int? count) => super.noSuchMethod( + Invocation.method(#restoreToCount, [count]), + returnValueForMissingStub: null, + ); + + @override + int getSaveCount() => + (super.noSuchMethod(Invocation.method(#getSaveCount, []), returnValue: 0) + as int); + + @override + void translate(double? dx, double? dy) => super.noSuchMethod( + Invocation.method(#translate, [dx, dy]), + returnValueForMissingStub: null, + ); + + @override + void scale(double? sx, [double? sy]) => super.noSuchMethod( + Invocation.method(#scale, [sx, sy]), + returnValueForMissingStub: null, + ); + + @override + void rotate(double? radians) => super.noSuchMethod( + Invocation.method(#rotate, [radians]), + returnValueForMissingStub: null, + ); + + @override + void skew(double? sx, double? sy) => super.noSuchMethod( + Invocation.method(#skew, [sx, sy]), + returnValueForMissingStub: null, + ); + + @override + void transform(_i5.Float64List? matrix4) => super.noSuchMethod( + Invocation.method(#transform, [matrix4]), + returnValueForMissingStub: null, + ); + + @override + _i5.Float64List getTransform() => + (super.noSuchMethod( + Invocation.method(#getTransform, []), + returnValue: _i5.Float64List(0), + ) + as _i5.Float64List); + + @override + void clipRect( + _i2.Rect? rect, { + _i2.ClipOp? clipOp = _i2.ClipOp.intersect, + bool? doAntiAlias = true, + }) => super.noSuchMethod( + Invocation.method( + #clipRect, + [rect], + {#clipOp: clipOp, #doAntiAlias: doAntiAlias}, + ), + returnValueForMissingStub: null, + ); + + @override + void clipRRect(_i2.RRect? rrect, {bool? doAntiAlias = true}) => + super.noSuchMethod( + Invocation.method(#clipRRect, [rrect], {#doAntiAlias: doAntiAlias}), + returnValueForMissingStub: null, + ); + + @override + void clipPath(_i2.Path? path, {bool? doAntiAlias = true}) => + super.noSuchMethod( + Invocation.method(#clipPath, [path], {#doAntiAlias: doAntiAlias}), + returnValueForMissingStub: null, + ); + + @override + _i2.Rect getLocalClipBounds() => + (super.noSuchMethod( + Invocation.method(#getLocalClipBounds, []), + returnValue: _FakeRect_0( + this, + Invocation.method(#getLocalClipBounds, []), + ), + ) + as _i2.Rect); + + @override + _i2.Rect getDestinationClipBounds() => + (super.noSuchMethod( + Invocation.method(#getDestinationClipBounds, []), + returnValue: _FakeRect_0( + this, + Invocation.method(#getDestinationClipBounds, []), + ), + ) + as _i2.Rect); + + @override + void drawColor(_i2.Color? color, _i2.BlendMode? blendMode) => + super.noSuchMethod( + Invocation.method(#drawColor, [color, blendMode]), + returnValueForMissingStub: null, + ); + + @override + void drawLine(_i2.Offset? p1, _i2.Offset? p2, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawLine, [p1, p2, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawPaint(_i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawPaint, [paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRect(_i2.Rect? rect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRect, [rect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRRect(_i2.RRect? rrect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRRect, [rrect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawDRRect(_i2.RRect? outer, _i2.RRect? inner, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawDRRect, [outer, inner, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawOval(_i2.Rect? rect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawOval, [rect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawCircle(_i2.Offset? c, double? radius, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawCircle, [c, radius, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawArc( + _i2.Rect? rect, + double? startAngle, + double? sweepAngle, + bool? useCenter, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawArc, [ + rect, + startAngle, + sweepAngle, + useCenter, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawPath(_i2.Path? path, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawPath, [path, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImage(_i2.Image? image, _i2.Offset? offset, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawImage, [image, offset, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImageRect( + _i2.Image? image, + _i2.Rect? src, + _i2.Rect? dst, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawImageRect, [image, src, dst, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImageNine( + _i2.Image? image, + _i2.Rect? center, + _i2.Rect? dst, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawImageNine, [image, center, dst, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawPicture(_i2.Picture? picture) => super.noSuchMethod( + Invocation.method(#drawPicture, [picture]), + returnValueForMissingStub: null, + ); + + @override + void drawParagraph(_i2.Paragraph? paragraph, _i2.Offset? offset) => + super.noSuchMethod( + Invocation.method(#drawParagraph, [paragraph, offset]), + returnValueForMissingStub: null, + ); + + @override + void drawPoints( + _i2.PointMode? pointMode, + List<_i2.Offset>? points, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawPoints, [pointMode, points, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRawPoints( + _i2.PointMode? pointMode, + _i5.Float32List? points, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawRawPoints, [pointMode, points, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawVertices( + _i2.Vertices? vertices, + _i2.BlendMode? blendMode, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawVertices, [vertices, blendMode, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawAtlas( + _i2.Image? atlas, + List<_i2.RSTransform>? transforms, + List<_i2.Rect>? rects, + List<_i2.Color>? colors, + _i2.BlendMode? blendMode, + _i2.Rect? cullRect, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawAtlas, [ + atlas, + transforms, + rects, + colors, + blendMode, + cullRect, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawRawAtlas( + _i2.Image? atlas, + _i5.Float32List? rstTransforms, + _i5.Float32List? rects, + _i5.Int32List? colors, + _i2.BlendMode? blendMode, + _i2.Rect? cullRect, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawRawAtlas, [ + atlas, + rstTransforms, + rects, + colors, + blendMode, + cullRect, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawShadow( + _i2.Path? path, + _i2.Color? color, + double? elevation, + bool? transparentOccluder, + ) => super.noSuchMethod( + Invocation.method(#drawShadow, [ + path, + color, + elevation, + transparentOccluder, + ]), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [CanvasWrapper]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCanvasWrapper extends _i1.Mock implements _i6.CanvasWrapper { + MockCanvasWrapper() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.Canvas get canvas => + (super.noSuchMethod( + Invocation.getter(#canvas), + returnValue: _FakeCanvas_1(this, Invocation.getter(#canvas)), + ) + as _i2.Canvas); + + @override + _i2.Size get size => + (super.noSuchMethod( + Invocation.getter(#size), + returnValue: _FakeSize_2(this, Invocation.getter(#size)), + ) + as _i2.Size); + + @override + void drawRRect(_i2.RRect? rrect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRRect, [rrect, paint]), + returnValueForMissingStub: null, + ); + + @override + void save() => super.noSuchMethod( + Invocation.method(#save, []), + returnValueForMissingStub: null, + ); + + @override + void restore() => super.noSuchMethod( + Invocation.method(#restore, []), + returnValueForMissingStub: null, + ); + + @override + void clipRect( + _i2.Rect? rect, { + _i2.ClipOp? clipOp = _i2.ClipOp.intersect, + bool? doAntiAlias = true, + }) => super.noSuchMethod( + Invocation.method( + #clipRect, + [rect], + {#clipOp: clipOp, #doAntiAlias: doAntiAlias}, + ), + returnValueForMissingStub: null, + ); + + @override + void translate(double? dx, double? dy) => super.noSuchMethod( + Invocation.method(#translate, [dx, dy]), + returnValueForMissingStub: null, + ); + + @override + void rotate(double? radius) => super.noSuchMethod( + Invocation.method(#rotate, [radius]), + returnValueForMissingStub: null, + ); + + @override + void drawPath(_i2.Path? path, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawPath, [path, paint]), + returnValueForMissingStub: null, + ); + + @override + void saveLayer(_i2.Rect? bounds, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#saveLayer, [bounds, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawPicture(_i2.Picture? picture) => super.noSuchMethod( + Invocation.method(#drawPicture, [picture]), + returnValueForMissingStub: null, + ); + + @override + void drawImage(_i2.Image? image, _i2.Offset? offset, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawImage, [image, offset, paint]), + returnValueForMissingStub: null, + ); + + @override + void clipPath(_i2.Path? path, {bool? doAntiAlias = true}) => + super.noSuchMethod( + Invocation.method(#clipPath, [path], {#doAntiAlias: doAntiAlias}), + returnValueForMissingStub: null, + ); + + @override + void drawRect(_i2.Rect? rect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRect, [rect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawLine(_i2.Offset? p1, _i2.Offset? p2, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawLine, [p1, p2, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawCircle(_i2.Offset? center, double? radius, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawCircle, [center, radius, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawArc( + _i2.Rect? rect, + double? startAngle, + double? sweepAngle, + bool? useCenter, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawArc, [ + rect, + startAngle, + sweepAngle, + useCenter, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawText( + _i3.TextPainter? tp, + _i2.Offset? offset, [ + double? rotateAngle, + ]) => super.noSuchMethod( + Invocation.method(#drawText, [tp, offset, rotateAngle]), + returnValueForMissingStub: null, + ); + + @override + void drawVerticalText(_i3.TextPainter? tp, _i2.Offset? offset) => + super.noSuchMethod( + Invocation.method(#drawVerticalText, [tp, offset]), + returnValueForMissingStub: null, + ); + + @override + void drawDot( + _i7.FlDotPainter? painter, + _i7.FlSpot? spot, + _i2.Offset? offset, + ) => super.noSuchMethod( + Invocation.method(#drawDot, [painter, spot, offset]), + returnValueForMissingStub: null, + ); + + @override + void drawErrorIndicator( + _i7.FlSpotErrorRangePainter? painter, + _i7.FlSpot? origin, + _i2.Offset? offset, + _i2.Rect? errorRelativeRect, + _i7.AxisChartData? axisData, + ) => super.noSuchMethod( + Invocation.method(#drawErrorIndicator, [ + painter, + origin, + offset, + errorRelativeRect, + axisData, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawRotated({ + required _i2.Size? size, + _i2.Offset? rotationOffset = _i2.Offset.zero, + _i2.Offset? drawOffset = _i2.Offset.zero, + required double? angle, + required _i6.DrawCallback? drawCallback, + }) => super.noSuchMethod( + Invocation.method(#drawRotated, [], { + #size: size, + #rotationOffset: rotationOffset, + #drawOffset: drawOffset, + #angle: angle, + #drawCallback: drawCallback, + }), + returnValueForMissingStub: null, + ); + + @override + void drawDashedLine( + _i2.Offset? from, + _i2.Offset? to, + _i2.Paint? painter, + List? dashArray, + ) => super.noSuchMethod( + Invocation.method(#drawDashedLine, [from, to, painter, dashArray]), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [BuildContext]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockBuildContext extends _i1.Mock implements _i3.BuildContext { + MockBuildContext() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Widget get widget => + (super.noSuchMethod( + Invocation.getter(#widget), + returnValue: _FakeWidget_3(this, Invocation.getter(#widget)), + ) + as _i3.Widget); + + @override + bool get mounted => + (super.noSuchMethod(Invocation.getter(#mounted), returnValue: false) + as bool); + + @override + bool get debugDoingBuild => + (super.noSuchMethod( + Invocation.getter(#debugDoingBuild), + returnValue: false, + ) + as bool); + + @override + _i3.InheritedWidget dependOnInheritedElement( + _i3.InheritedElement? ancestor, { + Object? aspect, + }) => + (super.noSuchMethod( + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + returnValue: _FakeInheritedWidget_4( + this, + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + ), + ) + as _i3.InheritedWidget); + + @override + void visitAncestorElements(_i3.ConditionalElementVisitor? visitor) => + super.noSuchMethod( + Invocation.method(#visitAncestorElements, [visitor]), + returnValueForMissingStub: null, + ); + + @override + void visitChildElements(_i3.ElementVisitor? visitor) => super.noSuchMethod( + Invocation.method(#visitChildElements, [visitor]), + returnValueForMissingStub: null, + ); + + @override + void dispatchNotification(_i3.Notification? notification) => + super.noSuchMethod( + Invocation.method(#dispatchNotification, [notification]), + returnValueForMissingStub: null, + ); + + @override + _i3.DiagnosticsNode describeElement( + String? name, { + _i4.DiagnosticsTreeStyle? style = _i4.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method(#describeElement, [name], {#style: style}), + returnValue: _FakeDiagnosticsNode_5( + this, + Invocation.method(#describeElement, [name], {#style: style}), + ), + ) + as _i3.DiagnosticsNode); + + @override + _i3.DiagnosticsNode describeWidget( + String? name, { + _i4.DiagnosticsTreeStyle? style = _i4.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method(#describeWidget, [name], {#style: style}), + returnValue: _FakeDiagnosticsNode_5( + this, + Invocation.method(#describeWidget, [name], {#style: style}), + ), + ) + as _i3.DiagnosticsNode); + + @override + List<_i3.DiagnosticsNode> describeMissingAncestor({ + required Type? expectedAncestorType, + }) => + (super.noSuchMethod( + Invocation.method(#describeMissingAncestor, [], { + #expectedAncestorType: expectedAncestorType, + }), + returnValue: <_i3.DiagnosticsNode>[], + ) + as List<_i3.DiagnosticsNode>); + + @override + _i3.DiagnosticsNode describeOwnershipChain(String? name) => + (super.noSuchMethod( + Invocation.method(#describeOwnershipChain, [name]), + returnValue: _FakeDiagnosticsNode_5( + this, + Invocation.method(#describeOwnershipChain, [name]), + ), + ) + as _i3.DiagnosticsNode); +} + +/// A class which mocks [Utils]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockUtils extends _i1.Mock implements _i8.Utils { + MockUtils() { + _i1.throwOnMissingStub(this); + } + + @override + double radians(double? degrees) => + (super.noSuchMethod( + Invocation.method(#radians, [degrees]), + returnValue: 0.0, + ) + as double); + + @override + double degrees(double? radians) => + (super.noSuchMethod( + Invocation.method(#degrees, [radians]), + returnValue: 0.0, + ) + as double); + + @override + double translateRotatedPosition(double? size, double? degree) => + (super.noSuchMethod( + Invocation.method(#translateRotatedPosition, [size, degree]), + returnValue: 0.0, + ) + as double); + + @override + _i2.Offset calculateRotationOffset(_i2.Size? size, double? degree) => + (super.noSuchMethod( + Invocation.method(#calculateRotationOffset, [size, degree]), + returnValue: _FakeOffset_6( + this, + Invocation.method(#calculateRotationOffset, [size, degree]), + ), + ) + as _i2.Offset); + + @override + _i3.BorderRadius? normalizeBorderRadius( + _i3.BorderRadius? borderRadius, + double? width, + ) => + (super.noSuchMethod( + Invocation.method(#normalizeBorderRadius, [borderRadius, width]), + ) + as _i3.BorderRadius?); + + @override + _i3.BorderSide normalizeBorderSide( + _i3.BorderSide? borderSide, + double? width, + ) => + (super.noSuchMethod( + Invocation.method(#normalizeBorderSide, [borderSide, width]), + returnValue: _FakeBorderSide_7( + this, + Invocation.method(#normalizeBorderSide, [borderSide, width]), + ), + ) + as _i3.BorderSide); + + @override + double getEfficientInterval( + double? axisViewSize, + double? diffInAxis, { + double? pixelPerInterval = 40.0, + }) => + (super.noSuchMethod( + Invocation.method( + #getEfficientInterval, + [axisViewSize, diffInAxis], + {#pixelPerInterval: pixelPerInterval}, + ), + returnValue: 0.0, + ) + as double); + + @override + double roundInterval(double? input) => + (super.noSuchMethod( + Invocation.method(#roundInterval, [input]), + returnValue: 0.0, + ) + as double); + + @override + int getFractionDigits(double? value) => + (super.noSuchMethod( + Invocation.method(#getFractionDigits, [value]), + returnValue: 0, + ) + as int); + + @override + String formatNumber(double? axisMin, double? axisMax, double? axisValue) => + (super.noSuchMethod( + Invocation.method(#formatNumber, [axisMin, axisMax, axisValue]), + returnValue: _i9.dummyValue( + this, + Invocation.method(#formatNumber, [axisMin, axisMax, axisValue]), + ), + ) + as String); + + @override + _i3.TextStyle getThemeAwareTextStyle( + _i3.BuildContext? context, + _i3.TextStyle? providedStyle, + ) => + (super.noSuchMethod( + Invocation.method(#getThemeAwareTextStyle, [ + context, + providedStyle, + ]), + returnValue: _FakeTextStyle_8( + this, + Invocation.method(#getThemeAwareTextStyle, [ + context, + providedStyle, + ]), + ), + ) + as _i3.TextStyle); + + @override + double getBestInitialIntervalValue( + double? min, + double? max, + double? interval, { + double? baseline = 0.0, + }) => + (super.noSuchMethod( + Invocation.method( + #getBestInitialIntervalValue, + [min, max, interval], + {#baseline: baseline}, + ), + returnValue: 0.0, + ) + as double); + + @override + double convertRadiusToSigma(double? radius) => + (super.noSuchMethod( + Invocation.method(#convertRadiusToSigma, [radius]), + returnValue: 0.0, + ) + as double); +} diff --git a/test/chart/radar_chart/radar_chart_renderer_test.dart b/test/chart/radar_chart/radar_chart_renderer_test.dart new file mode 100644 index 0000000..31354ab --- /dev/null +++ b/test/chart/radar_chart/radar_chart_renderer_test.dart @@ -0,0 +1,108 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart'; +import 'package:fl_chart/src/chart/radar_chart/radar_chart_painter.dart'; +import 'package:fl_chart/src/chart/radar_chart/radar_chart_renderer.dart'; +import 'package:fl_chart/src/utils/canvas_wrapper.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import '../data_pool.dart'; +import 'radar_chart_renderer_test.mocks.dart'; + +@GenerateMocks([Canvas, PaintingContext, BuildContext, RadarChartPainter]) +void main() { + group('RadarChartRenderer', () { + final data = RadarChartData( + dataSets: [MockData.radarDataSet1], + tickCount: 1, + ); + + final targetData = RadarChartData( + dataSets: [MockData.radarDataSet2], + tickCount: 1, + ); + + const textScaler = TextScaler.linear(4); + + final mockBuildContext = MockBuildContext(); + final renderRadarChart = RenderRadarChart( + mockBuildContext, + data, + targetData, + textScaler, + ); + + final mockPainter = MockRadarChartPainter(); + final mockPaintingContext = MockPaintingContext(); + final mockCanvas = MockCanvas(); + const mockSize = Size(44, 44); + when(mockPaintingContext.canvas).thenAnswer((realInvocation) => mockCanvas); + renderRadarChart + ..mockTestSize = mockSize + ..painter = mockPainter; + + test('test 1 correct data set', () { + expect(renderRadarChart.data == data, true); + expect(renderRadarChart.data == targetData, false); + expect(renderRadarChart.targetData == targetData, true); + expect(renderRadarChart.textScaler == textScaler, true); + expect(renderRadarChart.paintHolder.data == data, true); + expect(renderRadarChart.paintHolder.targetData == targetData, true); + expect(renderRadarChart.paintHolder.textScaler == textScaler, true); + }); + + test('test 2 check paint function', () { + renderRadarChart.paint(mockPaintingContext, const Offset(10, 10)); + verify(mockCanvas.save()).called(1); + verify(mockCanvas.translate(10, 10)).called(1); + final result = verify(mockPainter.paint(any, captureAny, captureAny)); + expect(result.callCount, 1); + + final canvasWrapper = result.captured[0] as CanvasWrapper; + expect(canvasWrapper.size, const Size(44, 44)); + expect(canvasWrapper.canvas, mockCanvas); + + final paintHolder = result.captured[1] as PaintHolder; + expect(paintHolder.data, data); + expect(paintHolder.targetData, targetData); + expect(paintHolder.textScaler, textScaler); + + verify(mockCanvas.restore()).called(1); + }); + + test('test 3 check getResponseAtLocation function', () { + final results = >[]; + when(mockPainter.handleTouch(captureAny, captureAny, captureAny)) + .thenAnswer((inv) { + results.add({ + 'local_position': inv.positionalArguments[0] as Offset, + 'size': inv.positionalArguments[1] as Size, + 'paint_holder': inv.positionalArguments[2] as PaintHolder, + }); + return MockData.radarTouchedSpot; + }); + final touchResponse = + renderRadarChart.getResponseAtLocation(MockData.offset1); + expect(touchResponse.touchedSpot, MockData.radarTouchedSpot); + expect(results[0]['local_position'] as Offset, MockData.offset1); + expect(results[0]['size'] as Size, mockSize); + final paintHolder = results[0]['paint_holder'] as PaintHolder; + expect(paintHolder.data, data); + expect(paintHolder.targetData, targetData); + expect(paintHolder.textScaler, textScaler); + }); + + test('test 4 check setters', () { + renderRadarChart + ..data = targetData + ..targetData = data + ..textScaler = const TextScaler.linear(22); + + expect(renderRadarChart.data, targetData); + expect(renderRadarChart.targetData, data); + expect(renderRadarChart.textScaler, const TextScaler.linear(22)); + }); + }); +} diff --git a/test/chart/radar_chart/radar_chart_renderer_test.mocks.dart b/test/chart/radar_chart/radar_chart_renderer_test.mocks.dart new file mode 100644 index 0000000..a201f4c --- /dev/null +++ b/test/chart/radar_chart/radar_chart_renderer_test.mocks.dart @@ -0,0 +1,970 @@ +// Mocks generated by Mockito 5.4.5 from annotations +// in fl_chart/test/chart/radar_chart/radar_chart_renderer_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:typed_data' as _i7; +import 'dart:ui' as _i2; + +import 'package:fl_chart/fl_chart.dart' as _i13; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart' + as _i12; +import 'package:fl_chart/src/chart/radar_chart/radar_chart_painter.dart' + as _i10; +import 'package:fl_chart/src/utils/canvas_wrapper.dart' as _i11; +import 'package:flutter/foundation.dart' as _i5; +import 'package:flutter/gestures.dart' as _i8; +import 'package:flutter/material.dart' as _i6; +import 'package:flutter/rendering.dart' as _i3; +import 'package:flutter/src/rendering/layer.dart' as _i4; +import 'package:flutter/src/widgets/notification_listener.dart' as _i9; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeRect_0 extends _i1.SmartFake implements _i2.Rect { + _FakeRect_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeCanvas_1 extends _i1.SmartFake implements _i2.Canvas { + _FakeCanvas_1(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakePaintingContext_2 extends _i1.SmartFake + implements _i3.PaintingContext { + _FakePaintingContext_2(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeColorFilterLayer_3 extends _i1.SmartFake + implements _i4.ColorFilterLayer { + _FakeColorFilterLayer_3(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeOpacityLayer_4 extends _i1.SmartFake implements _i4.OpacityLayer { + _FakeOpacityLayer_4(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeWidget_5 extends _i1.SmartFake implements _i6.Widget { + _FakeWidget_5(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeInheritedWidget_6 extends _i1.SmartFake + implements _i6.InheritedWidget { + _FakeInheritedWidget_6(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeDiagnosticsNode_7 extends _i1.SmartFake + implements _i5.DiagnosticsNode { + _FakeDiagnosticsNode_7(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({ + _i5.TextTreeConfiguration? parentConfiguration, + _i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info, + }) => super.toString(); +} + +/// A class which mocks [Canvas]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCanvas extends _i1.Mock implements _i2.Canvas { + MockCanvas() { + _i1.throwOnMissingStub(this); + } + + @override + void save() => super.noSuchMethod( + Invocation.method(#save, []), + returnValueForMissingStub: null, + ); + + @override + void saveLayer(_i2.Rect? bounds, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#saveLayer, [bounds, paint]), + returnValueForMissingStub: null, + ); + + @override + void restore() => super.noSuchMethod( + Invocation.method(#restore, []), + returnValueForMissingStub: null, + ); + + @override + void restoreToCount(int? count) => super.noSuchMethod( + Invocation.method(#restoreToCount, [count]), + returnValueForMissingStub: null, + ); + + @override + int getSaveCount() => + (super.noSuchMethod(Invocation.method(#getSaveCount, []), returnValue: 0) + as int); + + @override + void translate(double? dx, double? dy) => super.noSuchMethod( + Invocation.method(#translate, [dx, dy]), + returnValueForMissingStub: null, + ); + + @override + void scale(double? sx, [double? sy]) => super.noSuchMethod( + Invocation.method(#scale, [sx, sy]), + returnValueForMissingStub: null, + ); + + @override + void rotate(double? radians) => super.noSuchMethod( + Invocation.method(#rotate, [radians]), + returnValueForMissingStub: null, + ); + + @override + void skew(double? sx, double? sy) => super.noSuchMethod( + Invocation.method(#skew, [sx, sy]), + returnValueForMissingStub: null, + ); + + @override + void transform(_i7.Float64List? matrix4) => super.noSuchMethod( + Invocation.method(#transform, [matrix4]), + returnValueForMissingStub: null, + ); + + @override + _i7.Float64List getTransform() => + (super.noSuchMethod( + Invocation.method(#getTransform, []), + returnValue: _i7.Float64List(0), + ) + as _i7.Float64List); + + @override + void clipRect( + _i2.Rect? rect, { + _i2.ClipOp? clipOp = _i2.ClipOp.intersect, + bool? doAntiAlias = true, + }) => super.noSuchMethod( + Invocation.method( + #clipRect, + [rect], + {#clipOp: clipOp, #doAntiAlias: doAntiAlias}, + ), + returnValueForMissingStub: null, + ); + + @override + void clipRRect(_i2.RRect? rrect, {bool? doAntiAlias = true}) => + super.noSuchMethod( + Invocation.method(#clipRRect, [rrect], {#doAntiAlias: doAntiAlias}), + returnValueForMissingStub: null, + ); + + @override + void clipPath(_i2.Path? path, {bool? doAntiAlias = true}) => + super.noSuchMethod( + Invocation.method(#clipPath, [path], {#doAntiAlias: doAntiAlias}), + returnValueForMissingStub: null, + ); + + @override + _i2.Rect getLocalClipBounds() => + (super.noSuchMethod( + Invocation.method(#getLocalClipBounds, []), + returnValue: _FakeRect_0( + this, + Invocation.method(#getLocalClipBounds, []), + ), + ) + as _i2.Rect); + + @override + _i2.Rect getDestinationClipBounds() => + (super.noSuchMethod( + Invocation.method(#getDestinationClipBounds, []), + returnValue: _FakeRect_0( + this, + Invocation.method(#getDestinationClipBounds, []), + ), + ) + as _i2.Rect); + + @override + void drawColor(_i2.Color? color, _i2.BlendMode? blendMode) => + super.noSuchMethod( + Invocation.method(#drawColor, [color, blendMode]), + returnValueForMissingStub: null, + ); + + @override + void drawLine(_i2.Offset? p1, _i2.Offset? p2, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawLine, [p1, p2, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawPaint(_i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawPaint, [paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRect(_i2.Rect? rect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRect, [rect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRRect(_i2.RRect? rrect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRRect, [rrect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawDRRect(_i2.RRect? outer, _i2.RRect? inner, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawDRRect, [outer, inner, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawOval(_i2.Rect? rect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawOval, [rect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawCircle(_i2.Offset? c, double? radius, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawCircle, [c, radius, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawArc( + _i2.Rect? rect, + double? startAngle, + double? sweepAngle, + bool? useCenter, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawArc, [ + rect, + startAngle, + sweepAngle, + useCenter, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawPath(_i2.Path? path, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawPath, [path, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImage(_i2.Image? image, _i2.Offset? offset, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawImage, [image, offset, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImageRect( + _i2.Image? image, + _i2.Rect? src, + _i2.Rect? dst, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawImageRect, [image, src, dst, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImageNine( + _i2.Image? image, + _i2.Rect? center, + _i2.Rect? dst, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawImageNine, [image, center, dst, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawPicture(_i2.Picture? picture) => super.noSuchMethod( + Invocation.method(#drawPicture, [picture]), + returnValueForMissingStub: null, + ); + + @override + void drawParagraph(_i2.Paragraph? paragraph, _i2.Offset? offset) => + super.noSuchMethod( + Invocation.method(#drawParagraph, [paragraph, offset]), + returnValueForMissingStub: null, + ); + + @override + void drawPoints( + _i2.PointMode? pointMode, + List<_i2.Offset>? points, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawPoints, [pointMode, points, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRawPoints( + _i2.PointMode? pointMode, + _i7.Float32List? points, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawRawPoints, [pointMode, points, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawVertices( + _i2.Vertices? vertices, + _i2.BlendMode? blendMode, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawVertices, [vertices, blendMode, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawAtlas( + _i2.Image? atlas, + List<_i2.RSTransform>? transforms, + List<_i2.Rect>? rects, + List<_i2.Color>? colors, + _i2.BlendMode? blendMode, + _i2.Rect? cullRect, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawAtlas, [ + atlas, + transforms, + rects, + colors, + blendMode, + cullRect, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawRawAtlas( + _i2.Image? atlas, + _i7.Float32List? rstTransforms, + _i7.Float32List? rects, + _i7.Int32List? colors, + _i2.BlendMode? blendMode, + _i2.Rect? cullRect, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawRawAtlas, [ + atlas, + rstTransforms, + rects, + colors, + blendMode, + cullRect, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawShadow( + _i2.Path? path, + _i2.Color? color, + double? elevation, + bool? transparentOccluder, + ) => super.noSuchMethod( + Invocation.method(#drawShadow, [ + path, + color, + elevation, + transparentOccluder, + ]), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [PaintingContext]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPaintingContext extends _i1.Mock implements _i3.PaintingContext { + MockPaintingContext() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.Rect get estimatedBounds => + (super.noSuchMethod( + Invocation.getter(#estimatedBounds), + returnValue: _FakeRect_0(this, Invocation.getter(#estimatedBounds)), + ) + as _i2.Rect); + + @override + _i2.Canvas get canvas => + (super.noSuchMethod( + Invocation.getter(#canvas), + returnValue: _FakeCanvas_1(this, Invocation.getter(#canvas)), + ) + as _i2.Canvas); + + @override + void paintChild(_i3.RenderObject? child, _i2.Offset? offset) => + super.noSuchMethod( + Invocation.method(#paintChild, [child, offset]), + returnValueForMissingStub: null, + ); + + @override + void appendLayer(_i4.Layer? layer) => super.noSuchMethod( + Invocation.method(#appendLayer, [layer]), + returnValueForMissingStub: null, + ); + + @override + _i2.VoidCallback addCompositionCallback(_i4.CompositionCallback? callback) => + (super.noSuchMethod( + Invocation.method(#addCompositionCallback, [callback]), + returnValue: () {}, + ) + as _i2.VoidCallback); + + @override + void stopRecordingIfNeeded() => super.noSuchMethod( + Invocation.method(#stopRecordingIfNeeded, []), + returnValueForMissingStub: null, + ); + + @override + void setIsComplexHint() => super.noSuchMethod( + Invocation.method(#setIsComplexHint, []), + returnValueForMissingStub: null, + ); + + @override + void setWillChangeHint() => super.noSuchMethod( + Invocation.method(#setWillChangeHint, []), + returnValueForMissingStub: null, + ); + + @override + void addLayer(_i4.Layer? layer) => super.noSuchMethod( + Invocation.method(#addLayer, [layer]), + returnValueForMissingStub: null, + ); + + @override + void pushLayer( + _i4.ContainerLayer? childLayer, + _i3.PaintingContextCallback? painter, + _i2.Offset? offset, { + _i2.Rect? childPaintBounds, + }) => super.noSuchMethod( + Invocation.method( + #pushLayer, + [childLayer, painter, offset], + {#childPaintBounds: childPaintBounds}, + ), + returnValueForMissingStub: null, + ); + + @override + _i3.PaintingContext createChildContext( + _i4.ContainerLayer? childLayer, + _i2.Rect? bounds, + ) => + (super.noSuchMethod( + Invocation.method(#createChildContext, [childLayer, bounds]), + returnValue: _FakePaintingContext_2( + this, + Invocation.method(#createChildContext, [childLayer, bounds]), + ), + ) + as _i3.PaintingContext); + + @override + _i4.ClipRectLayer? pushClipRect( + bool? needsCompositing, + _i2.Offset? offset, + _i2.Rect? clipRect, + _i3.PaintingContextCallback? painter, { + _i2.Clip? clipBehavior = _i2.Clip.hardEdge, + _i4.ClipRectLayer? oldLayer, + }) => + (super.noSuchMethod( + Invocation.method( + #pushClipRect, + [needsCompositing, offset, clipRect, painter], + {#clipBehavior: clipBehavior, #oldLayer: oldLayer}, + ), + ) + as _i4.ClipRectLayer?); + + @override + _i4.ClipRRectLayer? pushClipRRect( + bool? needsCompositing, + _i2.Offset? offset, + _i2.Rect? bounds, + _i2.RRect? clipRRect, + _i3.PaintingContextCallback? painter, { + _i2.Clip? clipBehavior = _i2.Clip.antiAlias, + _i4.ClipRRectLayer? oldLayer, + }) => + (super.noSuchMethod( + Invocation.method( + #pushClipRRect, + [needsCompositing, offset, bounds, clipRRect, painter], + {#clipBehavior: clipBehavior, #oldLayer: oldLayer}, + ), + ) + as _i4.ClipRRectLayer?); + + @override + _i4.ClipPathLayer? pushClipPath( + bool? needsCompositing, + _i2.Offset? offset, + _i2.Rect? bounds, + _i2.Path? clipPath, + _i3.PaintingContextCallback? painter, { + _i2.Clip? clipBehavior = _i2.Clip.antiAlias, + _i4.ClipPathLayer? oldLayer, + }) => + (super.noSuchMethod( + Invocation.method( + #pushClipPath, + [needsCompositing, offset, bounds, clipPath, painter], + {#clipBehavior: clipBehavior, #oldLayer: oldLayer}, + ), + ) + as _i4.ClipPathLayer?); + + @override + _i4.ColorFilterLayer pushColorFilter( + _i2.Offset? offset, + _i2.ColorFilter? colorFilter, + _i3.PaintingContextCallback? painter, { + _i4.ColorFilterLayer? oldLayer, + }) => + (super.noSuchMethod( + Invocation.method( + #pushColorFilter, + [offset, colorFilter, painter], + {#oldLayer: oldLayer}, + ), + returnValue: _FakeColorFilterLayer_3( + this, + Invocation.method( + #pushColorFilter, + [offset, colorFilter, painter], + {#oldLayer: oldLayer}, + ), + ), + ) + as _i4.ColorFilterLayer); + + @override + _i4.TransformLayer? pushTransform( + bool? needsCompositing, + _i2.Offset? offset, + _i8.Matrix4? transform, + _i3.PaintingContextCallback? painter, { + _i4.TransformLayer? oldLayer, + }) => + (super.noSuchMethod( + Invocation.method( + #pushTransform, + [needsCompositing, offset, transform, painter], + {#oldLayer: oldLayer}, + ), + ) + as _i4.TransformLayer?); + + @override + _i4.OpacityLayer pushOpacity( + _i2.Offset? offset, + int? alpha, + _i3.PaintingContextCallback? painter, { + _i4.OpacityLayer? oldLayer, + }) => + (super.noSuchMethod( + Invocation.method( + #pushOpacity, + [offset, alpha, painter], + {#oldLayer: oldLayer}, + ), + returnValue: _FakeOpacityLayer_4( + this, + Invocation.method( + #pushOpacity, + [offset, alpha, painter], + {#oldLayer: oldLayer}, + ), + ), + ) + as _i4.OpacityLayer); + + @override + void clipPathAndPaint( + _i2.Path? path, + _i2.Clip? clipBehavior, + _i2.Rect? bounds, + _i2.VoidCallback? painter, + ) => super.noSuchMethod( + Invocation.method(#clipPathAndPaint, [path, clipBehavior, bounds, painter]), + returnValueForMissingStub: null, + ); + + @override + void clipRRectAndPaint( + _i2.RRect? rrect, + _i2.Clip? clipBehavior, + _i2.Rect? bounds, + _i2.VoidCallback? painter, + ) => super.noSuchMethod( + Invocation.method(#clipRRectAndPaint, [ + rrect, + clipBehavior, + bounds, + painter, + ]), + returnValueForMissingStub: null, + ); + + @override + void clipRectAndPaint( + _i2.Rect? rect, + _i2.Clip? clipBehavior, + _i2.Rect? bounds, + _i2.VoidCallback? painter, + ) => super.noSuchMethod( + Invocation.method(#clipRectAndPaint, [rect, clipBehavior, bounds, painter]), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [BuildContext]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockBuildContext extends _i1.Mock implements _i6.BuildContext { + MockBuildContext() { + _i1.throwOnMissingStub(this); + } + + @override + _i6.Widget get widget => + (super.noSuchMethod( + Invocation.getter(#widget), + returnValue: _FakeWidget_5(this, Invocation.getter(#widget)), + ) + as _i6.Widget); + + @override + bool get mounted => + (super.noSuchMethod(Invocation.getter(#mounted), returnValue: false) + as bool); + + @override + bool get debugDoingBuild => + (super.noSuchMethod( + Invocation.getter(#debugDoingBuild), + returnValue: false, + ) + as bool); + + @override + _i6.InheritedWidget dependOnInheritedElement( + _i6.InheritedElement? ancestor, { + Object? aspect, + }) => + (super.noSuchMethod( + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + returnValue: _FakeInheritedWidget_6( + this, + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + ), + ) + as _i6.InheritedWidget); + + @override + void visitAncestorElements(_i6.ConditionalElementVisitor? visitor) => + super.noSuchMethod( + Invocation.method(#visitAncestorElements, [visitor]), + returnValueForMissingStub: null, + ); + + @override + void visitChildElements(_i6.ElementVisitor? visitor) => super.noSuchMethod( + Invocation.method(#visitChildElements, [visitor]), + returnValueForMissingStub: null, + ); + + @override + void dispatchNotification(_i9.Notification? notification) => + super.noSuchMethod( + Invocation.method(#dispatchNotification, [notification]), + returnValueForMissingStub: null, + ); + + @override + _i5.DiagnosticsNode describeElement( + String? name, { + _i5.DiagnosticsTreeStyle? style = _i5.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method(#describeElement, [name], {#style: style}), + returnValue: _FakeDiagnosticsNode_7( + this, + Invocation.method(#describeElement, [name], {#style: style}), + ), + ) + as _i5.DiagnosticsNode); + + @override + _i5.DiagnosticsNode describeWidget( + String? name, { + _i5.DiagnosticsTreeStyle? style = _i5.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method(#describeWidget, [name], {#style: style}), + returnValue: _FakeDiagnosticsNode_7( + this, + Invocation.method(#describeWidget, [name], {#style: style}), + ), + ) + as _i5.DiagnosticsNode); + + @override + List<_i5.DiagnosticsNode> describeMissingAncestor({ + required Type? expectedAncestorType, + }) => + (super.noSuchMethod( + Invocation.method(#describeMissingAncestor, [], { + #expectedAncestorType: expectedAncestorType, + }), + returnValue: <_i5.DiagnosticsNode>[], + ) + as List<_i5.DiagnosticsNode>); + + @override + _i5.DiagnosticsNode describeOwnershipChain(String? name) => + (super.noSuchMethod( + Invocation.method(#describeOwnershipChain, [name]), + returnValue: _FakeDiagnosticsNode_7( + this, + Invocation.method(#describeOwnershipChain, [name]), + ), + ) + as _i5.DiagnosticsNode); +} + +/// A class which mocks [RadarChartPainter]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockRadarChartPainter extends _i1.Mock implements _i10.RadarChartPainter { + MockRadarChartPainter() { + _i1.throwOnMissingStub(this); + } + + @override + set dataSetsPosition(List<_i10.RadarDataSetsPosition>? _dataSetsPosition) => + super.noSuchMethod( + Invocation.setter(#dataSetsPosition, _dataSetsPosition), + returnValueForMissingStub: null, + ); + + @override + void paint( + _i6.BuildContext? context, + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.RadarChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#paint, [context, canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + double getDefaultChartCenterValue() => + (super.noSuchMethod( + Invocation.method(#getDefaultChartCenterValue, []), + returnValue: 0.0, + ) + as double); + + @override + double getChartCenterValue(_i13.RadarChartData? data) => + (super.noSuchMethod( + Invocation.method(#getChartCenterValue, [data]), + returnValue: 0.0, + ) + as double); + + @override + double getScaledPoint( + _i13.RadarEntry? point, + double? radius, + _i13.RadarChartData? data, + ) => + (super.noSuchMethod( + Invocation.method(#getScaledPoint, [point, radius, data]), + returnValue: 0.0, + ) + as double); + + @override + double getFirstTickValue(_i13.RadarChartData? data) => + (super.noSuchMethod( + Invocation.method(#getFirstTickValue, [data]), + returnValue: 0.0, + ) + as double); + + @override + double getSpaceBetweenTicks(_i13.RadarChartData? data) => + (super.noSuchMethod( + Invocation.method(#getSpaceBetweenTicks, [data]), + returnValue: 0.0, + ) + as double); + + @override + void drawTicks( + _i6.BuildContext? context, + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.RadarChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawTicks, [context, canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawGrids( + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.RadarChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawGrids, [canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawTitles( + _i6.BuildContext? context, + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.RadarChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawTitles, [context, canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawDataSets( + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.RadarChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawDataSets, [canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + _i13.RadarTouchedSpot? handleTouch( + _i2.Offset? touchedPoint, + _i2.Size? viewSize, + _i12.PaintHolder<_i13.RadarChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#handleTouch, [touchedPoint, viewSize, holder]), + ) + as _i13.RadarTouchedSpot?); + + @override + double radarCenterY(_i2.Size? size) => + (super.noSuchMethod( + Invocation.method(#radarCenterY, [size]), + returnValue: 0.0, + ) + as double); + + @override + double radarCenterX(_i2.Size? size) => + (super.noSuchMethod( + Invocation.method(#radarCenterX, [size]), + returnValue: 0.0, + ) + as double); + + @override + double radarRadius(_i2.Size? size) => + (super.noSuchMethod( + Invocation.method(#radarRadius, [size]), + returnValue: 0.0, + ) + as double); + + @override + List<_i10.RadarDataSetsPosition> calculateDataSetsPosition( + _i2.Size? viewSize, + _i12.PaintHolder<_i13.RadarChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#calculateDataSetsPosition, [viewSize, holder]), + returnValue: <_i10.RadarDataSetsPosition>[], + ) + as List<_i10.RadarDataSetsPosition>); +} diff --git a/test/chart/scatter_chart/scatter_chart_data_test.dart b/test/chart/scatter_chart/scatter_chart_data_test.dart new file mode 100644 index 0000000..43e8bcf --- /dev/null +++ b/test/chart/scatter_chart/scatter_chart_data_test.dart @@ -0,0 +1,529 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../data_pool.dart'; + +void main() { + group('ScatterChart data equality check', () { + test('ScatterChartData equality test', () { + expect(scatterChartData1 == scatterChartData1Clone, true); + expect( + scatterChartData1 == + scatterChartData1Clone.copyWith(showingTooltipIndicators: []), + false, + ); + expect( + scatterChartData1 == + scatterChartData1Clone.copyWith( + borderData: FlBorderData( + show: true, + border: Border.all(color: Colors.green), + ), + ), + false, + ); + expect( + scatterChartData1 == + scatterChartData1Clone.copyWith( + borderData: FlBorderData( + show: true, + border: Border.all(color: Colors.white), + ), + ), + true, + ); + expect( + scatterChartData1 == scatterChartData1Clone.copyWith(maxX: 444), + false, + ); + expect( + scatterChartData1 == + scatterChartData1Clone.copyWith( + scatterSpots: [ + ScatterSpot( + 0, + 0, + show: false, + dotPainter: FlDotCirclePainter( + radius: 33, + color: Colors.yellow, + ), + ), + ScatterSpot( + 2, + 2, + show: false, + renderPriority: 10, + dotPainter: FlDotCirclePainter( + radius: 11, + color: Colors.purple, + ), + ), + ScatterSpot( + 1, + 2, + show: false, + renderPriority: -1, + dotPainter: FlDotCirclePainter( + radius: 11, + color: Colors.white, + ), + ), + ], + ), + true, + ); + expect( + scatterChartData1 == + scatterChartData1Clone.copyWith( + scatterSpots: [ + ScatterSpot( + 2, + 2, + show: false, + renderPriority: 10, + dotPainter: FlDotCirclePainter( + radius: 11, + color: Colors.purple, + ), + ), + ScatterSpot( + 0, + 0, + show: false, + renderPriority: 0, + dotPainter: FlDotCirclePainter( + radius: 33, + color: Colors.yellow, + ), + ), + ScatterSpot( + 1, + 2, + show: false, + renderPriority: -1, + dotPainter: FlDotCirclePainter( + radius: 11, + color: Colors.white, + ), + ), + ], + ), + false, + ); + expect( + scatterChartData1 == + scatterChartData1Clone.copyWith(clipData: const FlClipData.all()), + false, + ); + expect( + scatterChartData1 == + scatterChartData1Clone.copyWith( + gridData: const FlGridData( + show: false, + getDrawingHorizontalLine: gridGetDrawingLine, + getDrawingVerticalLine: gridGetDrawingLine, + checkToShowHorizontalLine: gridCheckToShowLine, + checkToShowVerticalLine: gridCheckToShowLine, + drawVerticalLine: false, + horizontalInterval: 33, + verticalInterval: 1, + ), + ), + true, + ); + expect( + scatterChartData1 == + scatterChartData1Clone.copyWith( + gridData: const FlGridData( + getDrawingHorizontalLine: gridGetDrawingLine, + getDrawingVerticalLine: gridGetDrawingLine, + checkToShowHorizontalLine: gridCheckToShowLine, + checkToShowVerticalLine: gridCheckToShowLine, + drawVerticalLine: false, + horizontalInterval: 33, + verticalInterval: 1, + ), + ), + false, + ); + expect( + scatterChartData1 == + scatterChartData1Clone.copyWith( + gridData: FlGridData( + show: false, + getDrawingHorizontalLine: (value) => const FlLine( + color: Colors.green, + strokeWidth: 12, + dashArray: [1, 2], + ), + getDrawingVerticalLine: (value) => const FlLine( + color: Colors.yellow, + strokeWidth: 33, + dashArray: [0, 1], + ), + checkToShowHorizontalLine: (value) => false, + checkToShowVerticalLine: (value) => true, + drawVerticalLine: false, + horizontalInterval: 32, + verticalInterval: 1, + ), + ), + false, + ); + expect( + scatterChartData1 == + scatterChartData1Clone.copyWith( + titlesData: const FlTitlesData( + leftTitles: AxisTitles( + axisNameSize: 33, + axisNameWidget: MockData.widget1, + ), + rightTitles: AxisTitles( + axisNameSize: 1326, + axisNameWidget: MockData.widget3, + sideTitles: SideTitles(reservedSize: 500, showTitles: true), + ), + topTitles: AxisTitles( + axisNameSize: 34, + axisNameWidget: MockData.widget4, + ), + bottomTitles: AxisTitles( + axisNameSize: 22, + axisNameWidget: MockData.widget2, + ), + ), + ), + true, + ); + expect( + scatterChartData1 == + scatterChartData1Clone.copyWith( + titlesData: const FlTitlesData( + leftTitles: AxisTitles( + axisNameSize: 332, + axisNameWidget: Text('title 1'), + ), + rightTitles: AxisTitles( + axisNameSize: 1326, + axisNameWidget: Text('title 3'), + sideTitles: SideTitles(reservedSize: 500, showTitles: true), + ), + topTitles: AxisTitles( + axisNameSize: 34, + axisNameWidget: Text('title 4'), + ), + bottomTitles: AxisTitles( + axisNameSize: 22, + axisNameWidget: Text('title 2'), + ), + ), + ), + false, + ); + expect( + scatterChartData1 == + scatterChartData1Clone.copyWith( + titlesData: const FlTitlesData( + leftTitles: AxisTitles( + axisNameSize: 33, + axisNameWidget: Text('title 1'), + ), + rightTitles: AxisTitles( + axisNameSize: 1326, + axisNameWidget: Text('title 3'), + sideTitles: SideTitles(reservedSize: 500, showTitles: true), + ), + topTitles: AxisTitles( + axisNameSize: 34, + axisNameWidget: Text('title 4'), + ), + bottomTitles: AxisTitles( + axisNameSize: 22, + axisNameWidget: Text('title 2'), + sideTitles: SideTitles(showTitles: true), + ), + ), + ), + false, + ); + expect( + scatterChartData1 == + scatterChartData1Clone.copyWith( + titlesData: const FlTitlesData( + leftTitles: AxisTitles( + axisNameSize: 33, + axisNameWidget: Text('title 1'), + ), + rightTitles: AxisTitles( + axisNameSize: 1326, + axisNameWidget: Text('title 1'), + sideTitles: SideTitles(reservedSize: 500, showTitles: true), + ), + topTitles: AxisTitles( + axisNameSize: 34, + axisNameWidget: Text('title 4'), + ), + bottomTitles: AxisTitles( + axisNameSize: 22, + axisNameWidget: Text('title 2'), + ), + ), + ), + false, + ); + expect( + scatterChartData1 == + scatterChartData1Clone.copyWith( + titlesData: const FlTitlesData( + leftTitles: AxisTitles( + axisNameSize: 33, + axisNameWidget: Text('title 1'), + ), + rightTitles: AxisTitles( + axisNameSize: 13262, + axisNameWidget: Text('title 3'), + sideTitles: SideTitles(reservedSize: 500, showTitles: true), + ), + topTitles: AxisTitles( + axisNameSize: 34, + axisNameWidget: Text('title 4'), + ), + bottomTitles: AxisTitles( + axisNameSize: 22, + axisNameWidget: Text('title 2'), + ), + ), + ), + false, + ); + + expect( + scatterChartData1 == + scatterChartData1Clone.copyWith(showingTooltipIndicators: []), + false, + ); + + expect( + scatterChartData1 == + scatterChartData1Clone + .copyWith(showingTooltipIndicators: [2, 1, 0]), + false, + ); + + expect( + scatterChartData1 == + scatterChartData1Clone.copyWith( + scatterLabelSettings: ScatterLabelSettings( + showLabel: true, + getLabelTextStyleFunction: (int index, ScatterSpot spot) => + const TextStyle(color: Colors.green), + ), + ), + false, + ); + + expect( + scatterChartData1 == + scatterChartData1Clone.copyWith( + scatterLabelSettings: ScatterLabelSettings( + showLabel: false, + getLabelTextStyleFunction: (int index, ScatterSpot spot) => + const TextStyle(color: Colors.red), + getLabelFunction: (int index, ScatterSpot spot) => + 'Label - $index', + ), + ), + false, + ); + + expect( + scatterChartData1 == + scatterChartData1Clone.copyWith( + scatterLabelSettings: ScatterLabelSettings( + showLabel: true, + getLabelTextStyleFunction: (int index, ScatterSpot spot) => + const TextStyle(color: Colors.red), + getLabelFunction: (int index, ScatterSpot spot) => + 'Different Label - $index', + ), + ), + false, + ); + + expect( + scatterChartData1 == + scatterChartData1Clone.copyWith( + scatterLabelSettings: ScatterLabelSettings( + showLabel: true, + getLabelFunction: getLabel, + getLabelTextStyleFunction: getLabelTextStyle, + ), + ), + true, + ); + + expect( + scatterChartData1 == + scatterChartData1Clone.copyWith( + scatterLabelSettings: ScatterLabelSettings( + showLabel: true, + getLabelFunction: getLabel, + getLabelTextStyleFunction: getLabelTextStyle, + textDirection: TextDirection.rtl, + ), + ), + false, + ); + }); + + test('ScatterSpot equality test', () { + final scatterSpot = ScatterSpot(0, 1); + final scatterSpotClone = ScatterSpot(0, 1); + + expect(scatterSpot == scatterSpotClone.copyWith(), true); + expect(scatterSpot == scatterSpotClone.copyWith(y: 3), false); + expect(scatterSpot == scatterSpotClone.copyWith(x: 3), false); + }); + + test('ScatterTouchData equality test', () { + final sample = ScatterTouchData( + touchTooltipData: const ScatterTouchTooltipData( + maxContentWidth: 2, + getTooltipColor: scatterChartGetTooltipRedColor, + tooltipPadding: EdgeInsets.all(11), + ), + handleBuiltInTouches: false, + touchSpotThreshold: 23, + enabled: false, + ); + final sampleClone = ScatterTouchData( + touchTooltipData: const ScatterTouchTooltipData( + maxContentWidth: 2, + getTooltipColor: scatterChartGetTooltipRedColor, + tooltipPadding: EdgeInsets.all(11), + ), + handleBuiltInTouches: false, + touchSpotThreshold: 23, + enabled: false, + ); + expect(sample == sampleClone, true); + + expect( + sample == + sampleClone.copyWith( + touchCallback: (event, response) {}, + ), + false, + ); + expect( + sample == + sampleClone.copyWith( + enabled: true, + ), + false, + ); + expect( + sample == + sampleClone.copyWith( + touchSpotThreshold: 22, + ), + false, + ); + expect( + sample == + sampleClone.copyWith( + handleBuiltInTouches: true, + ), + false, + ); + expect( + sample == + sampleClone.copyWith( + longPressDuration: Duration.zero, + ), + false, + ); + }); + + test('ScatterTouchTooltipData equality test', () { + expect(scatterTouchTooltipData1 == scatterTouchTooltipData1Clone, true); + expect(scatterTouchTooltipData1 == scatterTouchTooltipData2, false); + expect(scatterTouchTooltipData1 == scatterTouchTooltipData3, false); + }); + + test('ScatterTooltipItem equality test', () { + final sample1 = ScatterTooltipItem( + 'aa', + textStyle: const TextStyle(color: Colors.red), + bottomMargin: 23, + ); + final sample2 = ScatterTooltipItem( + 'aa', + textStyle: const TextStyle(color: Colors.red), + bottomMargin: 23, + ); + expect(sample1 == sample2, true); + + var changed = ScatterTooltipItem( + 'a3a', + textStyle: const TextStyle(color: Colors.red), + bottomMargin: 23, + ); + expect(sample1 == changed, false); + + changed = ScatterTooltipItem( + 'aa', + textStyle: const TextStyle(color: Colors.green), + bottomMargin: 23, + ); + expect(sample1 == changed, false); + + changed = ScatterTooltipItem( + 'aa', + textStyle: const TextStyle(color: Colors.red), + bottomMargin: 0, + ); + expect(sample1 == changed, false); + }); + + test('ScatterLabelSettings equality test', () { + final sample1 = ScatterLabelSettings( + showLabel: true, + getLabelTextStyleFunction: getLabelTextStyle, + getLabelFunction: getLabel, + ); + final sample2 = ScatterLabelSettings( + showLabel: true, + getLabelTextStyleFunction: getLabelTextStyle, + getLabelFunction: getLabel, + ); + expect(sample1 == sample2, true); + + var changed = ScatterLabelSettings( + showLabel: false, + getLabelTextStyleFunction: getLabelTextStyle, + getLabelFunction: getLabel, + ); + expect(sample1 == changed, false); + + expect(sample1 == changed.copyWith(showLabel: true), true); + + changed = ScatterLabelSettings( + showLabel: true, + getLabelTextStyleFunction: getLabelTextStyle, + getLabelFunction: (int index, ScatterSpot spot) => 'Label', + ); + expect(sample1 == changed, false); + + changed = ScatterLabelSettings( + showLabel: true, + getLabelTextStyleFunction: getLabelTextStyle, + getLabelFunction: getLabel, + textDirection: TextDirection.rtl, + ); + expect(sample1 == changed, false); + }); + }); +} diff --git a/test/chart/scatter_chart/scatter_chart_helper_test.dart b/test/chart/scatter_chart/scatter_chart_helper_test.dart new file mode 100644 index 0000000..e6893ab --- /dev/null +++ b/test/chart/scatter_chart/scatter_chart_helper_test.dart @@ -0,0 +1,60 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/scatter_chart/scatter_chart_helper.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../data_pool.dart'; + +void main() { + group('Check caching of ScatterChartHelper.calculateMaxAxisValues', () { + test('Test validity 1', () { + final scatterSpots = [ + scatterSpot1, + scatterSpot2, + scatterSpot3, + scatterSpot4, + ]; + final (minX, maxX, minY, maxY) = + ScatterChartHelper.calculateMaxAxisValues(scatterSpots); + expect(minX, -14); + expect(maxX, 1); + expect(minY, -8); + expect(maxY, 40); + }); + + test('Test validity 2', () { + final scatterSpots = [ + ScatterSpot(3, -1), + ScatterSpot(-1, 3), + ]; + final (minX, maxX, minY, maxY) = + ScatterChartHelper.calculateMaxAxisValues(scatterSpots); + expect(minX, -1); + expect(maxX, 3); + expect(minY, -1); + expect(maxY, 3); + }); + + test('Test validity 3', () { + final scatterSpots = []; + final (minX, maxX, minY, maxY) = + ScatterChartHelper.calculateMaxAxisValues(scatterSpots); + expect(minX, 0); + expect(maxX, 0); + expect(minY, 0); + expect(maxY, 0); + }); + + test('Test equality', () { + final scatterSpots = [scatterSpot1, scatterSpot2, scatterSpot3]; + final scatterSpotsClone = [ + scatterSpot1Clone, + scatterSpot2Clone, + scatterSpot3, + ]; + final result1 = ScatterChartHelper.calculateMaxAxisValues(scatterSpots); + final result2 = + ScatterChartHelper.calculateMaxAxisValues(scatterSpotsClone); + expect(result1, result2); + }); + }); +} diff --git a/test/chart/scatter_chart/scatter_chart_painter_test.dart b/test/chart/scatter_chart/scatter_chart_painter_test.dart new file mode 100644 index 0000000..f88653d --- /dev/null +++ b/test/chart/scatter_chart/scatter_chart_painter_test.dart @@ -0,0 +1,880 @@ +import 'dart:math' as math; + +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart'; +import 'package:fl_chart/src/chart/scatter_chart/scatter_chart_painter.dart'; +import 'package:fl_chart/src/utils/canvas_wrapper.dart'; +import 'package:fl_chart/src/utils/utils.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import '../data_pool.dart'; +import 'scatter_chart_painter_test.mocks.dart'; + +@GenerateMocks([Canvas, CanvasWrapper, BuildContext, Utils]) +void main() { + group('paint()', () { + test('test 1', () { + final utilsMainInstance = Utils(); + const viewSize = Size(400, 400); + final data = ScatterChartData( + scatterSpots: [ + ScatterSpot(0, 1), + ScatterSpot(1, 3), + ScatterSpot(3, 4), + ], + ); + + final scatterPainter = ScatterChartPainter(); + final holder = PaintHolder( + data, + data, + TextScaler.noScaling, + ); + + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.getThemeAwareTextStyle(any, any)) + .thenAnswer((realInvocation) => textStyle1); + when(mockUtils.calculateRotationOffset(any, any)) + .thenAnswer((realInvocation) => Offset.zero); + when(mockUtils.convertRadiusToSigma(any)) + .thenAnswer((realInvocation) => 4.0); + when(mockUtils.getEfficientInterval(any, any)) + .thenAnswer((realInvocation) => 1.0); + when(mockUtils.getBestInitialIntervalValue(any, any, any)) + .thenAnswer((realInvocation) => 1.0); + when(mockUtils.normalizeBorderRadius(any, any)) + .thenAnswer((realInvocation) => BorderRadius.zero); + when(mockUtils.normalizeBorderSide(any, any)).thenAnswer( + (realInvocation) => const BorderSide(color: MockData.color0), + ); + + final mockBuildContext = MockBuildContext(); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenAnswer((realInvocation) => viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + scatterPainter.paint( + mockBuildContext, + mockCanvasWrapper, + holder, + ); + + verify(mockCanvasWrapper.drawDot(any, any, any)).called(3); + Utils.changeInstance(utilsMainInstance); + }); + }); + + group('drawSpots()', () { + test('test 1', () { + const viewSize = Size(100, 100); + + final dotPainter1 = FlDotCirclePainter(radius: 18); + final dotPainter3 = FlDotCirclePainter(radius: 4); + final dotPainter4 = FlDotCirclePainter(radius: 6); + + final spot1 = ScatterSpot(1, 1, dotPainter: dotPainter1); + final spot2 = ScatterSpot(3, 9, show: false); + final spot3 = ScatterSpot(8, 2, dotPainter: dotPainter3); + final spot4 = ScatterSpot(7, 5, dotPainter: dotPainter4); + final data = ScatterChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + scatterSpots: [spot1, spot2, spot3, spot4], + titlesData: const FlTitlesData(show: false), + clipData: const FlClipData.all(), + ); + + final scatterChartPainter = ScatterChartPainter(); + final holder = PaintHolder( + data, + data, + TextScaler.noScaling, + ); + + final mockBuildContext = MockBuildContext(); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenReturn(viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + scatterChartPainter.drawSpots( + mockBuildContext, + mockCanvasWrapper, + holder, + ); + + verify( + mockCanvasWrapper.drawDot( + dotPainter1, + spot1, + const Offset(10, 90), + ), + ).called(1); + verify( + mockCanvasWrapper.drawDot( + dotPainter3, + spot3, + const Offset(80, 80), + ), + ).called(1); + verify( + mockCanvasWrapper.drawDot( + dotPainter4, + spot4, + const Offset(70, 50), + ), + ).called(1); + + verifyNever(mockCanvasWrapper.drawText(any, any)); + verify(mockCanvasWrapper.clipRect(any)).called(1); + }); + + test('test 2', () { + const viewSize = Size(100, 100); + + final data = ScatterChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + scatterSpots: [ + ScatterSpot(1, 1, show: false), + ScatterSpot(3, 9, show: false), + ScatterSpot(8, 2, show: false), + ScatterSpot(7, 5, show: false), + ], + titlesData: const FlTitlesData(show: false), + clipData: const FlClipData.none(), + ); + + final scatterChartPainter = ScatterChartPainter(); + final holder = PaintHolder( + data, + data, + TextScaler.noScaling, + ); + + final mockBuildContext = MockBuildContext(); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenReturn(viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + scatterChartPainter.drawSpots( + mockBuildContext, + mockCanvasWrapper, + holder, + ); + + verifyNever(mockCanvasWrapper.drawCircle(any, any, any)); + verifyNever(mockCanvasWrapper.clipRect(any)); + + verifyNever(mockCanvasWrapper.drawText(any, any)); + }); + + test('test 3', () { + const viewSize = Size(100, 100); + + final data = ScatterChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + scatterSpots: [ + ScatterSpot(1, 1, dotPainter: FlDotCirclePainter(radius: 18)), + ScatterSpot(2, 2, dotPainter: FlDotCirclePainter(radius: 8)), + ScatterSpot(3, 9, show: false), + ScatterSpot(8, 8, dotPainter: FlDotCirclePainter(radius: 4)), + ScatterSpot(7, 5, dotPainter: FlDotCirclePainter(radius: 20)), + ScatterSpot(4, 6, dotPainter: FlDotCirclePainter(radius: 24)), + ], + titlesData: const FlTitlesData(show: false), + clipData: const FlClipData.all(), + scatterLabelSettings: ScatterLabelSettings( + showLabel: true, + getLabelTextStyleFunction: (int index, ScatterSpot spot) => + const TextStyle(fontSize: 12), + getLabelFunction: (int index, ScatterSpot spot) { + if (index == 5) { + return ''; + } + return 'Label : $index'; + }, + ), + ); + + final scatterChartPainter = ScatterChartPainter(); + final holder = PaintHolder( + data, + data, + TextScaler.noScaling, + ); + + final mockBuildContext = MockBuildContext(); + final mockCanvasWrapper = MockCanvasWrapper(); + when(mockCanvasWrapper.size).thenReturn(viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.getThemeAwareTextStyle(any, any)) + .thenReturn(const TextStyle(color: Color(0x00ffffff))); + when(mockUtils.calculateRotationOffset(any, any)).thenReturn(Offset.zero); + + scatterChartPainter.drawSpots( + mockBuildContext, + mockCanvasWrapper, + holder, + ); + + verify( + mockCanvasWrapper.drawDot( + any, + data.scatterSpots[0], + const Offset(10, 90), + ), + ).called(1); + verify( + mockCanvasWrapper.drawDot( + any, + data.scatterSpots[1], + const Offset(20, 80), + ), + ).called(1); + verify( + mockCanvasWrapper.drawDot( + any, + data.scatterSpots[3], + const Offset(80, 20), + ), + ).called(1); + verify( + mockCanvasWrapper.drawDot( + any, + data.scatterSpots[4], + const Offset(70, 50), + ), + ).called(1); + verify( + mockCanvasWrapper.drawDot( + any, + data.scatterSpots[5], + const Offset(40, 40), + ), + ).called(1); + + verify(mockCanvasWrapper.drawText(any, any)).called(4); + + verify(mockCanvasWrapper.clipRect(any)).called(1); + }); + }); + + group('drawTooltips()', () { + test('test 1', () { + const viewSize = Size(100, 100); + + final data = ScatterChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + scatterSpots: [ + ScatterSpot(1, 1, dotPainter: FlDotCirclePainter(radius: 18)), + ScatterSpot(3, 9, show: false), + ScatterSpot(8, 2, dotPainter: FlDotCirclePainter(radius: 4)), + ScatterSpot(7, 5, dotPainter: FlDotCirclePainter(radius: 6)), + ], + showingTooltipIndicators: [0, 2, 3], + titlesData: const FlTitlesData(show: false), + ); + + final scatterChartPainter = ScatterChartPainter(); + final holder = PaintHolder( + data, + data, + TextScaler.noScaling, + ); + final mockCanvasWrapper = MockCanvasWrapper(); + final mockBuildContext = MockBuildContext(); + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.getThemeAwareTextStyle(any, any)) + .thenReturn(const TextStyle(color: Color(0x00ffffff))); + when(mockUtils.calculateRotationOffset(any, any)).thenReturn(Offset.zero); + when(mockCanvasWrapper.size).thenReturn(viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + scatterChartPainter.drawTouchTooltips( + mockBuildContext, + mockCanvasWrapper, + holder, + ); + + verify( + mockCanvasWrapper.drawRotated( + size: anyNamed('size'), + rotationOffset: anyNamed('rotationOffset'), + drawOffset: anyNamed('drawOffset'), + angle: anyNamed('angle'), + drawCallback: anyNamed('drawCallback'), + ), + ).called(3); + }); + + test('test 2', () { + const viewSize = Size(100, 100); + + final data = ScatterChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + scatterSpots: [ + ScatterSpot(1, 1, dotPainter: FlDotCirclePainter(radius: 18)), + ScatterSpot(3, 9, show: false), + ScatterSpot(8, 2, dotPainter: FlDotCirclePainter(radius: 4)), + ScatterSpot(7, 5, dotPainter: FlDotCirclePainter(radius: 6)), + ], + showingTooltipIndicators: [0, 2, 3], + scatterTouchData: ScatterTouchData( + touchTooltipData: ScatterTouchTooltipData( + getTooltipItems: (spot) => null, + ), + ), + titlesData: const FlTitlesData(show: false), + ); + + final scatterChartPainter = ScatterChartPainter(); + final holder = PaintHolder( + data, + data, + TextScaler.noScaling, + ); + final mockCanvasWrapper = MockCanvasWrapper(); + final mockBuildContext = MockBuildContext(); + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.getThemeAwareTextStyle(any, any)) + .thenReturn(const TextStyle(color: Color(0x00ffffff))); + when(mockUtils.calculateRotationOffset(any, any)).thenReturn(Offset.zero); + when(mockCanvasWrapper.size).thenReturn(viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + scatterChartPainter.drawTouchTooltips( + mockBuildContext, + mockCanvasWrapper, + holder, + ); + + verifyNever( + mockCanvasWrapper.drawRotated( + size: null, + angle: null, + drawCallback: () {}, + ), + ); + verifyNever(mockCanvasWrapper.drawRect(any, any)); + }); + }); + + group('drawTouchTooltip()', () { + test('test 1', () { + const viewSize = Size(100, 100); + + final spot1 = ScatterSpot(1, 1); + final data = ScatterChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + scatterSpots: [ + spot1, + scatterSpot2, + scatterSpot3, + scatterSpot4, + ], + showingTooltipIndicators: [0, 2, 3], + titlesData: const FlTitlesData(show: false), + scatterTouchData: ScatterTouchData( + touchTooltipData: ScatterTouchTooltipData( + rotateAngle: 18, + getTooltipColor: (touchedSpot) => const Color(0xFF00FF00), + tooltipBorderRadius: const BorderRadius.only( + topLeft: Radius.circular(85), + topRight: Radius.circular(8), + ), + tooltipPadding: const EdgeInsets.all(12), + getTooltipItems: (_) { + return ScatterTooltipItem( + 'faketext', + textStyle: textStyle1, + textAlign: TextAlign.left, + textDirection: TextDirection.rtl, + children: [ + textSpan2, + textSpan1, + ], + ); + }, + ), + ), + ); + + final scatterChartPainter = ScatterChartPainter(); + final holder = PaintHolder( + data, + data, + TextScaler.noScaling, + ); + final mockCanvasWrapper = MockCanvasWrapper(); + final mockBuildContext = MockBuildContext(); + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.getThemeAwareTextStyle(any, any)).thenReturn(textStyle2); + when(mockUtils.calculateRotationOffset(any, any)).thenReturn(Offset.zero); + when(mockCanvasWrapper.size).thenReturn(viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + scatterChartPainter.drawTouchTooltip( + mockBuildContext, + mockCanvasWrapper, + data.scatterTouchData.touchTooltipData, + spot1, + holder, + ); + + final verificationResult = verify( + mockCanvasWrapper.drawRotated( + size: anyNamed('size'), + drawOffset: anyNamed('drawOffset'), + angle: 18, + drawCallback: captureAnyNamed('drawCallback'), + ), + ); + + final passedDrawCallback = + verificationResult.captured.first as DrawCallback; + passedDrawCallback(); + + verificationResult.called(1); + + final captured2 = verifyInOrder([ + mockCanvasWrapper.drawRRect(captureAny, captureAny), + mockCanvasWrapper.drawText(captureAny, any), + ]).captured; + + final rRect = captured2[0][0] as RRect; + final bgPaint = captured2[0][1] as Paint; + final textPainter = captured2[1][0] as TextPainter; + + expect(rRect.blRadiusX, 0); + expect(rRect.blRadiusY, 0); + expect(rRect.tlRadiusY, 85); + expect(rRect.trRadiusX, 8); + + expect(bgPaint.color, const Color(0xFF00FF00)); + expect( + textPainter.text, + const TextSpan( + style: textStyle2, + text: 'faketext', + children: [ + textSpan2, + textSpan1, + ], + ), + ); + }); + + test('test 2', () { + const viewSize = Size(100, 100); + + final spot1 = ScatterSpot(1, 1); + final data = ScatterChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + scatterSpots: [ + spot1, + scatterSpot2, + scatterSpot3, + scatterSpot4, + ], + showingTooltipIndicators: [0, 2, 3], + titlesData: const FlTitlesData(show: false), + scatterTouchData: ScatterTouchData( + touchTooltipData: ScatterTouchTooltipData( + rotateAngle: 18, + getTooltipColor: (touchedSpot) => const Color(0xFFFFFF00), + tooltipBorderRadius: BorderRadius.circular(22), + fitInsideHorizontally: false, + fitInsideVertically: true, + tooltipPadding: const EdgeInsets.all(12), + tooltipHorizontalAlignment: FLHorizontalAlignment.left, + getTooltipItems: (_) { + return ScatterTooltipItem( + 'faketext', + textStyle: textStyle2, + textAlign: TextAlign.right, + textDirection: TextDirection.ltr, + children: [ + textSpan1, + textSpan2, + ], + ); + }, + ), + ), + ); + + final scatterChartPainter = ScatterChartPainter(); + final holder = PaintHolder( + data, + data, + TextScaler.noScaling, + ); + final mockCanvasWrapper = MockCanvasWrapper(); + final mockBuildContext = MockBuildContext(); + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.getThemeAwareTextStyle(any, any)).thenReturn(textStyle1); + when(mockUtils.calculateRotationOffset(any, any)).thenReturn(Offset.zero); + when(mockCanvasWrapper.size).thenReturn(viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + scatterChartPainter.drawTouchTooltip( + mockBuildContext, + mockCanvasWrapper, + data.scatterTouchData.touchTooltipData, + spot1, + holder, + ); + + final verificationResult = verify( + mockCanvasWrapper.drawRotated( + size: anyNamed('size'), + drawOffset: anyNamed('drawOffset'), + angle: 18, + drawCallback: captureAnyNamed('drawCallback'), + ), + ); + + final passedDrawCallback = + verificationResult.captured.first as DrawCallback; + passedDrawCallback(); + + verificationResult.called(1); + + final captured2 = verifyInOrder([ + mockCanvasWrapper.drawRRect(captureAny, captureAny), + mockCanvasWrapper.drawText(captureAny, any), + ]).captured; + + final rRect = captured2[0][0] as RRect; + final bgPaint = captured2[0][1] as Paint; + final textPainter = captured2[1][0] as TextPainter; + + expect(rRect.blRadiusX, 22); + expect(rRect.tlRadiusY, 22); + + expect(rRect.left, -134); + + expect(bgPaint.color, const Color(0xFFFFFF00)); + expect( + textPainter.text, + const TextSpan( + style: textStyle1, + text: 'faketext', + children: [ + textSpan1, + textSpan2, + ], + ), + ); + }); + + test('test 3', () { + const viewSize = Size(100, 100); + + final spot1 = ScatterSpot(1, 1); + final data = ScatterChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + scatterSpots: [ + spot1, + scatterSpot2, + scatterSpot3, + scatterSpot4, + ], + showingTooltipIndicators: [0, 2, 3], + titlesData: const FlTitlesData(show: false), + scatterTouchData: ScatterTouchData( + touchTooltipData: ScatterTouchTooltipData( + rotateAngle: 18, + getTooltipColor: (touchedSpot) => const Color(0xFFFFFF00), + tooltipBorderRadius: BorderRadius.circular(22), + fitInsideHorizontally: false, + fitInsideVertically: true, + tooltipPadding: const EdgeInsets.all(12), + tooltipHorizontalAlignment: FLHorizontalAlignment.right, + getTooltipItems: (_) { + return ScatterTooltipItem( + 'faketext', + textStyle: textStyle2, + textAlign: TextAlign.right, + textDirection: TextDirection.ltr, + children: [ + textSpan1, + textSpan2, + ], + ); + }, + ), + ), + ); + + final scatterChartPainter = ScatterChartPainter(); + final holder = PaintHolder( + data, + data, + TextScaler.noScaling, + ); + final mockCanvasWrapper = MockCanvasWrapper(); + final mockBuildContext = MockBuildContext(); + final mockUtils = MockUtils(); + Utils.changeInstance(mockUtils); + when(mockUtils.getThemeAwareTextStyle(any, any)).thenReturn(textStyle1); + when(mockUtils.calculateRotationOffset(any, any)).thenReturn(Offset.zero); + when(mockCanvasWrapper.size).thenReturn(viewSize); + when(mockCanvasWrapper.canvas).thenReturn(MockCanvas()); + scatterChartPainter.drawTouchTooltip( + mockBuildContext, + mockCanvasWrapper, + data.scatterTouchData.touchTooltipData, + spot1, + holder, + ); + + final verificationResult = verify( + mockCanvasWrapper.drawRotated( + size: anyNamed('size'), + drawOffset: anyNamed('drawOffset'), + angle: 18, + drawCallback: captureAnyNamed('drawCallback'), + ), + ); + + final passedDrawCallback = + verificationResult.captured.first as DrawCallback; + passedDrawCallback(); + + verificationResult.called(1); + + final captured2 = verifyInOrder([ + mockCanvasWrapper.drawRRect(captureAny, captureAny), + mockCanvasWrapper.drawText(captureAny, any), + ]).captured; + + final rRect = captured2[0][0] as RRect; + final bgPaint = captured2[0][1] as Paint; + final textPainter = captured2[1][0] as TextPainter; + + expect(rRect.blRadiusX, 22); + expect(rRect.tlRadiusY, 22); + + expect(rRect.left, 10); + + expect(bgPaint.color, const Color(0xFFFFFF00)); + expect( + textPainter.text, + const TextSpan( + style: textStyle1, + text: 'faketext', + children: [ + textSpan1, + textSpan2, + ], + ), + ); + }); + }); + + group('handleTouch()', () { + test('test 1', () { + const viewSize = Size(100, 100); + final spots = [ + ScatterSpot(1, 1), + ScatterSpot(2, 4), + ScatterSpot(5, 2, dotPainter: FlDotCirclePainter(radius: 0.5)), + ScatterSpot(8, 7), + ]; + + final data = ScatterChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + titlesData: const FlTitlesData(show: false), + scatterSpots: spots, + ); + + final scatterChartPainter = ScatterChartPainter(); + final holder = PaintHolder( + data, + data, + TextScaler.noScaling, + ); + final touchedSpot = scatterChartPainter.handleTouch( + const Offset(10, 90), + viewSize, + holder, + ); + expect(touchedSpot!.spot, spots[0]); + + final touchedSpot2 = scatterChartPainter.handleTouch( + const Offset(50, 80), + viewSize, + holder, + ); + expect(touchedSpot2!.spot, spots[2]); + + final touchedSpot3 = scatterChartPainter.handleTouch( + const Offset(50.49, 80), + viewSize, + holder, + ); + expect(touchedSpot3!.spot, spots[2]); + + final touchedSpot4 = scatterChartPainter.handleTouch( + const Offset(50.5, 80), + viewSize, + holder, + ); + expect(touchedSpot4, null); + + final radius = spots[2].size.width / 2; + final touchedSpot5 = scatterChartPainter.handleTouch( + Offset( + 50 + (math.cos(math.pi / 4) * radius) - 0.01, + 80 + (math.sin(math.pi / 4) * radius) - 0.01, + ), + viewSize, + holder, + ); + expect(touchedSpot5!.spot, spots[2]); + + final touchedSpot6 = scatterChartPainter.handleTouch( + Offset( + 50 + (math.cos(math.pi / 4) * radius), + 80 + (math.sin(math.pi / 4) * radius), + ), + viewSize, + holder, + ); + expect(touchedSpot6, null); + }); + + test('test 2', () { + const viewSize = Size(100, 100); + final spots = [ + ScatterSpot(1, 1), + ScatterSpot(2, 4), + ScatterSpot(5, 2, dotPainter: FlDotCirclePainter(radius: 0.5)), + ScatterSpot(8, 7), + ]; + + final data = ScatterChartData( + minY: 0, + maxY: 10, + minX: 0, + maxX: 10, + titlesData: const FlTitlesData( + leftTitles: AxisTitles( + axisNameSize: 4, + axisNameWidget: Text('ss1'), + sideTitles: SideTitles( + showTitles: true, + reservedSize: 10, + ), + ), + rightTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 10, + ), + ), + topTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 6, + ), + ), + bottomTitles: AxisTitles( + axisNameSize: 4, + axisNameWidget: Text('ss2'), + sideTitles: SideTitles( + showTitles: true, + reservedSize: 6, + ), + ), + ), + scatterSpots: spots, + ); + + final scatterChartPainter = ScatterChartPainter(); + final holder = PaintHolder( + data, + data, + TextScaler.noScaling, + ); + final touchedSpot = scatterChartPainter.handleTouch( + const Offset(10, 90), + viewSize, + holder, + ); + expect(touchedSpot!.spot, spots[0]); + + final touchedSpot2 = scatterChartPainter.handleTouch( + const Offset(50, 80), + viewSize, + holder, + ); + expect(touchedSpot2!.spot, spots[2]); + + final touchedSpot3 = scatterChartPainter.handleTouch( + const Offset(50.49, 80), + viewSize, + holder, + ); + expect(touchedSpot3!.spot, spots[2]); + + final touchedSpot4 = scatterChartPainter.handleTouch( + const Offset(50.5, 80), + viewSize, + holder, + ); + expect(touchedSpot4, null); + + final radius = spots[2].size.width / 2; + final touchedSpot5 = scatterChartPainter.handleTouch( + Offset( + 50 + (math.cos(math.pi / 4) * radius) - 0.01, + 80 + (math.sin(math.pi / 4) * radius) - 0.01, + ), + viewSize, + holder, + ); + expect(touchedSpot5!.spot, spots[2]); + + final touchedSpot6 = scatterChartPainter.handleTouch( + Offset( + 50 + (math.cos(math.pi / 4) * radius), + 80 + (math.sin(math.pi / 4) * radius), + ), + viewSize, + holder, + ); + expect(touchedSpot6, null); + }); + }); +} diff --git a/test/chart/scatter_chart/scatter_chart_painter_test.mocks.dart b/test/chart/scatter_chart/scatter_chart_painter_test.mocks.dart new file mode 100644 index 0000000..90b5d03 --- /dev/null +++ b/test/chart/scatter_chart/scatter_chart_painter_test.mocks.dart @@ -0,0 +1,924 @@ +// Mocks generated by Mockito 5.4.5 from annotations +// in fl_chart/test/chart/scatter_chart/scatter_chart_painter_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:typed_data' as _i5; +import 'dart:ui' as _i2; + +import 'package:fl_chart/fl_chart.dart' as _i7; +import 'package:fl_chart/src/utils/canvas_wrapper.dart' as _i6; +import 'package:fl_chart/src/utils/utils.dart' as _i8; +import 'package:flutter/cupertino.dart' as _i3; +import 'package:flutter/foundation.dart' as _i4; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i9; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeRect_0 extends _i1.SmartFake implements _i2.Rect { + _FakeRect_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeCanvas_1 extends _i1.SmartFake implements _i2.Canvas { + _FakeCanvas_1(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeSize_2 extends _i1.SmartFake implements _i2.Size { + _FakeSize_2(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeWidget_3 extends _i1.SmartFake implements _i3.Widget { + _FakeWidget_3(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeInheritedWidget_4 extends _i1.SmartFake + implements _i3.InheritedWidget { + _FakeInheritedWidget_4(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeDiagnosticsNode_5 extends _i1.SmartFake + implements _i3.DiagnosticsNode { + _FakeDiagnosticsNode_5(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({ + _i4.TextTreeConfiguration? parentConfiguration, + _i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info, + }) => super.toString(); +} + +class _FakeOffset_6 extends _i1.SmartFake implements _i2.Offset { + _FakeOffset_6(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeBorderSide_7 extends _i1.SmartFake implements _i3.BorderSide { + _FakeBorderSide_7(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeTextStyle_8 extends _i1.SmartFake implements _i3.TextStyle { + _FakeTextStyle_8(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +/// A class which mocks [Canvas]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCanvas extends _i1.Mock implements _i2.Canvas { + MockCanvas() { + _i1.throwOnMissingStub(this); + } + + @override + void save() => super.noSuchMethod( + Invocation.method(#save, []), + returnValueForMissingStub: null, + ); + + @override + void saveLayer(_i2.Rect? bounds, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#saveLayer, [bounds, paint]), + returnValueForMissingStub: null, + ); + + @override + void restore() => super.noSuchMethod( + Invocation.method(#restore, []), + returnValueForMissingStub: null, + ); + + @override + void restoreToCount(int? count) => super.noSuchMethod( + Invocation.method(#restoreToCount, [count]), + returnValueForMissingStub: null, + ); + + @override + int getSaveCount() => + (super.noSuchMethod(Invocation.method(#getSaveCount, []), returnValue: 0) + as int); + + @override + void translate(double? dx, double? dy) => super.noSuchMethod( + Invocation.method(#translate, [dx, dy]), + returnValueForMissingStub: null, + ); + + @override + void scale(double? sx, [double? sy]) => super.noSuchMethod( + Invocation.method(#scale, [sx, sy]), + returnValueForMissingStub: null, + ); + + @override + void rotate(double? radians) => super.noSuchMethod( + Invocation.method(#rotate, [radians]), + returnValueForMissingStub: null, + ); + + @override + void skew(double? sx, double? sy) => super.noSuchMethod( + Invocation.method(#skew, [sx, sy]), + returnValueForMissingStub: null, + ); + + @override + void transform(_i5.Float64List? matrix4) => super.noSuchMethod( + Invocation.method(#transform, [matrix4]), + returnValueForMissingStub: null, + ); + + @override + _i5.Float64List getTransform() => + (super.noSuchMethod( + Invocation.method(#getTransform, []), + returnValue: _i5.Float64List(0), + ) + as _i5.Float64List); + + @override + void clipRect( + _i2.Rect? rect, { + _i2.ClipOp? clipOp = _i2.ClipOp.intersect, + bool? doAntiAlias = true, + }) => super.noSuchMethod( + Invocation.method( + #clipRect, + [rect], + {#clipOp: clipOp, #doAntiAlias: doAntiAlias}, + ), + returnValueForMissingStub: null, + ); + + @override + void clipRRect(_i2.RRect? rrect, {bool? doAntiAlias = true}) => + super.noSuchMethod( + Invocation.method(#clipRRect, [rrect], {#doAntiAlias: doAntiAlias}), + returnValueForMissingStub: null, + ); + + @override + void clipPath(_i2.Path? path, {bool? doAntiAlias = true}) => + super.noSuchMethod( + Invocation.method(#clipPath, [path], {#doAntiAlias: doAntiAlias}), + returnValueForMissingStub: null, + ); + + @override + _i2.Rect getLocalClipBounds() => + (super.noSuchMethod( + Invocation.method(#getLocalClipBounds, []), + returnValue: _FakeRect_0( + this, + Invocation.method(#getLocalClipBounds, []), + ), + ) + as _i2.Rect); + + @override + _i2.Rect getDestinationClipBounds() => + (super.noSuchMethod( + Invocation.method(#getDestinationClipBounds, []), + returnValue: _FakeRect_0( + this, + Invocation.method(#getDestinationClipBounds, []), + ), + ) + as _i2.Rect); + + @override + void drawColor(_i2.Color? color, _i2.BlendMode? blendMode) => + super.noSuchMethod( + Invocation.method(#drawColor, [color, blendMode]), + returnValueForMissingStub: null, + ); + + @override + void drawLine(_i2.Offset? p1, _i2.Offset? p2, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawLine, [p1, p2, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawPaint(_i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawPaint, [paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRect(_i2.Rect? rect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRect, [rect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRRect(_i2.RRect? rrect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRRect, [rrect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawDRRect(_i2.RRect? outer, _i2.RRect? inner, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawDRRect, [outer, inner, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawOval(_i2.Rect? rect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawOval, [rect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawCircle(_i2.Offset? c, double? radius, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawCircle, [c, radius, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawArc( + _i2.Rect? rect, + double? startAngle, + double? sweepAngle, + bool? useCenter, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawArc, [ + rect, + startAngle, + sweepAngle, + useCenter, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawPath(_i2.Path? path, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawPath, [path, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImage(_i2.Image? image, _i2.Offset? offset, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawImage, [image, offset, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImageRect( + _i2.Image? image, + _i2.Rect? src, + _i2.Rect? dst, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawImageRect, [image, src, dst, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImageNine( + _i2.Image? image, + _i2.Rect? center, + _i2.Rect? dst, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawImageNine, [image, center, dst, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawPicture(_i2.Picture? picture) => super.noSuchMethod( + Invocation.method(#drawPicture, [picture]), + returnValueForMissingStub: null, + ); + + @override + void drawParagraph(_i2.Paragraph? paragraph, _i2.Offset? offset) => + super.noSuchMethod( + Invocation.method(#drawParagraph, [paragraph, offset]), + returnValueForMissingStub: null, + ); + + @override + void drawPoints( + _i2.PointMode? pointMode, + List<_i2.Offset>? points, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawPoints, [pointMode, points, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRawPoints( + _i2.PointMode? pointMode, + _i5.Float32List? points, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawRawPoints, [pointMode, points, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawVertices( + _i2.Vertices? vertices, + _i2.BlendMode? blendMode, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawVertices, [vertices, blendMode, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawAtlas( + _i2.Image? atlas, + List<_i2.RSTransform>? transforms, + List<_i2.Rect>? rects, + List<_i2.Color>? colors, + _i2.BlendMode? blendMode, + _i2.Rect? cullRect, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawAtlas, [ + atlas, + transforms, + rects, + colors, + blendMode, + cullRect, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawRawAtlas( + _i2.Image? atlas, + _i5.Float32List? rstTransforms, + _i5.Float32List? rects, + _i5.Int32List? colors, + _i2.BlendMode? blendMode, + _i2.Rect? cullRect, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawRawAtlas, [ + atlas, + rstTransforms, + rects, + colors, + blendMode, + cullRect, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawShadow( + _i2.Path? path, + _i2.Color? color, + double? elevation, + bool? transparentOccluder, + ) => super.noSuchMethod( + Invocation.method(#drawShadow, [ + path, + color, + elevation, + transparentOccluder, + ]), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [CanvasWrapper]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCanvasWrapper extends _i1.Mock implements _i6.CanvasWrapper { + MockCanvasWrapper() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.Canvas get canvas => + (super.noSuchMethod( + Invocation.getter(#canvas), + returnValue: _FakeCanvas_1(this, Invocation.getter(#canvas)), + ) + as _i2.Canvas); + + @override + _i2.Size get size => + (super.noSuchMethod( + Invocation.getter(#size), + returnValue: _FakeSize_2(this, Invocation.getter(#size)), + ) + as _i2.Size); + + @override + void drawRRect(_i2.RRect? rrect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRRect, [rrect, paint]), + returnValueForMissingStub: null, + ); + + @override + void save() => super.noSuchMethod( + Invocation.method(#save, []), + returnValueForMissingStub: null, + ); + + @override + void restore() => super.noSuchMethod( + Invocation.method(#restore, []), + returnValueForMissingStub: null, + ); + + @override + void clipRect( + _i2.Rect? rect, { + _i2.ClipOp? clipOp = _i2.ClipOp.intersect, + bool? doAntiAlias = true, + }) => super.noSuchMethod( + Invocation.method( + #clipRect, + [rect], + {#clipOp: clipOp, #doAntiAlias: doAntiAlias}, + ), + returnValueForMissingStub: null, + ); + + @override + void translate(double? dx, double? dy) => super.noSuchMethod( + Invocation.method(#translate, [dx, dy]), + returnValueForMissingStub: null, + ); + + @override + void rotate(double? radius) => super.noSuchMethod( + Invocation.method(#rotate, [radius]), + returnValueForMissingStub: null, + ); + + @override + void drawPath(_i2.Path? path, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawPath, [path, paint]), + returnValueForMissingStub: null, + ); + + @override + void saveLayer(_i2.Rect? bounds, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#saveLayer, [bounds, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawPicture(_i2.Picture? picture) => super.noSuchMethod( + Invocation.method(#drawPicture, [picture]), + returnValueForMissingStub: null, + ); + + @override + void drawImage(_i2.Image? image, _i2.Offset? offset, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawImage, [image, offset, paint]), + returnValueForMissingStub: null, + ); + + @override + void clipPath(_i2.Path? path, {bool? doAntiAlias = true}) => + super.noSuchMethod( + Invocation.method(#clipPath, [path], {#doAntiAlias: doAntiAlias}), + returnValueForMissingStub: null, + ); + + @override + void drawRect(_i2.Rect? rect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRect, [rect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawLine(_i2.Offset? p1, _i2.Offset? p2, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawLine, [p1, p2, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawCircle(_i2.Offset? center, double? radius, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawCircle, [center, radius, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawArc( + _i2.Rect? rect, + double? startAngle, + double? sweepAngle, + bool? useCenter, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawArc, [ + rect, + startAngle, + sweepAngle, + useCenter, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawText( + _i3.TextPainter? tp, + _i2.Offset? offset, [ + double? rotateAngle, + ]) => super.noSuchMethod( + Invocation.method(#drawText, [tp, offset, rotateAngle]), + returnValueForMissingStub: null, + ); + + @override + void drawVerticalText(_i3.TextPainter? tp, _i2.Offset? offset) => + super.noSuchMethod( + Invocation.method(#drawVerticalText, [tp, offset]), + returnValueForMissingStub: null, + ); + + @override + void drawDot( + _i7.FlDotPainter? painter, + _i7.FlSpot? spot, + _i2.Offset? offset, + ) => super.noSuchMethod( + Invocation.method(#drawDot, [painter, spot, offset]), + returnValueForMissingStub: null, + ); + + @override + void drawErrorIndicator( + _i7.FlSpotErrorRangePainter? painter, + _i7.FlSpot? origin, + _i2.Offset? offset, + _i2.Rect? errorRelativeRect, + _i7.AxisChartData? axisData, + ) => super.noSuchMethod( + Invocation.method(#drawErrorIndicator, [ + painter, + origin, + offset, + errorRelativeRect, + axisData, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawRotated({ + required _i2.Size? size, + _i2.Offset? rotationOffset = _i2.Offset.zero, + _i2.Offset? drawOffset = _i2.Offset.zero, + required double? angle, + required _i6.DrawCallback? drawCallback, + }) => super.noSuchMethod( + Invocation.method(#drawRotated, [], { + #size: size, + #rotationOffset: rotationOffset, + #drawOffset: drawOffset, + #angle: angle, + #drawCallback: drawCallback, + }), + returnValueForMissingStub: null, + ); + + @override + void drawDashedLine( + _i2.Offset? from, + _i2.Offset? to, + _i2.Paint? painter, + List? dashArray, + ) => super.noSuchMethod( + Invocation.method(#drawDashedLine, [from, to, painter, dashArray]), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [BuildContext]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockBuildContext extends _i1.Mock implements _i3.BuildContext { + MockBuildContext() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Widget get widget => + (super.noSuchMethod( + Invocation.getter(#widget), + returnValue: _FakeWidget_3(this, Invocation.getter(#widget)), + ) + as _i3.Widget); + + @override + bool get mounted => + (super.noSuchMethod(Invocation.getter(#mounted), returnValue: false) + as bool); + + @override + bool get debugDoingBuild => + (super.noSuchMethod( + Invocation.getter(#debugDoingBuild), + returnValue: false, + ) + as bool); + + @override + _i3.InheritedWidget dependOnInheritedElement( + _i3.InheritedElement? ancestor, { + Object? aspect, + }) => + (super.noSuchMethod( + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + returnValue: _FakeInheritedWidget_4( + this, + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + ), + ) + as _i3.InheritedWidget); + + @override + void visitAncestorElements(_i3.ConditionalElementVisitor? visitor) => + super.noSuchMethod( + Invocation.method(#visitAncestorElements, [visitor]), + returnValueForMissingStub: null, + ); + + @override + void visitChildElements(_i3.ElementVisitor? visitor) => super.noSuchMethod( + Invocation.method(#visitChildElements, [visitor]), + returnValueForMissingStub: null, + ); + + @override + void dispatchNotification(_i3.Notification? notification) => + super.noSuchMethod( + Invocation.method(#dispatchNotification, [notification]), + returnValueForMissingStub: null, + ); + + @override + _i3.DiagnosticsNode describeElement( + String? name, { + _i4.DiagnosticsTreeStyle? style = _i4.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method(#describeElement, [name], {#style: style}), + returnValue: _FakeDiagnosticsNode_5( + this, + Invocation.method(#describeElement, [name], {#style: style}), + ), + ) + as _i3.DiagnosticsNode); + + @override + _i3.DiagnosticsNode describeWidget( + String? name, { + _i4.DiagnosticsTreeStyle? style = _i4.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method(#describeWidget, [name], {#style: style}), + returnValue: _FakeDiagnosticsNode_5( + this, + Invocation.method(#describeWidget, [name], {#style: style}), + ), + ) + as _i3.DiagnosticsNode); + + @override + List<_i3.DiagnosticsNode> describeMissingAncestor({ + required Type? expectedAncestorType, + }) => + (super.noSuchMethod( + Invocation.method(#describeMissingAncestor, [], { + #expectedAncestorType: expectedAncestorType, + }), + returnValue: <_i3.DiagnosticsNode>[], + ) + as List<_i3.DiagnosticsNode>); + + @override + _i3.DiagnosticsNode describeOwnershipChain(String? name) => + (super.noSuchMethod( + Invocation.method(#describeOwnershipChain, [name]), + returnValue: _FakeDiagnosticsNode_5( + this, + Invocation.method(#describeOwnershipChain, [name]), + ), + ) + as _i3.DiagnosticsNode); +} + +/// A class which mocks [Utils]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockUtils extends _i1.Mock implements _i8.Utils { + MockUtils() { + _i1.throwOnMissingStub(this); + } + + @override + double radians(double? degrees) => + (super.noSuchMethod( + Invocation.method(#radians, [degrees]), + returnValue: 0.0, + ) + as double); + + @override + double degrees(double? radians) => + (super.noSuchMethod( + Invocation.method(#degrees, [radians]), + returnValue: 0.0, + ) + as double); + + @override + double translateRotatedPosition(double? size, double? degree) => + (super.noSuchMethod( + Invocation.method(#translateRotatedPosition, [size, degree]), + returnValue: 0.0, + ) + as double); + + @override + _i2.Offset calculateRotationOffset(_i2.Size? size, double? degree) => + (super.noSuchMethod( + Invocation.method(#calculateRotationOffset, [size, degree]), + returnValue: _FakeOffset_6( + this, + Invocation.method(#calculateRotationOffset, [size, degree]), + ), + ) + as _i2.Offset); + + @override + _i3.BorderRadius? normalizeBorderRadius( + _i3.BorderRadius? borderRadius, + double? width, + ) => + (super.noSuchMethod( + Invocation.method(#normalizeBorderRadius, [borderRadius, width]), + ) + as _i3.BorderRadius?); + + @override + _i3.BorderSide normalizeBorderSide( + _i3.BorderSide? borderSide, + double? width, + ) => + (super.noSuchMethod( + Invocation.method(#normalizeBorderSide, [borderSide, width]), + returnValue: _FakeBorderSide_7( + this, + Invocation.method(#normalizeBorderSide, [borderSide, width]), + ), + ) + as _i3.BorderSide); + + @override + double getEfficientInterval( + double? axisViewSize, + double? diffInAxis, { + double? pixelPerInterval = 40.0, + }) => + (super.noSuchMethod( + Invocation.method( + #getEfficientInterval, + [axisViewSize, diffInAxis], + {#pixelPerInterval: pixelPerInterval}, + ), + returnValue: 0.0, + ) + as double); + + @override + double roundInterval(double? input) => + (super.noSuchMethod( + Invocation.method(#roundInterval, [input]), + returnValue: 0.0, + ) + as double); + + @override + int getFractionDigits(double? value) => + (super.noSuchMethod( + Invocation.method(#getFractionDigits, [value]), + returnValue: 0, + ) + as int); + + @override + String formatNumber(double? axisMin, double? axisMax, double? axisValue) => + (super.noSuchMethod( + Invocation.method(#formatNumber, [axisMin, axisMax, axisValue]), + returnValue: _i9.dummyValue( + this, + Invocation.method(#formatNumber, [axisMin, axisMax, axisValue]), + ), + ) + as String); + + @override + _i3.TextStyle getThemeAwareTextStyle( + _i3.BuildContext? context, + _i3.TextStyle? providedStyle, + ) => + (super.noSuchMethod( + Invocation.method(#getThemeAwareTextStyle, [ + context, + providedStyle, + ]), + returnValue: _FakeTextStyle_8( + this, + Invocation.method(#getThemeAwareTextStyle, [ + context, + providedStyle, + ]), + ), + ) + as _i3.TextStyle); + + @override + double getBestInitialIntervalValue( + double? min, + double? max, + double? interval, { + double? baseline = 0.0, + }) => + (super.noSuchMethod( + Invocation.method( + #getBestInitialIntervalValue, + [min, max, interval], + {#baseline: baseline}, + ), + returnValue: 0.0, + ) + as double); + + @override + double convertRadiusToSigma(double? radius) => + (super.noSuchMethod( + Invocation.method(#convertRadiusToSigma, [radius]), + returnValue: 0.0, + ) + as double); +} diff --git a/test/chart/scatter_chart/scatter_chart_renderer_test.dart b/test/chart/scatter_chart/scatter_chart_renderer_test.dart new file mode 100644 index 0000000..8188db4 --- /dev/null +++ b/test/chart/scatter_chart/scatter_chart_renderer_test.dart @@ -0,0 +1,146 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart'; +import 'package:fl_chart/src/chart/scatter_chart/scatter_chart_painter.dart'; +import 'package:fl_chart/src/chart/scatter_chart/scatter_chart_renderer.dart'; +import 'package:fl_chart/src/utils/canvas_wrapper.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import '../data_pool.dart'; +import 'scatter_chart_renderer_test.mocks.dart'; + +@GenerateMocks([Canvas, PaintingContext, BuildContext, ScatterChartPainter]) +void main() { + group('ScatterChartRenderer', () { + final data = ScatterChartData( + scatterSpots: [MockData.scatterSpot1, MockData.scatterSpot2], + ); + + final targetData = ScatterChartData(scatterSpots: [MockData.scatterSpot3]); + + const textScaler = TextScaler.linear(4); + + final mockBuildContext = MockBuildContext(); + final renderScatterChart = RenderScatterChart( + mockBuildContext, + data, + targetData, + textScaler, + null, + canBeScaled: false, + ); + + final mockPainter = MockScatterChartPainter(); + final mockPaintingContext = MockPaintingContext(); + final mockCanvas = MockCanvas(); + const mockSize = Size(44, 44); + when(mockPaintingContext.canvas).thenAnswer((realInvocation) => mockCanvas); + renderScatterChart + ..mockTestSize = mockSize + ..painter = mockPainter; + + test('test 1 correct data set', () { + expect(renderScatterChart.data == data, true); + expect(renderScatterChart.data == targetData, false); + expect(renderScatterChart.targetData == targetData, true); + expect(renderScatterChart.textScaler == textScaler, true); + expect(renderScatterChart.paintHolder.data == data, true); + expect(renderScatterChart.paintHolder.targetData == targetData, true); + expect(renderScatterChart.paintHolder.textScaler == textScaler, true); + }); + + test('test 2 check paint function', () { + renderScatterChart.paint(mockPaintingContext, const Offset(10, 10)); + verify(mockCanvas.save()).called(1); + verify(mockCanvas.translate(10, 10)).called(1); + final result = verify(mockPainter.paint(any, captureAny, captureAny)); + expect(result.callCount, 1); + + final canvasWrapper = result.captured[0] as CanvasWrapper; + expect(canvasWrapper.size, const Size(44, 44)); + expect(canvasWrapper.canvas, mockCanvas); + + final paintHolder = result.captured[1] as PaintHolder; + expect(paintHolder.data, data); + expect(paintHolder.targetData, targetData); + expect(paintHolder.textScaler, textScaler); + + verify(mockCanvas.restore()).called(1); + }); + + test('test 3 check getResponseAtLocation function', () { + final results = >[]; + when(mockPainter.handleTouch(captureAny, captureAny, captureAny)) + .thenAnswer((inv) { + results.add({ + 'local_position': inv.positionalArguments[0] as Offset, + 'size': inv.positionalArguments[1] as Size, + 'paint_holder': inv.positionalArguments[2] as PaintHolder, + }); + return MockData.scatterTouchedSpot; + }); + when(mockPainter.getChartCoordinateFromPixel(any, any, any)) + .thenAnswer((_) => const Offset(10, 10)); + final touchResponse = + renderScatterChart.getResponseAtLocation(MockData.offset1); + expect(touchResponse.touchedSpot, MockData.scatterTouchedSpot); + expect(touchResponse.touchChartCoordinate, const Offset(10, 10)); + expect(results[0]['local_position'] as Offset, MockData.offset1); + expect(results[0]['size'] as Size, mockSize); + final paintHolder = results[0]['paint_holder'] as PaintHolder; + expect(paintHolder.data, data); + expect(paintHolder.targetData, targetData); + expect(paintHolder.textScaler, textScaler); + }); + + test('test 4 check setters', () { + renderScatterChart + ..data = targetData + ..targetData = data + ..textScaler = const TextScaler.linear(22); + + expect(renderScatterChart.data, targetData); + expect(renderScatterChart.targetData, data); + expect(renderScatterChart.textScaler, const TextScaler.linear(22)); + }); + + test('passes chart virtual rect to paint holder', () { + final rect1 = Offset.zero & const Size(100, 100); + final renderScatterChart = RenderScatterChart( + mockBuildContext, + data, + targetData, + textScaler, + null, + canBeScaled: false, + ); + + expect(renderScatterChart.chartVirtualRect, isNull); + expect(renderScatterChart.paintHolder.chartVirtualRect, isNull); + + renderScatterChart.chartVirtualRect = rect1; + + expect(renderScatterChart.chartVirtualRect, rect1); + expect(renderScatterChart.paintHolder.chartVirtualRect, rect1); + }); + + test('uses canBeScaled', () { + final renderScatterChart = RenderScatterChart( + mockBuildContext, + data, + targetData, + textScaler, + null, + canBeScaled: false, + ); + + expect(renderScatterChart.canBeScaled, false); + + renderScatterChart.canBeScaled = true; + + expect(renderScatterChart.canBeScaled, true); + }); + }); +} diff --git a/test/chart/scatter_chart/scatter_chart_renderer_test.mocks.dart b/test/chart/scatter_chart/scatter_chart_renderer_test.mocks.dart new file mode 100644 index 0000000..54dca7d --- /dev/null +++ b/test/chart/scatter_chart/scatter_chart_renderer_test.mocks.dart @@ -0,0 +1,1057 @@ +// Mocks generated by Mockito 5.4.5 from annotations +// in fl_chart/test/chart/scatter_chart/scatter_chart_renderer_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:typed_data' as _i7; +import 'dart:ui' as _i2; + +import 'package:fl_chart/fl_chart.dart' as _i13; +import 'package:fl_chart/src/chart/base/base_chart/base_chart_painter.dart' + as _i12; +import 'package:fl_chart/src/chart/scatter_chart/scatter_chart_painter.dart' + as _i10; +import 'package:fl_chart/src/utils/canvas_wrapper.dart' as _i11; +import 'package:flutter/foundation.dart' as _i5; +import 'package:flutter/gestures.dart' as _i8; +import 'package:flutter/material.dart' as _i6; +import 'package:flutter/rendering.dart' as _i3; +import 'package:flutter/src/rendering/layer.dart' as _i4; +import 'package:flutter/src/widgets/notification_listener.dart' as _i9; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeRect_0 extends _i1.SmartFake implements _i2.Rect { + _FakeRect_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeCanvas_1 extends _i1.SmartFake implements _i2.Canvas { + _FakeCanvas_1(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakePaintingContext_2 extends _i1.SmartFake + implements _i3.PaintingContext { + _FakePaintingContext_2(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeColorFilterLayer_3 extends _i1.SmartFake + implements _i4.ColorFilterLayer { + _FakeColorFilterLayer_3(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeOpacityLayer_4 extends _i1.SmartFake implements _i4.OpacityLayer { + _FakeOpacityLayer_4(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeWidget_5 extends _i1.SmartFake implements _i6.Widget { + _FakeWidget_5(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeInheritedWidget_6 extends _i1.SmartFake + implements _i6.InheritedWidget { + _FakeInheritedWidget_6(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeDiagnosticsNode_7 extends _i1.SmartFake + implements _i5.DiagnosticsNode { + _FakeDiagnosticsNode_7(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({ + _i5.TextTreeConfiguration? parentConfiguration, + _i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info, + }) => super.toString(); +} + +class _FakeOffset_8 extends _i1.SmartFake implements _i2.Offset { + _FakeOffset_8(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +/// A class which mocks [Canvas]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCanvas extends _i1.Mock implements _i2.Canvas { + MockCanvas() { + _i1.throwOnMissingStub(this); + } + + @override + void save() => super.noSuchMethod( + Invocation.method(#save, []), + returnValueForMissingStub: null, + ); + + @override + void saveLayer(_i2.Rect? bounds, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#saveLayer, [bounds, paint]), + returnValueForMissingStub: null, + ); + + @override + void restore() => super.noSuchMethod( + Invocation.method(#restore, []), + returnValueForMissingStub: null, + ); + + @override + void restoreToCount(int? count) => super.noSuchMethod( + Invocation.method(#restoreToCount, [count]), + returnValueForMissingStub: null, + ); + + @override + int getSaveCount() => + (super.noSuchMethod(Invocation.method(#getSaveCount, []), returnValue: 0) + as int); + + @override + void translate(double? dx, double? dy) => super.noSuchMethod( + Invocation.method(#translate, [dx, dy]), + returnValueForMissingStub: null, + ); + + @override + void scale(double? sx, [double? sy]) => super.noSuchMethod( + Invocation.method(#scale, [sx, sy]), + returnValueForMissingStub: null, + ); + + @override + void rotate(double? radians) => super.noSuchMethod( + Invocation.method(#rotate, [radians]), + returnValueForMissingStub: null, + ); + + @override + void skew(double? sx, double? sy) => super.noSuchMethod( + Invocation.method(#skew, [sx, sy]), + returnValueForMissingStub: null, + ); + + @override + void transform(_i7.Float64List? matrix4) => super.noSuchMethod( + Invocation.method(#transform, [matrix4]), + returnValueForMissingStub: null, + ); + + @override + _i7.Float64List getTransform() => + (super.noSuchMethod( + Invocation.method(#getTransform, []), + returnValue: _i7.Float64List(0), + ) + as _i7.Float64List); + + @override + void clipRect( + _i2.Rect? rect, { + _i2.ClipOp? clipOp = _i2.ClipOp.intersect, + bool? doAntiAlias = true, + }) => super.noSuchMethod( + Invocation.method( + #clipRect, + [rect], + {#clipOp: clipOp, #doAntiAlias: doAntiAlias}, + ), + returnValueForMissingStub: null, + ); + + @override + void clipRRect(_i2.RRect? rrect, {bool? doAntiAlias = true}) => + super.noSuchMethod( + Invocation.method(#clipRRect, [rrect], {#doAntiAlias: doAntiAlias}), + returnValueForMissingStub: null, + ); + + @override + void clipPath(_i2.Path? path, {bool? doAntiAlias = true}) => + super.noSuchMethod( + Invocation.method(#clipPath, [path], {#doAntiAlias: doAntiAlias}), + returnValueForMissingStub: null, + ); + + @override + _i2.Rect getLocalClipBounds() => + (super.noSuchMethod( + Invocation.method(#getLocalClipBounds, []), + returnValue: _FakeRect_0( + this, + Invocation.method(#getLocalClipBounds, []), + ), + ) + as _i2.Rect); + + @override + _i2.Rect getDestinationClipBounds() => + (super.noSuchMethod( + Invocation.method(#getDestinationClipBounds, []), + returnValue: _FakeRect_0( + this, + Invocation.method(#getDestinationClipBounds, []), + ), + ) + as _i2.Rect); + + @override + void drawColor(_i2.Color? color, _i2.BlendMode? blendMode) => + super.noSuchMethod( + Invocation.method(#drawColor, [color, blendMode]), + returnValueForMissingStub: null, + ); + + @override + void drawLine(_i2.Offset? p1, _i2.Offset? p2, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawLine, [p1, p2, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawPaint(_i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawPaint, [paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRect(_i2.Rect? rect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRect, [rect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRRect(_i2.RRect? rrect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRRect, [rrect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawDRRect(_i2.RRect? outer, _i2.RRect? inner, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawDRRect, [outer, inner, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawOval(_i2.Rect? rect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawOval, [rect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawCircle(_i2.Offset? c, double? radius, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawCircle, [c, radius, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawArc( + _i2.Rect? rect, + double? startAngle, + double? sweepAngle, + bool? useCenter, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawArc, [ + rect, + startAngle, + sweepAngle, + useCenter, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawPath(_i2.Path? path, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawPath, [path, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImage(_i2.Image? image, _i2.Offset? offset, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawImage, [image, offset, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImageRect( + _i2.Image? image, + _i2.Rect? src, + _i2.Rect? dst, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawImageRect, [image, src, dst, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImageNine( + _i2.Image? image, + _i2.Rect? center, + _i2.Rect? dst, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawImageNine, [image, center, dst, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawPicture(_i2.Picture? picture) => super.noSuchMethod( + Invocation.method(#drawPicture, [picture]), + returnValueForMissingStub: null, + ); + + @override + void drawParagraph(_i2.Paragraph? paragraph, _i2.Offset? offset) => + super.noSuchMethod( + Invocation.method(#drawParagraph, [paragraph, offset]), + returnValueForMissingStub: null, + ); + + @override + void drawPoints( + _i2.PointMode? pointMode, + List<_i2.Offset>? points, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawPoints, [pointMode, points, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRawPoints( + _i2.PointMode? pointMode, + _i7.Float32List? points, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawRawPoints, [pointMode, points, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawVertices( + _i2.Vertices? vertices, + _i2.BlendMode? blendMode, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawVertices, [vertices, blendMode, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawAtlas( + _i2.Image? atlas, + List<_i2.RSTransform>? transforms, + List<_i2.Rect>? rects, + List<_i2.Color>? colors, + _i2.BlendMode? blendMode, + _i2.Rect? cullRect, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawAtlas, [ + atlas, + transforms, + rects, + colors, + blendMode, + cullRect, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawRawAtlas( + _i2.Image? atlas, + _i7.Float32List? rstTransforms, + _i7.Float32List? rects, + _i7.Int32List? colors, + _i2.BlendMode? blendMode, + _i2.Rect? cullRect, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawRawAtlas, [ + atlas, + rstTransforms, + rects, + colors, + blendMode, + cullRect, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawShadow( + _i2.Path? path, + _i2.Color? color, + double? elevation, + bool? transparentOccluder, + ) => super.noSuchMethod( + Invocation.method(#drawShadow, [ + path, + color, + elevation, + transparentOccluder, + ]), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [PaintingContext]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPaintingContext extends _i1.Mock implements _i3.PaintingContext { + MockPaintingContext() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.Rect get estimatedBounds => + (super.noSuchMethod( + Invocation.getter(#estimatedBounds), + returnValue: _FakeRect_0(this, Invocation.getter(#estimatedBounds)), + ) + as _i2.Rect); + + @override + _i2.Canvas get canvas => + (super.noSuchMethod( + Invocation.getter(#canvas), + returnValue: _FakeCanvas_1(this, Invocation.getter(#canvas)), + ) + as _i2.Canvas); + + @override + void paintChild(_i3.RenderObject? child, _i2.Offset? offset) => + super.noSuchMethod( + Invocation.method(#paintChild, [child, offset]), + returnValueForMissingStub: null, + ); + + @override + void appendLayer(_i4.Layer? layer) => super.noSuchMethod( + Invocation.method(#appendLayer, [layer]), + returnValueForMissingStub: null, + ); + + @override + _i2.VoidCallback addCompositionCallback(_i4.CompositionCallback? callback) => + (super.noSuchMethod( + Invocation.method(#addCompositionCallback, [callback]), + returnValue: () {}, + ) + as _i2.VoidCallback); + + @override + void stopRecordingIfNeeded() => super.noSuchMethod( + Invocation.method(#stopRecordingIfNeeded, []), + returnValueForMissingStub: null, + ); + + @override + void setIsComplexHint() => super.noSuchMethod( + Invocation.method(#setIsComplexHint, []), + returnValueForMissingStub: null, + ); + + @override + void setWillChangeHint() => super.noSuchMethod( + Invocation.method(#setWillChangeHint, []), + returnValueForMissingStub: null, + ); + + @override + void addLayer(_i4.Layer? layer) => super.noSuchMethod( + Invocation.method(#addLayer, [layer]), + returnValueForMissingStub: null, + ); + + @override + void pushLayer( + _i4.ContainerLayer? childLayer, + _i3.PaintingContextCallback? painter, + _i2.Offset? offset, { + _i2.Rect? childPaintBounds, + }) => super.noSuchMethod( + Invocation.method( + #pushLayer, + [childLayer, painter, offset], + {#childPaintBounds: childPaintBounds}, + ), + returnValueForMissingStub: null, + ); + + @override + _i3.PaintingContext createChildContext( + _i4.ContainerLayer? childLayer, + _i2.Rect? bounds, + ) => + (super.noSuchMethod( + Invocation.method(#createChildContext, [childLayer, bounds]), + returnValue: _FakePaintingContext_2( + this, + Invocation.method(#createChildContext, [childLayer, bounds]), + ), + ) + as _i3.PaintingContext); + + @override + _i4.ClipRectLayer? pushClipRect( + bool? needsCompositing, + _i2.Offset? offset, + _i2.Rect? clipRect, + _i3.PaintingContextCallback? painter, { + _i2.Clip? clipBehavior = _i2.Clip.hardEdge, + _i4.ClipRectLayer? oldLayer, + }) => + (super.noSuchMethod( + Invocation.method( + #pushClipRect, + [needsCompositing, offset, clipRect, painter], + {#clipBehavior: clipBehavior, #oldLayer: oldLayer}, + ), + ) + as _i4.ClipRectLayer?); + + @override + _i4.ClipRRectLayer? pushClipRRect( + bool? needsCompositing, + _i2.Offset? offset, + _i2.Rect? bounds, + _i2.RRect? clipRRect, + _i3.PaintingContextCallback? painter, { + _i2.Clip? clipBehavior = _i2.Clip.antiAlias, + _i4.ClipRRectLayer? oldLayer, + }) => + (super.noSuchMethod( + Invocation.method( + #pushClipRRect, + [needsCompositing, offset, bounds, clipRRect, painter], + {#clipBehavior: clipBehavior, #oldLayer: oldLayer}, + ), + ) + as _i4.ClipRRectLayer?); + + @override + _i4.ClipPathLayer? pushClipPath( + bool? needsCompositing, + _i2.Offset? offset, + _i2.Rect? bounds, + _i2.Path? clipPath, + _i3.PaintingContextCallback? painter, { + _i2.Clip? clipBehavior = _i2.Clip.antiAlias, + _i4.ClipPathLayer? oldLayer, + }) => + (super.noSuchMethod( + Invocation.method( + #pushClipPath, + [needsCompositing, offset, bounds, clipPath, painter], + {#clipBehavior: clipBehavior, #oldLayer: oldLayer}, + ), + ) + as _i4.ClipPathLayer?); + + @override + _i4.ColorFilterLayer pushColorFilter( + _i2.Offset? offset, + _i2.ColorFilter? colorFilter, + _i3.PaintingContextCallback? painter, { + _i4.ColorFilterLayer? oldLayer, + }) => + (super.noSuchMethod( + Invocation.method( + #pushColorFilter, + [offset, colorFilter, painter], + {#oldLayer: oldLayer}, + ), + returnValue: _FakeColorFilterLayer_3( + this, + Invocation.method( + #pushColorFilter, + [offset, colorFilter, painter], + {#oldLayer: oldLayer}, + ), + ), + ) + as _i4.ColorFilterLayer); + + @override + _i4.TransformLayer? pushTransform( + bool? needsCompositing, + _i2.Offset? offset, + _i8.Matrix4? transform, + _i3.PaintingContextCallback? painter, { + _i4.TransformLayer? oldLayer, + }) => + (super.noSuchMethod( + Invocation.method( + #pushTransform, + [needsCompositing, offset, transform, painter], + {#oldLayer: oldLayer}, + ), + ) + as _i4.TransformLayer?); + + @override + _i4.OpacityLayer pushOpacity( + _i2.Offset? offset, + int? alpha, + _i3.PaintingContextCallback? painter, { + _i4.OpacityLayer? oldLayer, + }) => + (super.noSuchMethod( + Invocation.method( + #pushOpacity, + [offset, alpha, painter], + {#oldLayer: oldLayer}, + ), + returnValue: _FakeOpacityLayer_4( + this, + Invocation.method( + #pushOpacity, + [offset, alpha, painter], + {#oldLayer: oldLayer}, + ), + ), + ) + as _i4.OpacityLayer); + + @override + void clipPathAndPaint( + _i2.Path? path, + _i2.Clip? clipBehavior, + _i2.Rect? bounds, + _i2.VoidCallback? painter, + ) => super.noSuchMethod( + Invocation.method(#clipPathAndPaint, [path, clipBehavior, bounds, painter]), + returnValueForMissingStub: null, + ); + + @override + void clipRRectAndPaint( + _i2.RRect? rrect, + _i2.Clip? clipBehavior, + _i2.Rect? bounds, + _i2.VoidCallback? painter, + ) => super.noSuchMethod( + Invocation.method(#clipRRectAndPaint, [ + rrect, + clipBehavior, + bounds, + painter, + ]), + returnValueForMissingStub: null, + ); + + @override + void clipRectAndPaint( + _i2.Rect? rect, + _i2.Clip? clipBehavior, + _i2.Rect? bounds, + _i2.VoidCallback? painter, + ) => super.noSuchMethod( + Invocation.method(#clipRectAndPaint, [rect, clipBehavior, bounds, painter]), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [BuildContext]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockBuildContext extends _i1.Mock implements _i6.BuildContext { + MockBuildContext() { + _i1.throwOnMissingStub(this); + } + + @override + _i6.Widget get widget => + (super.noSuchMethod( + Invocation.getter(#widget), + returnValue: _FakeWidget_5(this, Invocation.getter(#widget)), + ) + as _i6.Widget); + + @override + bool get mounted => + (super.noSuchMethod(Invocation.getter(#mounted), returnValue: false) + as bool); + + @override + bool get debugDoingBuild => + (super.noSuchMethod( + Invocation.getter(#debugDoingBuild), + returnValue: false, + ) + as bool); + + @override + _i6.InheritedWidget dependOnInheritedElement( + _i6.InheritedElement? ancestor, { + Object? aspect, + }) => + (super.noSuchMethod( + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + returnValue: _FakeInheritedWidget_6( + this, + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + ), + ) + as _i6.InheritedWidget); + + @override + void visitAncestorElements(_i6.ConditionalElementVisitor? visitor) => + super.noSuchMethod( + Invocation.method(#visitAncestorElements, [visitor]), + returnValueForMissingStub: null, + ); + + @override + void visitChildElements(_i6.ElementVisitor? visitor) => super.noSuchMethod( + Invocation.method(#visitChildElements, [visitor]), + returnValueForMissingStub: null, + ); + + @override + void dispatchNotification(_i9.Notification? notification) => + super.noSuchMethod( + Invocation.method(#dispatchNotification, [notification]), + returnValueForMissingStub: null, + ); + + @override + _i5.DiagnosticsNode describeElement( + String? name, { + _i5.DiagnosticsTreeStyle? style = _i5.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method(#describeElement, [name], {#style: style}), + returnValue: _FakeDiagnosticsNode_7( + this, + Invocation.method(#describeElement, [name], {#style: style}), + ), + ) + as _i5.DiagnosticsNode); + + @override + _i5.DiagnosticsNode describeWidget( + String? name, { + _i5.DiagnosticsTreeStyle? style = _i5.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method(#describeWidget, [name], {#style: style}), + returnValue: _FakeDiagnosticsNode_7( + this, + Invocation.method(#describeWidget, [name], {#style: style}), + ), + ) + as _i5.DiagnosticsNode); + + @override + List<_i5.DiagnosticsNode> describeMissingAncestor({ + required Type? expectedAncestorType, + }) => + (super.noSuchMethod( + Invocation.method(#describeMissingAncestor, [], { + #expectedAncestorType: expectedAncestorType, + }), + returnValue: <_i5.DiagnosticsNode>[], + ) + as List<_i5.DiagnosticsNode>); + + @override + _i5.DiagnosticsNode describeOwnershipChain(String? name) => + (super.noSuchMethod( + Invocation.method(#describeOwnershipChain, [name]), + returnValue: _FakeDiagnosticsNode_7( + this, + Invocation.method(#describeOwnershipChain, [name]), + ), + ) + as _i5.DiagnosticsNode); +} + +/// A class which mocks [ScatterChartPainter]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockScatterChartPainter extends _i1.Mock + implements _i10.ScatterChartPainter { + MockScatterChartPainter() { + _i1.throwOnMissingStub(this); + } + + @override + void paint( + _i6.BuildContext? context, + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.ScatterChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#paint, [context, canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawSpots( + _i6.BuildContext? context, + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.ScatterChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawSpots, [context, canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawScatterErrorBars( + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.ScatterChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawScatterErrorBars, [canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawTouchTooltips( + _i6.BuildContext? context, + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.ScatterChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawTouchTooltips, [context, canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawTouchTooltip( + _i6.BuildContext? context, + _i11.CanvasWrapper? canvasWrapper, + _i13.ScatterTouchTooltipData? tooltipData, + _i13.ScatterSpot? showOnSpot, + _i12.PaintHolder<_i13.ScatterChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawTouchTooltip, [ + context, + canvasWrapper, + tooltipData, + showOnSpot, + holder, + ]), + returnValueForMissingStub: null, + ); + + @override + _i13.ScatterTouchedSpot? handleTouch( + _i2.Offset? localPosition, + _i2.Size? viewSize, + _i12.PaintHolder<_i13.ScatterChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#handleTouch, [localPosition, viewSize, holder]), + ) + as _i13.ScatterTouchedSpot?); + + @override + void drawGrid( + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.ScatterChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawGrid, [canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawBackground( + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.ScatterChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawBackground, [canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawRangeAnnotation( + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.ScatterChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawRangeAnnotation, [canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawExtraLines( + _i6.BuildContext? context, + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.ScatterChartData>? holder, + ) => super.noSuchMethod( + Invocation.method(#drawExtraLines, [context, canvasWrapper, holder]), + returnValueForMissingStub: null, + ); + + @override + void drawHorizontalLines( + _i6.BuildContext? context, + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.ScatterChartData>? holder, + _i2.Size? viewSize, + ) => super.noSuchMethod( + Invocation.method(#drawHorizontalLines, [ + context, + canvasWrapper, + holder, + viewSize, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawVerticalLines( + _i6.BuildContext? context, + _i11.CanvasWrapper? canvasWrapper, + _i12.PaintHolder<_i13.ScatterChartData>? holder, + _i2.Size? viewSize, + ) => super.noSuchMethod( + Invocation.method(#drawVerticalLines, [ + context, + canvasWrapper, + holder, + viewSize, + ]), + returnValueForMissingStub: null, + ); + + @override + double getPixelX( + double? spotX, + _i2.Size? viewSize, + _i12.PaintHolder<_i13.ScatterChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#getPixelX, [spotX, viewSize, holder]), + returnValue: 0.0, + ) + as double); + + @override + double getPixelY( + double? spotY, + _i2.Size? viewSize, + _i12.PaintHolder<_i13.ScatterChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#getPixelY, [spotY, viewSize, holder]), + returnValue: 0.0, + ) + as double); + + @override + double getXForPixel( + double? pixelX, + _i2.Size? viewSize, + _i12.PaintHolder<_i13.ScatterChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#getXForPixel, [pixelX, viewSize, holder]), + returnValue: 0.0, + ) + as double); + + @override + double getYForPixel( + double? pixelY, + _i2.Size? viewSize, + _i12.PaintHolder<_i13.ScatterChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#getYForPixel, [pixelY, viewSize, holder]), + returnValue: 0.0, + ) + as double); + + @override + _i2.Offset getChartCoordinateFromPixel( + _i2.Offset? pixelOffset, + _i2.Size? viewSize, + _i12.PaintHolder<_i13.ScatterChartData>? holder, + ) => + (super.noSuchMethod( + Invocation.method(#getChartCoordinateFromPixel, [ + pixelOffset, + viewSize, + holder, + ]), + returnValue: _FakeOffset_8( + this, + Invocation.method(#getChartCoordinateFromPixel, [ + pixelOffset, + viewSize, + holder, + ]), + ), + ) + as _i2.Offset); + + @override + double getTooltipLeft( + double? dx, + double? tooltipWidth, + _i13.FLHorizontalAlignment? tooltipHorizontalAlignment, + double? tooltipHorizontalOffset, + ) => + (super.noSuchMethod( + Invocation.method(#getTooltipLeft, [ + dx, + tooltipWidth, + tooltipHorizontalAlignment, + tooltipHorizontalOffset, + ]), + returnValue: 0.0, + ) + as double); +} diff --git a/test/chart/scatter_chart/scatter_chart_test.dart b/test/chart/scatter_chart/scatter_chart_test.dart new file mode 100644 index 0000000..184d252 --- /dev/null +++ b/test/chart/scatter_chart/scatter_chart_test.dart @@ -0,0 +1,831 @@ +import 'package:fl_chart/src/chart/base/axis_chart/axis_chart_scaffold_widget.dart'; +import 'package:fl_chart/src/chart/base/axis_chart/scale_axis.dart'; +import 'package:fl_chart/src/chart/base/axis_chart/transformation_config.dart'; +import 'package:fl_chart/src/chart/scatter_chart/scatter_chart.dart'; +import 'package:fl_chart/src/chart/scatter_chart/scatter_chart_data.dart'; +import 'package:fl_chart/src/chart/scatter_chart/scatter_chart_renderer.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + Widget createTestWidget({ + required ScatterChart chart, + }) { + return MaterialApp( + home: chart, + ); + } + + group('ScatterChart', () { + testWidgets('has correct default values', (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: ScatterChart( + ScatterChartData(), + ), + ), + ); + + final scatterChart = tester.widget( + find.byType(ScatterChart), + ); + expect( + scatterChart.transformationConfig, + const FlTransformationConfig(), + ); + }); + + testWidgets('passes interaction parameters to AxisChartScaffoldWidget', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: ScatterChart( + ScatterChartData(), + ), + ), + ); + + final axisChartScaffoldWidget = tester.widget( + find.byType(AxisChartScaffoldWidget), + ); + + expect( + axisChartScaffoldWidget.transformationConfig, + const FlTransformationConfig(), + ); + + await tester.pumpAndSettle(); + + final transformationConfig = FlTransformationConfig( + scaleAxis: FlScaleAxis.free, + trackpadScrollCausesScale: true, + maxScale: 10, + minScale: 1.5, + transformationController: TransformationController(), + ); + await tester.pumpWidget( + createTestWidget( + chart: ScatterChart( + ScatterChartData(), + transformationConfig: transformationConfig, + ), + ), + ); + + final axisChartScaffoldWidget1 = tester.widget( + find.byType(AxisChartScaffoldWidget), + ); + + expect( + axisChartScaffoldWidget1.transformationConfig, + transformationConfig, + ); + }); + + for (final scaleAxis in FlScaleAxis.scalingEnabledAxis) { + testWidgets('passes canBeScaled true for $scaleAxis', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: ScatterChart( + ScatterChartData(), + transformationConfig: FlTransformationConfig( + scaleAxis: scaleAxis, + ), + ), + ), + ); + + final scatterChartLeaf = tester.widget( + find.byType(ScatterChartLeaf), + ); + expect(scatterChartLeaf.canBeScaled, true); + }); + } + + testWidgets('passes canBeScaled false for FlScaleAxis.none', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: ScatterChart( + ScatterChartData(), + // This is for test + // ignore: avoid_redundant_argument_values + transformationConfig: const FlTransformationConfig( + // This is for test + // ignore: avoid_redundant_argument_values + scaleAxis: FlScaleAxis.none, + ), + ), + ), + ); + + final scatterChartLeaf = tester.widget( + find.byType(ScatterChartLeaf), + ); + expect(scatterChartLeaf.canBeScaled, false); + }); + + group('touch gesture', () { + testWidgets('does not scale with FlScaleAxis.none', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: ScatterChart( + ScatterChartData(), + ), + ), + ); + + final scatterChartCenterOffset = + tester.getCenter(find.byType(ScatterChartLeaf)); + final scaleStart1 = scatterChartCenterOffset; + final scaleStart2 = scatterChartCenterOffset; + final scaleEnd1 = scatterChartCenterOffset + const Offset(100, 100); + final scaleEnd2 = scatterChartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await tester.pump(); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await tester.pump(); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final scatterChartLeaf = tester.widget( + find.byType(ScatterChartLeaf), + ); + + expect(scatterChartLeaf.chartVirtualRect, isNull); + }); + + testWidgets('scales freely with FlScaleAxis.free', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: ScatterChart( + ScatterChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.free, + ), + ), + ), + ); + + final scatterChartCenterOffset = + tester.getCenter(find.byType(ScatterChartLeaf)); + final scaleStart1 = scatterChartCenterOffset; + final scaleStart2 = scatterChartCenterOffset; + final scaleEnd1 = scatterChartCenterOffset + const Offset(100, 100); + final scaleEnd2 = scatterChartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await tester.pump(); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await tester.pump(); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final scatterChartLeaf = tester.widget( + find.byType(ScatterChartLeaf), + ); + final renderBox = tester.renderObject( + find.byType(ScatterChartLeaf), + ); + final chartVirtualRect = scatterChartLeaf.chartVirtualRect!; + + expect(chartVirtualRect.size, greaterThan(renderBox.size)); + expect(chartVirtualRect.left, isNegative); + expect(chartVirtualRect.top, isNegative); + }); + + testWidgets('scales horizontally with FlScaleAxis.horizontal', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: ScatterChart( + ScatterChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.horizontal, + ), + ), + ), + ); + + final chartCenterOffset = + tester.getCenter(find.byType(ScatterChartLeaf)); + final scaleStart1 = chartCenterOffset; + final scaleStart2 = chartCenterOffset; + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await tester.pump(); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await tester.pump(); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final scatterChartLeaf = tester.widget( + find.byType(ScatterChartLeaf), + ); + final renderBox = tester.renderObject( + find.byType(ScatterChartLeaf), + ); + final chartVirtualRect = scatterChartLeaf.chartVirtualRect!; + + expect(chartVirtualRect.size.height, renderBox.size.height); + expect(chartVirtualRect.size.width, greaterThan(renderBox.size.width)); + expect(chartVirtualRect.left, isNegative); + expect(chartVirtualRect.top, 0); + }); + + testWidgets('scales vertically with FlScaleAxis.vertical', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: ScatterChart( + ScatterChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.vertical, + ), + ), + ), + ); + + final chartCenterOffset = + tester.getCenter(find.byType(ScatterChartLeaf)); + final scaleStart1 = chartCenterOffset; + final scaleStart2 = chartCenterOffset; + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await tester.pump(); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await tester.pump(); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final scatterChartLeaf = tester.widget( + find.byType(ScatterChartLeaf), + ); + final renderBox = tester.renderObject( + find.byType(ScatterChartLeaf), + ); + final chartVirtualRect = scatterChartLeaf.chartVirtualRect!; + + expect( + chartVirtualRect.size.height, + greaterThan(renderBox.size.height), + ); + expect(chartVirtualRect.size.width, renderBox.size.width); + expect(chartVirtualRect.left, 0); + expect(chartVirtualRect.top, isNegative); + }); + + group('pans', () { + testWidgets('only horizontally with FlScaleAxis.horizontal', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: ScatterChart( + ScatterChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.horizontal, + ), + ), + ), + ); + + final chartCenterOffset = + tester.getCenter(find.byType(ScatterChartLeaf)); + final scaleStart1 = chartCenterOffset; + final scaleStart2 = chartCenterOffset; + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final scatterChartLeafBeforePan = tester.widget( + find.byType(ScatterChartLeaf), + ); + final chartVirtualRectBeforePan = + scatterChartLeafBeforePan.chartVirtualRect!; + expect(chartVirtualRectBeforePan.top, 0); + + const panOffset = Offset(100, 100); + await tester.dragFrom(chartCenterOffset, panOffset); + await tester.pumpAndSettle(); + + final scatterChartLeafAfterPan = tester.widget( + find.byType(ScatterChartLeaf), + ); + final chartVirtualRectAfterPan = + scatterChartLeafAfterPan.chartVirtualRect!; + + expect(chartVirtualRectBeforePan.size, chartVirtualRectAfterPan.size); + expect( + chartVirtualRectAfterPan.left, + greaterThan(chartVirtualRectBeforePan.left), + ); + expect(chartVirtualRectAfterPan.top, 0); + }); + + testWidgets('only vertically with FlScaleAxis.vertical', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: ScatterChart( + ScatterChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.vertical, + ), + ), + ), + ); + + final chartCenterOffset = + tester.getCenter(find.byType(ScatterChartLeaf)); + final scaleStart1 = chartCenterOffset; + final scaleStart2 = chartCenterOffset; + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final scatterChartLeafBeforePan = tester.widget( + find.byType(ScatterChartLeaf), + ); + final chartVirtualRectBeforePan = + scatterChartLeafBeforePan.chartVirtualRect!; + expect(chartVirtualRectBeforePan.left, 0); + + const panOffset = Offset(100, 100); + await tester.dragFrom(chartCenterOffset, panOffset); + await tester.pumpAndSettle(); + + final scatterChartLeafAfterPan = tester.widget( + find.byType(ScatterChartLeaf), + ); + final chartVirtualRectAfterPan = + scatterChartLeafAfterPan.chartVirtualRect!; + + expect(chartVirtualRectAfterPan.left, 0); + expect( + chartVirtualRectAfterPan.top, + greaterThan(chartVirtualRectBeforePan.top), + ); + }); + + testWidgets('freely with FlScaleAxis.free', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: ScatterChart( + ScatterChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.free, + ), + ), + ), + ); + + final chartCenterOffset = + tester.getCenter(find.byType(ScatterChartLeaf)); + final scaleStart1 = chartCenterOffset; + final scaleStart2 = chartCenterOffset; + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final scatterChartLeafBeforePan = tester.widget( + find.byType(ScatterChartLeaf), + ); + final chartVirtualRectBeforePan = + scatterChartLeafBeforePan.chartVirtualRect!; + expect(chartVirtualRectBeforePan.top, isNegative); + expect(chartVirtualRectBeforePan.left, isNegative); + + const panOffset = Offset(100, 100); + await tester.dragFrom(chartCenterOffset, panOffset); + await tester.pumpAndSettle(); + + final scatterChartLeafAfterPan = tester.widget( + find.byType(ScatterChartLeaf), + ); + final chartVirtualRectAfterPan = + scatterChartLeafAfterPan.chartVirtualRect!; + + expect( + chartVirtualRectAfterPan.left, + greaterThan(chartVirtualRectBeforePan.left), + ); + expect( + chartVirtualRectAfterPan.top, + greaterThan(chartVirtualRectBeforePan.top), + ); + }); + }); + }); + + group('trackpad scroll', () { + group('pans', () { + testWidgets('only horizontally with FlScaleAxis.horizontal', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: ScatterChart( + ScatterChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.horizontal, + ), + ), + ), + ); + + final chartCenterOffset = + tester.getCenter(find.byType(ScatterChartLeaf)); + final scaleStart1 = chartCenterOffset; + final scaleStart2 = chartCenterOffset; + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await tester.pump(); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await tester.pump(); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final scatterChartLeafBeforePan = tester.widget( + find.byType(ScatterChartLeaf), + ); + + final chartVirtualRectBeforePan = + scatterChartLeafBeforePan.chartVirtualRect!; + expect(chartVirtualRectBeforePan.top, 0); + + final pointer = TestPointer(1, PointerDeviceKind.trackpad); + const leftAndUp = Offset(-100, -100); + await tester.sendEventToBinding(pointer.hover(chartCenterOffset)); + await tester.pump(); + await tester.sendEventToBinding(pointer.scroll(leftAndUp)); + await tester.pump(); + + final scatterChartLeafAfterPan = tester.widget( + find.byType(ScatterChartLeaf), + ); + final chartVirtualRectAfterPan = + scatterChartLeafAfterPan.chartVirtualRect!; + + expect( + chartVirtualRectAfterPan.left, + greaterThan(chartVirtualRectBeforePan.left), + ); + expect(chartVirtualRectAfterPan.top, 0); + }); + + testWidgets('vertically with FlScaleAxis.vertical', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: ScatterChart( + ScatterChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.vertical, + ), + ), + ), + ); + + final chartCenterOffset = + tester.getCenter(find.byType(ScatterChartLeaf)); + final scaleStart1 = chartCenterOffset; + final scaleStart2 = chartCenterOffset; + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await tester.pump(); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await tester.pump(); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final scatterChartLeafBeforePan = tester.widget( + find.byType(ScatterChartLeaf), + ); + + final chartVirtualRectBeforePan = + scatterChartLeafBeforePan.chartVirtualRect!; + expect(chartVirtualRectBeforePan.left, 0); + + final pointer = TestPointer(1, PointerDeviceKind.trackpad); + const leftAndUp = Offset(-100, -100); + await tester.sendEventToBinding(pointer.hover(chartCenterOffset)); + await tester.pump(); + await tester.sendEventToBinding(pointer.scroll(leftAndUp)); + await tester.pump(); + + final scatterChartLeafAfterPan = tester.widget( + find.byType(ScatterChartLeaf), + ); + final chartVirtualRectAfterPan = + scatterChartLeafAfterPan.chartVirtualRect!; + + expect(chartVirtualRectAfterPan.left, 0); + expect( + chartVirtualRectAfterPan.top, + greaterThan(chartVirtualRectBeforePan.top), + ); + }); + + testWidgets('freely with FlScaleAxis.free', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: ScatterChart( + ScatterChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.free, + ), + ), + ), + ); + + final chartCenterOffset = + tester.getCenter(find.byType(ScatterChartLeaf)); + final scaleStart1 = chartCenterOffset; + final scaleStart2 = chartCenterOffset; + final scaleEnd1 = chartCenterOffset + const Offset(100, 100); + final scaleEnd2 = chartCenterOffset - const Offset(100, 100); + + final gesture1 = await tester.startGesture(scaleStart1); + final gesture2 = await tester.startGesture(scaleStart2); + await tester.pump(); + await gesture1.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await tester.pump(); + await gesture1.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + + final scatterChartLeafBeforePan = tester.widget( + find.byType(ScatterChartLeaf), + ); + + final chartVirtualRectBeforePan = + scatterChartLeafBeforePan.chartVirtualRect!; + + final pointer = TestPointer(1, PointerDeviceKind.trackpad); + const leftAndUp = Offset(-100, -100); + await tester.sendEventToBinding(pointer.hover(chartCenterOffset)); + await tester.pump(); + await tester.sendEventToBinding(pointer.scroll(leftAndUp)); + await tester.pump(); + + final scatterChartLeafAfterPan = tester.widget( + find.byType(ScatterChartLeaf), + ); + final chartVirtualRectAfterPan = + scatterChartLeafAfterPan.chartVirtualRect!; + + expect( + chartVirtualRectAfterPan.left, + greaterThan(chartVirtualRectBeforePan.left), + ); + expect( + chartVirtualRectAfterPan.top, + greaterThan(chartVirtualRectBeforePan.top), + ); + }); + }); + + testWidgets( + 'does not scale with FlScaleAxis.none when ' + 'trackpadScrollCausesScale is true', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: ScatterChart( + ScatterChartData(), + transformationConfig: const FlTransformationConfig( + // This is for test + // ignore: avoid_redundant_argument_values + scaleAxis: FlScaleAxis.none, + trackpadScrollCausesScale: true, + ), + ), + ), + ); + + final pointer = TestPointer(1, PointerDeviceKind.trackpad); + final chartCenterOffset = + tester.getCenter(find.byType(ScatterChartLeaf)); + const scrollAmount = Offset(0, -100); + + await tester.sendEventToBinding(pointer.hover(chartCenterOffset)); + await tester.pump(); + await tester.sendEventToBinding(pointer.scroll(scrollAmount)); + await tester.pump(); + + final scatterChartLeaf = tester.widget( + find.byType(ScatterChartLeaf), + ); + expect(scatterChartLeaf.chartVirtualRect, null); + }, + ); + + for (final scaleAxis in FlScaleAxis.scalingEnabledAxis) { + testWidgets( + 'does not scale when trackpadScrollCausesScale is false ' + 'for $scaleAxis', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: ScatterChart( + ScatterChartData(), + transformationConfig: FlTransformationConfig( + scaleAxis: scaleAxis, + // This is for test + // ignore: avoid_redundant_argument_values + trackpadScrollCausesScale: false, + ), + ), + ), + ); + + final pointer = TestPointer(1, PointerDeviceKind.trackpad); + final chartCenterOffset = tester.getCenter( + find.byType(ScatterChartLeaf), + ); + const scrollAmount = Offset(0, -100); + + await tester.sendEventToBinding(pointer.hover(chartCenterOffset)); + await tester.pump(); + await tester.sendEventToBinding(pointer.scroll(scrollAmount)); + await tester.pump(); + + final scatterChartLeaf = tester.widget( + find.byType(ScatterChartLeaf), + ); + expect(scatterChartLeaf.chartVirtualRect, null); + }, + ); + } + + testWidgets('scales horizontally with FlScaleAxis.horizontal', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: ScatterChart( + ScatterChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.horizontal, + trackpadScrollCausesScale: true, + ), + ), + ), + ); + + final pointer = TestPointer(1, PointerDeviceKind.trackpad); + final chartCenterOffset = + tester.getCenter(find.byType(ScatterChartLeaf)); + const scrollAmount = Offset(0, -100); + + await tester.sendEventToBinding(pointer.hover(chartCenterOffset)); + await tester.pump(); + await tester.sendEventToBinding(pointer.scroll(scrollAmount)); + await tester.pump(); + + final scatterChartLeaf = tester.widget( + find.byType(ScatterChartLeaf), + ); + final renderBox = tester.renderObject( + find.byType(ScatterChartLeaf), + ); + final chartVirtualRect = scatterChartLeaf.chartVirtualRect!; + + expect(chartVirtualRect.size.height, renderBox.size.height); + expect(chartVirtualRect.size.width, greaterThan(renderBox.size.width)); + expect(chartVirtualRect.left, isNegative); + expect(chartVirtualRect.top, 0); + }); + + testWidgets('scales vertically with FlScaleAxis.vertical', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: ScatterChart( + ScatterChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.vertical, + trackpadScrollCausesScale: true, + ), + ), + ), + ); + + final pointer = TestPointer(1, PointerDeviceKind.trackpad); + final chartCenterOffset = + tester.getCenter(find.byType(ScatterChartLeaf)); + const scrollAmount = Offset(0, -100); + + await tester.sendEventToBinding(pointer.hover(chartCenterOffset)); + await tester.pump(); + await tester.sendEventToBinding(pointer.scroll(scrollAmount)); + await tester.pump(); + + final scatterChartLeaf = tester.widget( + find.byType(ScatterChartLeaf), + ); + final renderBox = tester.renderObject( + find.byType(ScatterChartLeaf), + ); + final chartVirtualRect = scatterChartLeaf.chartVirtualRect!; + + expect( + chartVirtualRect.size.height, + greaterThan(renderBox.size.height), + ); + expect(chartVirtualRect.size.width, renderBox.size.width); + expect(chartVirtualRect.left, 0); + expect(chartVirtualRect.top, isNegative); + }); + + testWidgets('scales freely with FlScaleAxis.free', + (WidgetTester tester) async { + await tester.pumpWidget( + createTestWidget( + chart: ScatterChart( + ScatterChartData(), + transformationConfig: const FlTransformationConfig( + scaleAxis: FlScaleAxis.free, + trackpadScrollCausesScale: true, + ), + ), + ), + ); + + final pointer = TestPointer(1, PointerDeviceKind.trackpad); + final chartCenterOffset = + tester.getCenter(find.byType(ScatterChartLeaf)); + const scrollAmount = Offset(0, -100); + + await tester.sendEventToBinding(pointer.hover(chartCenterOffset)); + await tester.pump(); + await tester.sendEventToBinding(pointer.scroll(scrollAmount)); + await tester.pump(); + + final scatterChartLeaf = + tester.widget(find.byType(ScatterChartLeaf)); + final renderBox = tester.renderObject( + find.byType(ScatterChartLeaf), + ); + final chartVirtualRect = scatterChartLeaf.chartVirtualRect!; + + expect(chartVirtualRect.size, greaterThan(renderBox.size)); + expect(chartVirtualRect.left, isNegative); + expect(chartVirtualRect.top, isNegative); + }); + }); + }); +} diff --git a/test/extensions/bar_chart_data_extensions_test.dart b/test/extensions/bar_chart_data_extensions_test.dart new file mode 100644 index 0000000..0079604 --- /dev/null +++ b/test/extensions/bar_chart_data_extensions_test.dart @@ -0,0 +1,79 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/extensions/bar_chart_data_extension.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../chart/data_pool.dart'; + +void main() { + group('BarChartDataExtension.calculateGroupsX', () { + test('calculates correct positions for basic alignments', () { + expect( + MockData.barChartData1 + .copyWith(alignment: BarChartAlignment.start) + .calculateGroupsX(100), + [9.0, 43.0, 77.0], + ); + + expect( + MockData.barChartData1 + .copyWith(alignment: BarChartAlignment.end) + .calculateGroupsX(100), + [23.0, 57.0, 91.0], + ); + + expect( + MockData.barChartData1 + .copyWith(alignment: BarChartAlignment.center) + .calculateGroupsX(100), + [16.0, 50.0, 84.0], + ); + + expect( + MockData.barChartData1 + .copyWith(alignment: BarChartAlignment.spaceBetween) + .calculateGroupsX(100), + [9.0, 50.0, 91.0], + ); + + expect( + MockData.barChartData1 + .copyWith(alignment: BarChartAlignment.spaceAround) + .calculateGroupsX(100), + [16.666666666666668, 50.0, 83.33333333333334], + ); + + expect( + MockData.barChartData1 + .copyWith(alignment: BarChartAlignment.spaceEvenly) + .calculateGroupsX(100), + [20.5, 50.0, 79.5], + ); + }); + + for (final alignment in [ + BarChartAlignment.start, + BarChartAlignment.end, + BarChartAlignment.center, + ]) { + test( + 'spaces evenly when a groupX exceeds view width for $alignment', + () { + expect( + MockData.barChartData1 + .copyWith(alignment: alignment) + .calculateGroupsX(60), + [10.5, 30.0, 49.5], + ); + }, + ); + } + + test('Throws Assertion error when barGroups is empty', () { + expect( + () => MockData.barChartData1 + .copyWith(barGroups: []).calculateGroupsX(100), + throwsAssertionError, + ); + }); + }); +} diff --git a/test/extensions/border_extension_test.dart b/test/extensions/border_extension_test.dart new file mode 100644 index 0000000..9481808 --- /dev/null +++ b/test/extensions/border_extension_test.dart @@ -0,0 +1,30 @@ +import 'package:fl_chart/src/extensions/border_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('Border isVisible()', () { + test('test 1', () { + final border = Border( + left: BorderSide( + color: Colors.red.withValues(alpha: 0.00001), + width: 10, + ), + ); + expect(border.isVisible(), true); + }); + + test('test 2', () { + final border = Border.all(width: 0); + expect(border.isVisible(), false); + }); + + test('test 3', () { + final border = Border.all( + color: Colors.red.withValues(alpha: 0), + width: 10, + ); + expect(border.isVisible(), false); + }); + }); +} diff --git a/test/extensions/color_extensions_test.dart b/test/extensions/color_extensions_test.dart new file mode 100644 index 0000000..e6f3df6 --- /dev/null +++ b/test/extensions/color_extensions_test.dart @@ -0,0 +1,18 @@ +import 'dart:ui'; + +import 'package:fl_chart/src/extensions/color_extension.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('test darken', () { + expect( + const Color(0x11111111).darken(), + isSameColorAs(const Color(0x110a0a0a)), + ); + + expect( + const Color(0x11111111).darken(100), + isSameColorAs(const Color(0x11000000)), + ); + }); +} diff --git a/test/extensions/edge_insets_extension_test.dart b/test/extensions/edge_insets_extension_test.dart new file mode 100644 index 0000000..a00d0e6 --- /dev/null +++ b/test/extensions/edge_insets_extension_test.dart @@ -0,0 +1,19 @@ +import 'package:fl_chart/src/extensions/edge_insets_extension.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('test onlyTopBottom', () { + const input = EdgeInsets.symmetric(horizontal: 10, vertical: 20); + + expect( + input.onlyTopBottom, + const EdgeInsets.symmetric(vertical: 20), + ); + + expect( + input.onlyLeftRight, + const EdgeInsets.symmetric(horizontal: 10), + ); + }); +} diff --git a/test/extensions/fl_border_data_extension_test.dart b/test/extensions/fl_border_data_extension_test.dart new file mode 100644 index 0000000..08bdabd --- /dev/null +++ b/test/extensions/fl_border_data_extension_test.dart @@ -0,0 +1,39 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/extensions/fl_border_data_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('test allSidesPadding', () { + expect( + FlBorderData( + show: false, + border: Border.all( + color: Colors.red, + width: 10, + ), + ).allSidesPadding, + EdgeInsets.zero, + ); + + expect( + FlBorderData( + show: true, + border: Border( + left: const BorderSide( + color: Colors.transparent, + ), + top: BorderSide( + width: 10, + color: Colors.red.withValues(alpha: 0.5), + ), + bottom: const BorderSide( + width: 4, + color: Colors.red, + ), + ), + ).allSidesPadding, + const EdgeInsets.fromLTRB(1, 10, 0, 4), + ); + }); +} diff --git a/test/extensions/fl_titles_data_extension_test.dart b/test/extensions/fl_titles_data_extension_test.dart new file mode 100644 index 0000000..854de16 --- /dev/null +++ b/test/extensions/fl_titles_data_extension_test.dart @@ -0,0 +1,19 @@ +import 'package:fl_chart/src/extensions/fl_titles_data_extension.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../chart/data_pool.dart'; + +void main() { + test('test allSidesPadding', () { + expect( + MockData.flTitlesData1.copyWith(show: false).allSidesPadding, + EdgeInsets.zero, + ); + + expect( + MockData.flTitlesData1.allSidesPadding, + const EdgeInsets.fromLTRB(27, 16, 16, 16), + ); + }); +} diff --git a/test/extensions/gradient_extension_test.dart b/test/extensions/gradient_extension_test.dart new file mode 100644 index 0000000..95fd022 --- /dev/null +++ b/test/extensions/gradient_extension_test.dart @@ -0,0 +1,111 @@ +import 'package:fl_chart/src/extensions/gradient_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('GradientExtension.getSafeColorStops', () { + group('returns linearly calculated stops', () { + test('when no stops are provided', () { + expect( + const _TestGradient( + colors: [Colors.red, Colors.blue], + ).getSafeColorStops(), + [0, 1], + ); + + expect( + const _TestGradient( + colors: [Colors.red, Colors.blue, Colors.green], + ).getSafeColorStops(), + [0, 0.5, 1], + ); + }); + + test('when less stops than colors are provided', () { + expect( + const _TestGradient( + colors: [Colors.red, Colors.blue, Colors.green], + stops: [0.2, 0.8], + ).getSafeColorStops(), + [0, 0.5, 1], + ); + }); + + test('when more stops than colors are provided', () { + expect( + const _TestGradient( + colors: [Colors.red, Colors.blue], + stops: [0.2, 0.8, 0.9], + ).getSafeColorStops(), + [0, 1], + ); + }); + }); + + test('returns stops when same length as colors', () { + expect( + const _TestGradient( + colors: [Colors.red, Colors.blue], + stops: [0.1, 0.8], + ).getSafeColorStops(), + [0.1, 0.8], + ); + }); + + group('throws ArgumentError', () { + group('when colors is empty', () { + test('without stops', () { + expect( + () => const _TestGradient(colors: []).getSafeColorStops(), + throwsArgumentError, + ); + }); + + test('with stops', () { + expect( + () => const _TestGradient( + colors: [], + stops: [0.1, 0.8], + ).getSafeColorStops(), + throwsArgumentError, + ); + }); + }); + + group('when colors length is 1', () { + test('without stops', () { + expect( + () => const _TestGradient( + colors: [Colors.red], + ).getSafeColorStops(), + throwsArgumentError, + ); + }); + + test('with stops', () { + expect( + () => const _TestGradient( + colors: [Colors.red], + stops: [0.1, 0.8], + ).getSafeColorStops(), + throwsArgumentError, + ); + }); + }); + }); + }); +} + +class _TestGradient extends Gradient { + const _TestGradient({required super.colors, super.stops}); + + @override + Shader createShader(Rect rect, {TextDirection? textDirection}) => + throw UnimplementedError(); + + @override + Gradient scale(double t) => throw UnimplementedError(); + + @override + Gradient withOpacity(double opacity) => throw UnimplementedError(); +} diff --git a/test/extensions/paint_extension_test.dart b/test/extensions/paint_extension_test.dart new file mode 100644 index 0000000..0990bcd --- /dev/null +++ b/test/extensions/paint_extension_test.dart @@ -0,0 +1,55 @@ +import 'package:fl_chart/src/extensions/paint_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../chart/data_pool.dart'; + +void main() { + test('test transparentIfWidthIsZero', () { + final paint = Paint() + ..color = MockData.color0 + ..strokeWidth = 4 + ..transparentIfWidthIsZero(); + expect(paint.strokeWidth, 4); + expect(MockData.color0, paint.color); + + paint + ..strokeWidth = 0.5 + ..transparentIfWidthIsZero(); + expect(paint.strokeWidth, 0.5); + expect(MockData.color0, paint.color); + + paint + ..strokeWidth = 0.0 + ..transparentIfWidthIsZero(); + expect(paint.strokeWidth, 0.0); + expect(MockData.color0.withValues(alpha: 0), paint.color); + }); + + test('test setColorOrGradient', () { + final paint = Paint() + ..color = MockData.color0 + ..setColorOrGradient(null, MockData.gradient1, MockData.rect1); + expect(paint.shader, isNotNull); + + paint.setColorOrGradient(MockData.color0, null, MockData.rect1); + expect(paint.color, MockData.color0); + expect(paint.shader, isNull); + }); + + test('test setColorOrGradientForLine', () { + final paint = Paint() + ..color = MockData.color0 + ..setColorOrGradientForLine( + null, + MockData.gradient1, + from: MockData.rect1.topLeft, + to: MockData.rect1.bottomRight, + ); + expect(paint.shader, isNotNull); + + paint.setColorOrGradient(MockData.color0, null, MockData.rect1); + expect(paint.color, MockData.color0); + expect(paint.shader, isNull); + }); +} diff --git a/test/extensions/path_extension_test.dart b/test/extensions/path_extension_test.dart new file mode 100644 index 0000000..aced263 --- /dev/null +++ b/test/extensions/path_extension_test.dart @@ -0,0 +1,25 @@ +import 'dart:ui'; + +import 'package:fl_chart/src/extensions/path_extension.dart'; +import 'package:fl_chart/src/utils/path_drawing/dash_path.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../helper_methods.dart'; + +void main() { + test('test transparentIfWidthIsZero', () { + final path1 = Path() + ..moveTo(0, 0) + ..lineTo(10, 0); + expect( + path1.toDashedPath(null), + path1, + ); + + final path2 = + dashPath(path1, dashArray: CircularIntervalList([10.0, 5.0])); + + expect(HelperMethods.equalsPaths(path1.toDashedPath([10, 5]), path2), true); + }); +} diff --git a/test/extensions/rrect_extension_test.dart b/test/extensions/rrect_extension_test.dart new file mode 100644 index 0000000..614a069 --- /dev/null +++ b/test/extensions/rrect_extension_test.dart @@ -0,0 +1,16 @@ +import 'dart:ui'; + +import 'package:fl_chart/src/extensions/rrect_extension.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../chart/data_pool.dart'; + +void main() { + test('test getRect', () { + expect( + MockData.rRect1.getRect(), + const Rect.fromLTRB(1, 1, 1, 1), + ); + }); +} diff --git a/test/extensions/side_titles_extension_test.dart b/test/extensions/side_titles_extension_test.dart new file mode 100644 index 0000000..54b9238 --- /dev/null +++ b/test/extensions/side_titles_extension_test.dart @@ -0,0 +1,42 @@ +import 'package:fl_chart/src/chart/base/axis_chart/axis_chart_data.dart'; +import 'package:fl_chart/src/extensions/side_titles_extension.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('test totalReservedSize', () { + expect( + const AxisTitles( + axisNameSize: 12, + sideTitles: SideTitles( + showTitles: true, + reservedSize: 20, + ), + ).totalReservedSize, + 20, + ); + + expect( + const AxisTitles( + axisNameWidget: Text('asdf'), + axisNameSize: 12, + sideTitles: SideTitles( + reservedSize: 20, + ), + ).totalReservedSize, + 12, + ); + + expect( + const AxisTitles( + axisNameWidget: Text('asdf'), + axisNameSize: 12, + sideTitles: SideTitles( + showTitles: true, + reservedSize: 20, + ), + ).totalReservedSize, + 32, + ); + }); +} diff --git a/test/extensions/size_extension_test.dart b/test/extensions/size_extension_test.dart new file mode 100644 index 0000000..c04b6fc --- /dev/null +++ b/test/extensions/size_extension_test.dart @@ -0,0 +1,24 @@ +import 'dart:ui'; + +import 'package:fl_chart/src/extensions/size_extension.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('test rotateByQuarterTurns extension.', () { + expect(const Size(100, 200).rotateByQuarterTurns(0), const Size(100, 200)); + expect(const Size(100, 200).rotateByQuarterTurns(1), const Size(200, 100)); + expect(const Size(100, 200).rotateByQuarterTurns(2), const Size(100, 200)); + expect(const Size(100, 200).rotateByQuarterTurns(3), const Size(200, 100)); + expect(const Size(100, 200).rotateByQuarterTurns(4), const Size(100, 200)); + expect(const Size(100, 200).rotateByQuarterTurns(5), const Size(200, 100)); + expect(const Size(100, 200).rotateByQuarterTurns(6), const Size(100, 200)); + expect( + () => const Size(100, 200).rotateByQuarterTurns(-1), + throwsArgumentError, + ); + expect( + () => const Size(100, 200).rotateByQuarterTurns(-3), + throwsArgumentError, + ); + }); +} diff --git a/test/extensions/text_align_extension_test.dart b/test/extensions/text_align_extension_test.dart new file mode 100644 index 0000000..3e0163c --- /dev/null +++ b/test/extensions/text_align_extension_test.dart @@ -0,0 +1,58 @@ +import 'package:fl_chart/src/extensions/text_align_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('test getFinalHorizontalAlignment extension.', () { + const textAlignLeft = TextAlign.left; + const textAlignRight = TextAlign.right; + + expect( + textAlignLeft.getFinalHorizontalAlignment(TextDirection.rtl), + HorizontalAlignment.left, + ); + expect( + textAlignLeft.getFinalHorizontalAlignment(TextDirection.ltr), + HorizontalAlignment.left, + ); + + expect( + textAlignRight.getFinalHorizontalAlignment(TextDirection.rtl), + HorizontalAlignment.right, + ); + expect( + textAlignRight.getFinalHorizontalAlignment(TextDirection.ltr), + HorizontalAlignment.right, + ); + + const textAlignStart = TextAlign.start; + expect( + textAlignStart.getFinalHorizontalAlignment(TextDirection.ltr), + HorizontalAlignment.left, + ); + expect( + textAlignStart.getFinalHorizontalAlignment(TextDirection.rtl), + HorizontalAlignment.right, + ); + + const textAlignEnd = TextAlign.end; + expect( + textAlignEnd.getFinalHorizontalAlignment(TextDirection.rtl), + HorizontalAlignment.left, + ); + expect( + textAlignEnd.getFinalHorizontalAlignment(TextDirection.ltr), + HorizontalAlignment.right, + ); + + const textAlignCenter = TextAlign.center; + expect( + textAlignCenter.getFinalHorizontalAlignment(TextDirection.rtl), + HorizontalAlignment.center, + ); + expect( + textAlignCenter.getFinalHorizontalAlignment(TextDirection.ltr), + HorizontalAlignment.center, + ); + }); +} diff --git a/test/helper_methods.dart b/test/helper_methods.dart new file mode 100644 index 0000000..026dd38 --- /dev/null +++ b/test/helper_methods.dart @@ -0,0 +1,103 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'chart/data_pool.dart'; + +void main() { + test('test equalsPaths', () { + expect( + HelperMethods.equalsPaths(MockData.path1, MockData.path1Duplicate), + true, + ); + expect(HelperMethods.equalsPaths(MockData.path1, MockData.path2), false); + }); +} + +class HelperMethods { + static bool equalsPaths(Path path1, Path path2) { + final metrics1 = path1.computeMetrics().toList(); + final metrics2 = path2.computeMetrics().toList(); + if (metrics1.length != metrics2.length) { + return false; + } + for (var i = 0; i < metrics1.length; i++) { + if (metrics1[i].length != metrics2[i].length) { + return false; + } + if (metrics1[i].isClosed != metrics2[i].isClosed) { + return false; + } + if (metrics1[i].contourIndex != metrics2[i].contourIndex) { + return false; + } + final half = metrics1[i].length / 2; + final tangent1 = metrics1[i].getTangentForOffset(half); + final tangent2 = metrics2[i].getTangentForOffset(half); + if (tangent1!.position != tangent2!.position) { + return false; + } + if (tangent1.angle != tangent2.angle) { + return false; + } + if (tangent1.vector != tangent2.vector) { + return false; + } + } + return true; + } + + static bool equalsRRects( + RRect rrect1, + RRect rrect2, { + double tolerance = 0.05, + }) { + if ((rrect1.left - rrect2.left).abs() > tolerance) { + return false; + } + + if ((rrect1.top - rrect2.top).abs() > tolerance) { + return false; + } + + if ((rrect1.right - rrect2.right).abs() > tolerance) { + return false; + } + + if ((rrect1.bottom - rrect2.bottom).abs() > tolerance) { + return false; + } + + if (rrect1.blRadius != rrect2.blRadius) { + return false; + } + + if (rrect1.brRadius != rrect2.brRadius) { + return false; + } + + if (rrect1.trRadius != rrect2.trRadius) { + return false; + } + + if (rrect1.tlRadius != rrect2.tlRadius) { + return false; + } + + return true; + } + + static bool equalsOffsets( + Offset offset1, + Offset offset2, { + double tolerance = 0.05, + }) { + if ((offset1.dx - offset2.dx).abs() > tolerance) { + return false; + } + + if ((offset1.dy - offset2.dy).abs() > tolerance) { + return false; + } + + return true; + } +} diff --git a/test/matchers.dart b/test/matchers.dart new file mode 100644 index 0000000..8318738 --- /dev/null +++ b/test/matchers.dart @@ -0,0 +1,97 @@ +import 'package:fl_chart/src/chart/base/axis_chart/axis_chart_data.dart'; +import 'package:fl_chart/src/chart/scatter_chart/scatter_chart_data.dart'; +import 'package:flutter_test/flutter_test.dart'; + +Matcher matchesScatterSpotWithCirclePainter(ScatterSpot spot) { + return isA() + .having( + (spot) => spot.x, + 'x', + spot.x, + ) + .having( + (spot) => spot.y, + 'y', + spot.y, + ) + .having( + (spot) => spot.show, + 'show', + spot.show, + ) + .having( + (spot) => spot.dotPainter, + 'dotPainter', + isA().having( + (painter) => painter.color, + 'color', + isSameColorAs((spot.dotPainter as FlDotCirclePainter).color), + ), + ); +} + +Matcher matchesVerticalRangeAnnotation(VerticalRangeAnnotation annotation) { + return isA() + .having( + (annotation) => annotation.x1, + 'x1', + annotation.x1, + ) + .having( + (annotation) => annotation.x2, + 'x2', + annotation.x2, + ) + .having( + (annotation) => annotation.color, + 'color', + isSameColorAs(annotation.color!), + ); +} + +Matcher matchesHorizontalRangeAnnotation(HorizontalRangeAnnotation annotation) { + return isA() + .having( + (annotation) => annotation.y1, + 'y1', + annotation.y1, + ) + .having( + (annotation) => annotation.y2, + 'y2', + annotation.y2, + ) + .having( + (annotation) => annotation.color, + 'color', + isSameColorAs(annotation.color!), + ); +} + +Matcher matchesVerticalLine(VerticalLine line) { + return isA() + .having( + (line) => line.x, + 'x', + line.x, + ) + .having( + (line) => line.color, + 'color', + isSameColorAs(line.color!), + ); +} + +Matcher matchesHorizontalLine(HorizontalLine line) { + return isA() + .having( + (line) => line.y, + 'y', + line.y, + ) + .having( + (line) => line.color, + 'color', + isSameColorAs(line.color!), + ); +} diff --git a/test/utils/canvas_wrapper_test.dart b/test/utils/canvas_wrapper_test.dart new file mode 100644 index 0000000..6bac98b --- /dev/null +++ b/test/utils/canvas_wrapper_test.dart @@ -0,0 +1,152 @@ +import 'dart:ui'; + +import 'package:fl_chart/src/chart/base/axis_chart/axis_chart_data.dart'; +import 'package:fl_chart/src/utils/canvas_wrapper.dart'; +import 'package:fl_chart/src/utils/utils.dart'; +import 'package:flutter/painting.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import '../chart/data_pool.dart'; +import 'canvas_wrapper_test.mocks.dart'; + +@GenerateMocks([Canvas, FlDotPainter, Utils]) +void main() { + final mockCanvas = MockCanvas(); + final canvasWrapper = CanvasWrapper(mockCanvas, MockData.size1); + + test('test drawRRect', () { + canvasWrapper.drawRRect(MockData.rRect1, MockData.paint1); + verify(mockCanvas.drawRRect(MockData.rRect1, MockData.paint1)).called(1); + }); + + test('test save', () { + canvasWrapper.save(); + verify(mockCanvas.save()).called(1); + }); + + test('test restore', () { + canvasWrapper.restore(); + verify(mockCanvas.restore()).called(1); + }); + + test('test clipRect', () { + canvasWrapper.clipRect(MockData.rect1); + verify(mockCanvas.clipRect(MockData.rect1)).called(1); + }); + + test('test translate', () { + canvasWrapper.translate(11, 232); + verify(mockCanvas.translate(11, 232)).called(1); + }); + + test('test rotate', () { + canvasWrapper.rotate(12); + verify(mockCanvas.rotate(12)).called(1); + }); + + test('test drawPath', () { + canvasWrapper.drawPath(MockData.path1, MockData.paint1); + verify(mockCanvas.drawPath(MockData.path1, MockData.paint1)).called(1); + }); + + test('test saveLayer', () { + canvasWrapper.saveLayer(MockData.rect1, MockData.paint1); + verify(mockCanvas.saveLayer(MockData.rect1, MockData.paint1)).called(1); + }); + + test('test drawPicture', () { + canvasWrapper.drawPicture(MockData.picture1()); + verify(mockCanvas.drawPicture(MockData.picture1())).called(1); + }); + + test('test clipPath', () { + canvasWrapper.clipPath(MockData.path1); + verify(mockCanvas.clipPath(MockData.path1)).called(1); + }); + + test('test drawRect', () { + canvasWrapper.drawRect(MockData.rect1, MockData.paint1); + verify(mockCanvas.drawRect(MockData.rect1, MockData.paint1)).called(1); + }); + + test('test drawLine', () { + canvasWrapper.drawLine(MockData.offset1, MockData.offset2, MockData.paint1); + verify( + mockCanvas.drawLine( + MockData.offset1, + MockData.offset2, + MockData.paint1, + ), + ).called(1); + }); + + test('test drawCircle', () { + canvasWrapper.drawCircle(MockData.offset1, 12, MockData.paint1); + verify(mockCanvas.drawCircle(MockData.offset1, 12, MockData.paint1)) + .called(1); + }); + + test('test drawArc', () { + canvasWrapper.drawArc(MockData.rect1, 12, 22, false, MockData.paint1); + verify(mockCanvas.drawArc(MockData.rect1, 12, 22, false, MockData.paint1)) + .called(1); + }); + + test('test drawDot', () { + final painter = MockFlDotPainter(); + canvasWrapper.drawDot(painter, MockData.lineBarSpot1, MockData.offset1); + verify(painter.draw(mockCanvas, MockData.lineBarSpot1, MockData.offset1)) + .called(1); + }); + + test('test drawRotated', () { + final utilsMainInstance = Utils(); + final mockUtils = MockUtils(); + when(mockUtils.radians(any)).thenAnswer((realInvocation) => 12); + Utils.changeInstance(mockUtils); + + var calledCallback = false; + void callback() { + calledCallback = true; + } + + canvasWrapper.drawRotated( + size: const Size(240, 240), + rotationOffset: MockData.offset1, + drawOffset: MockData.offset2, + angle: 12, + drawCallback: callback, + ); + verify(mockCanvas.save()).called(1); + verify(mockCanvas.translate(123, 123)).called(1); + verify(mockCanvas.rotate(12)).called(1); + verify(mockCanvas.translate(-122, -122)).called(1); + expect(calledCallback, true); + verify(mockCanvas.restore()).called(1); + Utils.changeInstance(utilsMainInstance); + }); + + test('test drawText', () { + final tp = MockData.textPainter2; + canvasWrapper.drawText(tp, MockData.offset1); + verify(tp.paint(mockCanvas, MockData.offset1)).called(1); + }); + + test('test drawVerticalText', () { + final tp = MockData.textPainter2; + final mockUtils = MockUtils(); + when(mockUtils.radians(any)).thenAnswer((realInvocation) => 90); + Utils.changeInstance(mockUtils); + + canvasWrapper.drawVerticalText(tp, MockData.offset1); + verify(mockCanvas.save()).called(1); + verify(mockCanvas.translate(MockData.offset1.dx, MockData.offset1.dy)) + .called(1); + verify(mockCanvas.rotate(90)).called(1); + verify(mockCanvas.translate(-MockData.offset1.dx, -MockData.offset1.dy)) + .called(1); + verify(tp.paint(mockCanvas, MockData.offset1)).called(1); + verify(mockCanvas.restore()).called(1); + }); +} diff --git a/test/utils/canvas_wrapper_test.mocks.dart b/test/utils/canvas_wrapper_test.mocks.dart new file mode 100644 index 0000000..2c601e9 --- /dev/null +++ b/test/utils/canvas_wrapper_test.mocks.dart @@ -0,0 +1,626 @@ +// Mocks generated by Mockito 5.4.5 from annotations +// in fl_chart/test/utils/canvas_wrapper_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:typed_data' as _i5; +import 'dart:ui' as _i2; + +import 'package:fl_chart/fl_chart.dart' as _i3; +import 'package:fl_chart/src/utils/utils.dart' as _i6; +import 'package:flutter/material.dart' as _i4; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i7; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeRect_0 extends _i1.SmartFake implements _i2.Rect { + _FakeRect_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeColor_1 extends _i1.SmartFake implements _i2.Color { + _FakeColor_1(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeSize_2 extends _i1.SmartFake implements _i2.Size { + _FakeSize_2(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeFlDotPainter_3 extends _i1.SmartFake implements _i3.FlDotPainter { + _FakeFlDotPainter_3(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeOffset_4 extends _i1.SmartFake implements _i2.Offset { + _FakeOffset_4(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeBorderSide_5 extends _i1.SmartFake implements _i4.BorderSide { + _FakeBorderSide_5(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i4.DiagnosticLevel? minLevel = _i4.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeTextStyle_6 extends _i1.SmartFake implements _i4.TextStyle { + _FakeTextStyle_6(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i4.DiagnosticLevel? minLevel = _i4.DiagnosticLevel.info}) => + super.toString(); +} + +/// A class which mocks [Canvas]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCanvas extends _i1.Mock implements _i2.Canvas { + MockCanvas() { + _i1.throwOnMissingStub(this); + } + + @override + void save() => super.noSuchMethod( + Invocation.method(#save, []), + returnValueForMissingStub: null, + ); + + @override + void saveLayer(_i2.Rect? bounds, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#saveLayer, [bounds, paint]), + returnValueForMissingStub: null, + ); + + @override + void restore() => super.noSuchMethod( + Invocation.method(#restore, []), + returnValueForMissingStub: null, + ); + + @override + void restoreToCount(int? count) => super.noSuchMethod( + Invocation.method(#restoreToCount, [count]), + returnValueForMissingStub: null, + ); + + @override + int getSaveCount() => + (super.noSuchMethod(Invocation.method(#getSaveCount, []), returnValue: 0) + as int); + + @override + void translate(double? dx, double? dy) => super.noSuchMethod( + Invocation.method(#translate, [dx, dy]), + returnValueForMissingStub: null, + ); + + @override + void scale(double? sx, [double? sy]) => super.noSuchMethod( + Invocation.method(#scale, [sx, sy]), + returnValueForMissingStub: null, + ); + + @override + void rotate(double? radians) => super.noSuchMethod( + Invocation.method(#rotate, [radians]), + returnValueForMissingStub: null, + ); + + @override + void skew(double? sx, double? sy) => super.noSuchMethod( + Invocation.method(#skew, [sx, sy]), + returnValueForMissingStub: null, + ); + + @override + void transform(_i5.Float64List? matrix4) => super.noSuchMethod( + Invocation.method(#transform, [matrix4]), + returnValueForMissingStub: null, + ); + + @override + _i5.Float64List getTransform() => + (super.noSuchMethod( + Invocation.method(#getTransform, []), + returnValue: _i5.Float64List(0), + ) + as _i5.Float64List); + + @override + void clipRect( + _i2.Rect? rect, { + _i2.ClipOp? clipOp = _i2.ClipOp.intersect, + bool? doAntiAlias = true, + }) => super.noSuchMethod( + Invocation.method( + #clipRect, + [rect], + {#clipOp: clipOp, #doAntiAlias: doAntiAlias}, + ), + returnValueForMissingStub: null, + ); + + @override + void clipRRect(_i2.RRect? rrect, {bool? doAntiAlias = true}) => + super.noSuchMethod( + Invocation.method(#clipRRect, [rrect], {#doAntiAlias: doAntiAlias}), + returnValueForMissingStub: null, + ); + + @override + void clipPath(_i2.Path? path, {bool? doAntiAlias = true}) => + super.noSuchMethod( + Invocation.method(#clipPath, [path], {#doAntiAlias: doAntiAlias}), + returnValueForMissingStub: null, + ); + + @override + _i2.Rect getLocalClipBounds() => + (super.noSuchMethod( + Invocation.method(#getLocalClipBounds, []), + returnValue: _FakeRect_0( + this, + Invocation.method(#getLocalClipBounds, []), + ), + ) + as _i2.Rect); + + @override + _i2.Rect getDestinationClipBounds() => + (super.noSuchMethod( + Invocation.method(#getDestinationClipBounds, []), + returnValue: _FakeRect_0( + this, + Invocation.method(#getDestinationClipBounds, []), + ), + ) + as _i2.Rect); + + @override + void drawColor(_i2.Color? color, _i2.BlendMode? blendMode) => + super.noSuchMethod( + Invocation.method(#drawColor, [color, blendMode]), + returnValueForMissingStub: null, + ); + + @override + void drawLine(_i2.Offset? p1, _i2.Offset? p2, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawLine, [p1, p2, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawPaint(_i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawPaint, [paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRect(_i2.Rect? rect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRect, [rect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRRect(_i2.RRect? rrect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawRRect, [rrect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawDRRect(_i2.RRect? outer, _i2.RRect? inner, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawDRRect, [outer, inner, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawOval(_i2.Rect? rect, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawOval, [rect, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawCircle(_i2.Offset? c, double? radius, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawCircle, [c, radius, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawArc( + _i2.Rect? rect, + double? startAngle, + double? sweepAngle, + bool? useCenter, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawArc, [ + rect, + startAngle, + sweepAngle, + useCenter, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawPath(_i2.Path? path, _i2.Paint? paint) => super.noSuchMethod( + Invocation.method(#drawPath, [path, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImage(_i2.Image? image, _i2.Offset? offset, _i2.Paint? paint) => + super.noSuchMethod( + Invocation.method(#drawImage, [image, offset, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImageRect( + _i2.Image? image, + _i2.Rect? src, + _i2.Rect? dst, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawImageRect, [image, src, dst, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawImageNine( + _i2.Image? image, + _i2.Rect? center, + _i2.Rect? dst, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawImageNine, [image, center, dst, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawPicture(_i2.Picture? picture) => super.noSuchMethod( + Invocation.method(#drawPicture, [picture]), + returnValueForMissingStub: null, + ); + + @override + void drawParagraph(_i2.Paragraph? paragraph, _i2.Offset? offset) => + super.noSuchMethod( + Invocation.method(#drawParagraph, [paragraph, offset]), + returnValueForMissingStub: null, + ); + + @override + void drawPoints( + _i2.PointMode? pointMode, + List<_i2.Offset>? points, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawPoints, [pointMode, points, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawRawPoints( + _i2.PointMode? pointMode, + _i5.Float32List? points, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawRawPoints, [pointMode, points, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawVertices( + _i2.Vertices? vertices, + _i2.BlendMode? blendMode, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawVertices, [vertices, blendMode, paint]), + returnValueForMissingStub: null, + ); + + @override + void drawAtlas( + _i2.Image? atlas, + List<_i2.RSTransform>? transforms, + List<_i2.Rect>? rects, + List<_i2.Color>? colors, + _i2.BlendMode? blendMode, + _i2.Rect? cullRect, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawAtlas, [ + atlas, + transforms, + rects, + colors, + blendMode, + cullRect, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawRawAtlas( + _i2.Image? atlas, + _i5.Float32List? rstTransforms, + _i5.Float32List? rects, + _i5.Int32List? colors, + _i2.BlendMode? blendMode, + _i2.Rect? cullRect, + _i2.Paint? paint, + ) => super.noSuchMethod( + Invocation.method(#drawRawAtlas, [ + atlas, + rstTransforms, + rects, + colors, + blendMode, + cullRect, + paint, + ]), + returnValueForMissingStub: null, + ); + + @override + void drawShadow( + _i2.Path? path, + _i2.Color? color, + double? elevation, + bool? transparentOccluder, + ) => super.noSuchMethod( + Invocation.method(#drawShadow, [ + path, + color, + elevation, + transparentOccluder, + ]), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [FlDotPainter]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFlDotPainter extends _i1.Mock implements _i3.FlDotPainter { + MockFlDotPainter() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.Color get mainColor => + (super.noSuchMethod( + Invocation.getter(#mainColor), + returnValue: _FakeColor_1(this, Invocation.getter(#mainColor)), + ) + as _i2.Color); + + @override + List get props => + (super.noSuchMethod(Invocation.getter(#props), returnValue: []) + as List); + + @override + void draw(_i2.Canvas? canvas, _i3.FlSpot? spot, _i2.Offset? offsetInCanvas) => + super.noSuchMethod( + Invocation.method(#draw, [canvas, spot, offsetInCanvas]), + returnValueForMissingStub: null, + ); + + @override + _i2.Size getSize(_i3.FlSpot? spot) => + (super.noSuchMethod( + Invocation.method(#getSize, [spot]), + returnValue: _FakeSize_2(this, Invocation.method(#getSize, [spot])), + ) + as _i2.Size); + + @override + _i3.FlDotPainter lerp(_i3.FlDotPainter? a, _i3.FlDotPainter? b, double? t) => + (super.noSuchMethod( + Invocation.method(#lerp, [a, b, t]), + returnValue: _FakeFlDotPainter_3( + this, + Invocation.method(#lerp, [a, b, t]), + ), + ) + as _i3.FlDotPainter); + + @override + bool hitTest( + _i3.FlSpot? spot, + _i2.Offset? touched, + _i2.Offset? center, + double? extraThreshold, + ) => + (super.noSuchMethod( + Invocation.method(#hitTest, [ + spot, + touched, + center, + extraThreshold, + ]), + returnValue: false, + ) + as bool); +} + +/// A class which mocks [Utils]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockUtils extends _i1.Mock implements _i6.Utils { + MockUtils() { + _i1.throwOnMissingStub(this); + } + + @override + double radians(double? degrees) => + (super.noSuchMethod( + Invocation.method(#radians, [degrees]), + returnValue: 0.0, + ) + as double); + + @override + double degrees(double? radians) => + (super.noSuchMethod( + Invocation.method(#degrees, [radians]), + returnValue: 0.0, + ) + as double); + + @override + double translateRotatedPosition(double? size, double? degree) => + (super.noSuchMethod( + Invocation.method(#translateRotatedPosition, [size, degree]), + returnValue: 0.0, + ) + as double); + + @override + _i2.Offset calculateRotationOffset(_i2.Size? size, double? degree) => + (super.noSuchMethod( + Invocation.method(#calculateRotationOffset, [size, degree]), + returnValue: _FakeOffset_4( + this, + Invocation.method(#calculateRotationOffset, [size, degree]), + ), + ) + as _i2.Offset); + + @override + _i4.BorderRadius? normalizeBorderRadius( + _i4.BorderRadius? borderRadius, + double? width, + ) => + (super.noSuchMethod( + Invocation.method(#normalizeBorderRadius, [borderRadius, width]), + ) + as _i4.BorderRadius?); + + @override + _i4.BorderSide normalizeBorderSide( + _i4.BorderSide? borderSide, + double? width, + ) => + (super.noSuchMethod( + Invocation.method(#normalizeBorderSide, [borderSide, width]), + returnValue: _FakeBorderSide_5( + this, + Invocation.method(#normalizeBorderSide, [borderSide, width]), + ), + ) + as _i4.BorderSide); + + @override + double getEfficientInterval( + double? axisViewSize, + double? diffInAxis, { + double? pixelPerInterval = 40.0, + }) => + (super.noSuchMethod( + Invocation.method( + #getEfficientInterval, + [axisViewSize, diffInAxis], + {#pixelPerInterval: pixelPerInterval}, + ), + returnValue: 0.0, + ) + as double); + + @override + double roundInterval(double? input) => + (super.noSuchMethod( + Invocation.method(#roundInterval, [input]), + returnValue: 0.0, + ) + as double); + + @override + int getFractionDigits(double? value) => + (super.noSuchMethod( + Invocation.method(#getFractionDigits, [value]), + returnValue: 0, + ) + as int); + + @override + String formatNumber(double? axisMin, double? axisMax, double? axisValue) => + (super.noSuchMethod( + Invocation.method(#formatNumber, [axisMin, axisMax, axisValue]), + returnValue: _i7.dummyValue( + this, + Invocation.method(#formatNumber, [axisMin, axisMax, axisValue]), + ), + ) + as String); + + @override + _i4.TextStyle getThemeAwareTextStyle( + _i4.BuildContext? context, + _i4.TextStyle? providedStyle, + ) => + (super.noSuchMethod( + Invocation.method(#getThemeAwareTextStyle, [ + context, + providedStyle, + ]), + returnValue: _FakeTextStyle_6( + this, + Invocation.method(#getThemeAwareTextStyle, [ + context, + providedStyle, + ]), + ), + ) + as _i4.TextStyle); + + @override + double getBestInitialIntervalValue( + double? min, + double? max, + double? interval, { + double? baseline = 0.0, + }) => + (super.noSuchMethod( + Invocation.method( + #getBestInitialIntervalValue, + [min, max, interval], + {#baseline: baseline}, + ), + returnValue: 0.0, + ) + as double); + + @override + double convertRadiusToSigma(double? radius) => + (super.noSuchMethod( + Invocation.method(#convertRadiusToSigma, [radius]), + returnValue: 0.0, + ) + as double); +} diff --git a/test/utils/lerp_test.dart b/test/utils/lerp_test.dart new file mode 100644 index 0000000..de45ce4 --- /dev/null +++ b/test/utils/lerp_test.dart @@ -0,0 +1,598 @@ +import 'dart:ui'; + +import 'package:fl_chart/fl_chart.dart'; +import 'package:fl_chart/src/utils/lerp.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../chart/data_pool.dart'; +import '../matchers.dart'; + +void main() { + const tolerance = 0.001; + + test('test lerpList', () { + final list1 = [1, 1, 2]; + final list2 = [1, 2, 3, 5]; + expect(lerpList(list1, list2, 0, lerp: lerpDouble), [1.0, 1.0, 2.0, 5.0]); + expect(lerpList(list1, list2, 0.5, lerp: lerpDouble), [1.0, 1.5, 2.5, 5.0]); + expect(lerpList(list1, list1, 0.5, lerp: lerpDouble), list1); + }); + + test('test lerpColorList', () { + const list1 = [ + MockData.color1, + MockData.color1, + MockData.color2, + ]; + const list2 = [ + MockData.color1, + MockData.color2, + MockData.color3, + MockData.color5, + ]; + expect(lerpColorList(list1, list2, 0), [ + isSameColorAs(MockData.color1), + isSameColorAs(MockData.color1), + isSameColorAs(MockData.color2), + isSameColorAs(MockData.color5), + ]); + expect(lerpColorList(list1, list2, 1), list2); + expect(lerpColorList(list1, list2, 0.5), [ + isSameColorAs(MockData.color1), + isSameColorAs(const Color(0x19191919)), + isSameColorAs(const Color(0x2a2a2a2a)), + isSameColorAs(MockData.color5), + ]); + }); + + test('test lerpColor', () { + expect( + lerpColor(MockData.color1, MockData.color1, 0.5), + isSameColorAs(MockData.color1), + ); + expect( + lerpColor(MockData.color1, MockData.color1, 0), + isSameColorAs(MockData.color1), + ); + expect( + lerpColor(MockData.color1, MockData.color1, 1), + isSameColorAs(MockData.color1), + ); + + expect( + lerpColor(MockData.color1, MockData.color2, 0), + isSameColorAs(MockData.color1), + ); + expect( + lerpColor(MockData.color1, MockData.color2, 0.3), + isSameColorAs(const Color(0x16161616)), + ); + expect( + lerpColor(MockData.color1, MockData.color2, 1), + isSameColorAs(MockData.color2), + ); + }); + + test('test lerpDoubleAllowInfinity', () { + expect(lerpDoubleAllowInfinity(12, 12, 0), 12); + expect(lerpDoubleAllowInfinity(12, 12, 0.2), 12); + expect(lerpDoubleAllowInfinity(12, 12, 0.5), 12); + expect(lerpDoubleAllowInfinity(12, 12, 1), 12); + + expect(lerpDoubleAllowInfinity(12, double.infinity, 1), double.infinity); + expect(lerpDoubleAllowInfinity(12, double.infinity, 0), double.infinity); + expect(lerpDoubleAllowInfinity(12, double.infinity, 0.4), double.infinity); + + expect(lerpDoubleAllowInfinity(double.infinity, 12, 1), 12); + expect(lerpDoubleAllowInfinity(double.infinity, 12, 0), 12); + expect(lerpDoubleAllowInfinity(double.infinity, 12, 0.4), 12); + + expect(lerpDoubleAllowInfinity(0, 10, 0.4), 4); + expect(lerpDoubleAllowInfinity(0, 10, 0.2), 2); + expect(lerpDoubleAllowInfinity(0, 10, 0.8), 8); + }); + + test('test lerpDoubleList', () { + const list1 = [ + 0, + 0, + 0, + ]; + const list2 = [ + 10, + 100, + 1000, + 10000, + ]; + expect(lerpDoubleList(list1, list2, 0), const [ + 0, + 0, + 0, + 10000, + ]); + expect(lerpDoubleList(list1, list2, 0.5), [ + 5, + 50, + 500, + 10000, + ]); + expect(lerpDoubleList(list1, list2, 1), list2); + }); + + test('test lerpIntList', () { + const list1 = [ + 0, + 0, + 0, + ]; + const list2 = [ + 10, + 100, + 1000, + 10000, + ]; + expect(lerpIntList(list1, list2, 0), const [ + 0, + 0, + 0, + 10000, + ]); + expect(lerpIntList(list1, list2, 0.5), [ + 5, + 50, + 500, + 10000, + ]); + expect(lerpIntList(list1, list2, 1), list2); + }); + + test('test lerpInt', () { + expect(lerpInt(0, 10, 1), 10); + expect(lerpInt(0, 10, 0.34), 3); + expect(lerpInt(0, 10, 0.38), 4); + }); + + test('test lerpNonNullDouble', () { + expect(lerpNonNullDouble(0, 10, 1), 10); + expect(lerpNonNullDouble(0, 10, 0.34), closeTo(3.4, tolerance)); + expect(lerpNonNullDouble(0, 10, 0.38), closeTo(3.8, tolerance)); + }); + + test('test lerpFlSpotList', () { + final list1 = [ + MockData.flSpot0, + MockData.flSpot0, + MockData.flSpot0, + ]; + final list2 = [ + MockData.flSpot1, + MockData.flSpot2, + MockData.flSpot3, + MockData.flSpot4, + ]; + expect(lerpFlSpotList(list1, list2, 0), [ + MockData.flSpot0, + MockData.flSpot0, + MockData.flSpot0, + MockData.flSpot4, + ]); + expect(lerpFlSpotList(list1, list2, 0.5), [ + const FlSpot(0.5, 0.5), + MockData.flSpot1, + const FlSpot(1.5, 1.5), + MockData.flSpot4, + ]); + expect(lerpFlSpotList(list1, list2, 1), list2); + }); + + test('test lerpHorizontalLineList', () { + final list1 = [ + MockData.horizontalLine0, + MockData.horizontalLine0, + MockData.horizontalLine0, + ]; + final list2 = [ + MockData.horizontalLine1, + MockData.horizontalLine2, + MockData.horizontalLine3, + MockData.horizontalLine4, + ]; + expect(lerpHorizontalLineList(list1, list2, 0), [ + MockData.horizontalLine0, + MockData.horizontalLine0, + MockData.horizontalLine0, + MockData.horizontalLine4, + ]); + expect(lerpHorizontalLineList(list1, list2, 0.5), [ + matchesHorizontalLine( + HorizontalLine(y: 0.5, color: const Color(0x08080808)), + ), + matchesHorizontalLine(MockData.horizontalLine1), + matchesHorizontalLine( + HorizontalLine(y: 1.5, color: const Color(0x19191919)), + ), + matchesHorizontalLine(MockData.horizontalLine4), + ]); + expect(lerpHorizontalLineList(list1, list2, 1), list2); + }); + + test('test lerpVerticalLineList', () { + final list1 = [ + MockData.verticalLine0, + MockData.verticalLine0, + MockData.verticalLine0, + ]; + final list2 = [ + MockData.verticalLine1, + MockData.verticalLine2, + MockData.verticalLine3, + MockData.verticalLine4, + ]; + expect(lerpVerticalLineList(list1, list2, 0), [ + MockData.verticalLine0, + MockData.verticalLine0, + MockData.verticalLine0, + MockData.verticalLine4, + ]); + expect(lerpVerticalLineList(list1, list2, 0.5), [ + matchesVerticalLine( + VerticalLine(x: 0.5, color: const Color(0x08080808)), + ), + matchesVerticalLine(MockData.verticalLine1), + matchesVerticalLine( + VerticalLine(x: 1.5, color: const Color(0x19191919)), + ), + matchesVerticalLine(MockData.verticalLine4), + ]); + expect(lerpVerticalLineList(list1, list2, 1), list2); + }); + + test('test lerpHorizontalRangeAnnotationList', () { + final list1 = [ + MockData.horizontalRangeAnnotation0, + MockData.horizontalRangeAnnotation0, + MockData.horizontalRangeAnnotation0, + ]; + final list2 = [ + MockData.horizontalRangeAnnotation1, + MockData.horizontalRangeAnnotation2, + MockData.horizontalRangeAnnotation3, + MockData.horizontalRangeAnnotation4, + ]; + expect(lerpHorizontalRangeAnnotationList(list1, list2, 0), [ + MockData.horizontalRangeAnnotation0, + MockData.horizontalRangeAnnotation0, + MockData.horizontalRangeAnnotation0, + MockData.horizontalRangeAnnotation4, + ]); + expect(lerpHorizontalRangeAnnotationList(list1, list2, 0.5), [ + matchesHorizontalRangeAnnotation( + HorizontalRangeAnnotation( + y1: 0.5, + y2: 1.5, + color: const Color(0x08080808), + ), + ), + matchesHorizontalRangeAnnotation(MockData.horizontalRangeAnnotation1), + matchesHorizontalRangeAnnotation( + HorizontalRangeAnnotation( + y1: 1.5, + y2: 2.5, + color: const Color(0x19191919), + ), + ), + matchesHorizontalRangeAnnotation(MockData.horizontalRangeAnnotation4), + ]); + expect(lerpHorizontalRangeAnnotationList(list1, list2, 1), list2); + }); + + test('test lerpVerticalRangeAnnotationList', () { + final list1 = [ + MockData.verticalRangeAnnotation0, + MockData.verticalRangeAnnotation0, + MockData.verticalRangeAnnotation0, + ]; + final list2 = [ + MockData.verticalRangeAnnotation1, + MockData.verticalRangeAnnotation2, + MockData.verticalRangeAnnotation3, + MockData.verticalRangeAnnotation4, + ]; + expect(lerpVerticalRangeAnnotationList(list1, list2, 0), [ + MockData.verticalRangeAnnotation0, + MockData.verticalRangeAnnotation0, + MockData.verticalRangeAnnotation0, + MockData.verticalRangeAnnotation4, + ]); + expect(lerpVerticalRangeAnnotationList(list1, list2, 0.5), [ + matchesVerticalRangeAnnotation( + VerticalRangeAnnotation( + x1: 0.5, + x2: 1.5, + color: const Color(0x08080808), + ), + ), + matchesVerticalRangeAnnotation(MockData.verticalRangeAnnotation1), + matchesVerticalRangeAnnotation( + VerticalRangeAnnotation( + x1: 1.5, + x2: 2.5, + color: const Color(0x19191919), + ), + ), + matchesVerticalRangeAnnotation(MockData.verticalRangeAnnotation4), + ]); + expect(lerpVerticalRangeAnnotationList(list1, list2, 1), list2); + }); + + test('test lerpBetweenBarsDataList', () { + final list1 = [ + MockData.verticalRangeAnnotation0, + MockData.verticalRangeAnnotation0, + MockData.verticalRangeAnnotation0, + ]; + final list2 = [ + MockData.verticalRangeAnnotation1, + MockData.verticalRangeAnnotation2, + MockData.verticalRangeAnnotation3, + MockData.verticalRangeAnnotation4, + ]; + expect(lerpVerticalRangeAnnotationList(list1, list2, 0), [ + MockData.verticalRangeAnnotation0, + MockData.verticalRangeAnnotation0, + MockData.verticalRangeAnnotation0, + MockData.verticalRangeAnnotation4, + ]); + expect(lerpVerticalRangeAnnotationList(list1, list2, 0.5), [ + matchesVerticalRangeAnnotation( + VerticalRangeAnnotation( + x1: 0.5, + x2: 1.5, + color: const Color(0x08080808), + ), + ), + matchesVerticalRangeAnnotation(MockData.verticalRangeAnnotation1), + matchesVerticalRangeAnnotation( + VerticalRangeAnnotation( + x1: 1.5, + x2: 2.5, + color: const Color(0x19191919), + ), + ), + matchesVerticalRangeAnnotation(MockData.verticalRangeAnnotation4), + ]); + expect(lerpVerticalRangeAnnotationList(list1, list2, 1), list2); + }); + + test('test lerpRadarEntryList', () { + final list1 = [ + MockData.radarEntry0, + MockData.radarEntry0, + MockData.radarEntry0, + ]; + final list2 = [ + MockData.radarEntry1, + MockData.radarEntry2, + MockData.radarEntry3, + MockData.radarEntry4, + ]; + expect(lerpRadarEntryList(list1, list2, 0), [ + MockData.radarEntry0, + MockData.radarEntry0, + MockData.radarEntry0, + MockData.radarEntry4, + ]); + expect(lerpRadarEntryList(list1, list2, 0.5), [ + const RadarEntry(value: 0.5), + MockData.radarEntry1, + const RadarEntry(value: 1.5), + MockData.radarEntry4, + ]); + expect(lerpRadarEntryList(list1, list2, 1), list2); + }); + + test('test lerpScatterSpotList', () { + final list1 = [ + MockData.scatterSpot0, + MockData.scatterSpot0, + MockData.scatterSpot0, + ]; + final list2 = [ + MockData.scatterSpot1, + MockData.scatterSpot2, + MockData.scatterSpot3, + MockData.scatterSpot4, + ]; + expect(lerpScatterSpotList(list1, list2, 0), [ + MockData.scatterSpot0, + MockData.scatterSpot0, + MockData.scatterSpot0, + MockData.scatterSpot4, + ]); + expect(lerpScatterSpotList(list1, list2, 0.5), [ + matchesScatterSpotWithCirclePainter( + ScatterSpot( + 0.5, + 0.5, + dotPainter: FlDotCirclePainter(color: const Color(0x08080808)), + ), + ), + matchesScatterSpotWithCirclePainter(MockData.scatterSpot1), + matchesScatterSpotWithCirclePainter( + ScatterSpot( + 1.5, + 1.5, + dotPainter: FlDotCirclePainter(color: const Color(0x19191919)), + ), + ), + matchesScatterSpotWithCirclePainter(MockData.scatterSpot4), + ]); + expect(lerpScatterSpotList(list1, list2, 1), list2); + }); + + test('test lerpGradient', () { + final colors = [ + MockData.color0, + MockData.color1, + MockData.color2, + MockData.color3, + ]; + expect( + lerpGradient(colors, [], 0), + isSameColorAs(const Color(0x00000000)), + ); + expect( + lerpGradient(colors, [], 0.2), + isSameColorAs(const Color(0x00000000)), + ); + expect( + lerpGradient(colors, [], 0.4), + isSameColorAs(const Color(0x0a0a0a0a)), + ); + expect( + lerpGradient(colors, [], 0.6), + isSameColorAs(const Color(0x17171717)), + ); + expect( + lerpGradient(colors, [], 0.8), + isSameColorAs(const Color(0x25252525)), + ); + expect( + lerpGradient(colors, [], 1), + isSameColorAs(const Color(0x33333333)), + ); + }); + + test('lerpLineChartBarDataList uses `LineChartBarData.lerp`', () { + final list1 = [ + MockData.lineChartBarData1, + MockData.lineChartBarData1, + MockData.lineChartBarData1, + ]; + final list2 = [ + MockData.lineChartBarData2, + MockData.lineChartBarData2, + MockData.lineChartBarData2, + MockData.lineChartBarData2, + ]; + + expect( + lerpLineChartBarDataList(list1, list2, 1), + lerpList(list1, list2, 1, lerp: LineChartBarData.lerp), + ); + }); + + test('lerpBetweenBarsDataList uses `BetweenBarsData.lerp`', () { + final list1 = [ + betweenBarsData1, + betweenBarsData1, + betweenBarsData1, + ]; + final list2 = [ + betweenBarsData2, + betweenBarsData2, + betweenBarsData2, + betweenBarsData2, + ]; + + expect( + lerpBetweenBarsDataList(list1, list2, 1), + lerpList(list1, list2, 1, lerp: BetweenBarsData.lerp), + ); + }); + + test('lerpBarChartGroupDataList uses `BarChartGroupData.lerp`', () { + final list1 = [ + barChartGroupData1, + barChartGroupData1, + barChartGroupData1, + ]; + final list2 = [ + barChartGroupData2, + barChartGroupData2, + barChartGroupData2, + barChartGroupData2, + ]; + + expect( + lerpBarChartGroupDataList(list1, list2, 1), + lerpList(list1, list2, 1, lerp: BarChartGroupData.lerp), + ); + }); + + test('lerpBarChartRodDataList uses `BarChartRodData.lerp`', () { + final list1 = [ + barChartRodData1, + barChartRodData1, + barChartRodData1, + ]; + final list2 = [ + barChartRodData2, + barChartRodData2, + barChartRodData2, + barChartRodData2, + ]; + + expect( + lerpBarChartRodDataList(list1, list2, 1), + lerpList(list1, list2, 1, lerp: BarChartRodData.lerp), + ); + }); + + test('lerpPieChartSectionDataList uses `PieChartSectionData.lerp`', () { + final list1 = [ + MockData.pieChartSectionData1, + MockData.pieChartSectionData1, + MockData.pieChartSectionData1, + ]; + final list2 = [ + MockData.pieChartSectionData2, + MockData.pieChartSectionData2, + MockData.pieChartSectionData2, + MockData.pieChartSectionData2, + ]; + + expect( + lerpPieChartSectionDataList(list1, list2, 1), + lerpList(list1, list2, 1, lerp: PieChartSectionData.lerp), + ); + }); + + test('lerpBarChartRodStackList uses `BarChartRodStackItem.lerp`', () { + final list1 = [ + barChartRodStackItem1, + barChartRodStackItem1, + barChartRodStackItem1, + ]; + final list2 = [ + barChartRodStackItem2, + barChartRodStackItem2, + barChartRodStackItem2, + barChartRodStackItem2, + ]; + + expect( + lerpBarChartRodStackList(list1, list2, 1), + lerpList(list1, list2, 1, lerp: BarChartRodStackItem.lerp), + ); + }); + + test('lerpRadarDataSetList uses `RadarDataSet.lerp`', () { + final list1 = [ + MockData.radarDataSet1, + MockData.radarDataSet1, + MockData.radarDataSet1, + ]; + final list2 = [ + MockData.radarDataSet2, + MockData.radarDataSet2, + MockData.radarDataSet2, + MockData.radarDataSet2, + ]; + + expect( + lerpRadarDataSetList(list1, list2, 1), + lerpList(list1, list2, 1, lerp: RadarDataSet.lerp), + ); + }); +} diff --git a/test/utils/utils_test.dart b/test/utils/utils_test.dart new file mode 100644 index 0000000..bbe4867 --- /dev/null +++ b/test/utils/utils_test.dart @@ -0,0 +1,355 @@ +import 'package:fl_chart/src/utils/lerp.dart'; +import 'package:fl_chart/src/utils/utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; + +import '../chart/data_pool.dart'; +import 'utils_test.mocks.dart'; + +@GenerateMocks([Utils, BuildContext]) +void main() { + const tolerance = 0.001; + + test('changeInstance', () { + final Utils mockUtils = MockUtils(); + final realUtils = Utils(); + expect(Utils(), realUtils); + Utils.changeInstance(mockUtils); + expect(Utils(), mockUtils); + expect(Utils() != realUtils, true); + Utils.changeInstance(realUtils); + expect(Utils(), realUtils); + expect(Utils() != mockUtils, true); + }); + + test('test degrees to radians', () { + expect(Utils().radians(57.2958), closeTo(1, tolerance)); + expect(Utils().radians(120), closeTo(2.0944, tolerance)); + expect(Utils().radians(324), closeTo(5.65487, tolerance)); + expect(Utils().radians(180), closeTo(3.1415, tolerance)); + }); + + test('test radians to degree', () { + expect(Utils().degrees(1.5), closeTo(85.9437, tolerance)); + expect(Utils().degrees(1.8), closeTo(103.132, tolerance)); + expect(Utils().degrees(1.2), closeTo(68.7549, tolerance)); + }); + + test('translate rotated position', () { + expect(Utils().translateRotatedPosition(100, 90), 25); + expect(Utils().translateRotatedPosition(100, 0), 0); + }); + + test('calculateRotationOffset()', () { + expect(Utils().calculateRotationOffset(MockData.size1, 90), Offset.zero); + expect( + Utils().calculateRotationOffset(MockData.size1, 45).dx, + closeTo(-2.278, tolerance), + ); + expect( + Utils().calculateRotationOffset(MockData.size1, 45).dy, + closeTo(-2.278, tolerance), + ); + + expect( + Utils().calculateRotationOffset(MockData.size1, 180).dx, + closeTo(0.0, tolerance), + ); + expect( + Utils().calculateRotationOffset(MockData.size1, 180).dy, + closeTo(0.0, tolerance), + ); + + expect( + Utils().calculateRotationOffset(MockData.size1, 220).dx, + closeTo(-2.2485, tolerance), + ); + expect( + Utils().calculateRotationOffset(MockData.size1, 220).dy, + closeTo(-2.2485, tolerance), + ); + + expect( + Utils().calculateRotationOffset(MockData.size1, 350).dx, + closeTo(-0.87150, tolerance), + ); + expect( + Utils().calculateRotationOffset(MockData.size1, 350).dy, + closeTo(-0.87150, tolerance), + ); + }); + + test('normalizeBorderRadius()', () { + const input1 = BorderRadius.only( + topLeft: Radius.circular(18), + topRight: Radius.circular(18), + bottomRight: Radius.circular(18), + bottomLeft: Radius.circular(18), + ); + const output1 = input1; + expect(Utils().normalizeBorderRadius(input1, 40), output1); + + const input2 = BorderRadius.only( + topLeft: Radius.circular(24), + topRight: Radius.circular(24), + bottomRight: Radius.circular(24), + bottomLeft: Radius.circular(24), + ); + const output2 = BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + bottomRight: Radius.circular(20), + bottomLeft: Radius.circular(20), + ); + expect(Utils().normalizeBorderRadius(input2, 40), output2); + }); + + test('normalizeBorderSide()', () { + const input1 = BorderSide(width: 4); + const output1 = input1; + expect(Utils().normalizeBorderSide(input1, 40), output1); + + const input2 = BorderSide(width: 24); + const output2 = BorderSide(width: 20); + expect(Utils().normalizeBorderSide(input2, 40), output2); + }); + + test('lerp gradient', () { + expect( + lerpGradient( + [ + Colors.red, + Colors.green, + ], + [], + 0, + ), + Colors.red, + ); + + expect( + lerpGradient( + [ + Colors.red, + Colors.green, + ], + [], + 1, + ), + Colors.green, + ); + }); + + test('test roundInterval', () { + expect(Utils().roundInterval(99), 100); + expect(Utils().roundInterval(75), 50); + expect(Utils().roundInterval(76), 100); + expect(Utils().roundInterval(60), 50); + expect(Utils().roundInterval(0.000123), 0.0001); + expect(Utils().roundInterval(0.000190), 0.0002); + expect(Utils().roundInterval(0.000200), 0.0002); + expect(Utils().roundInterval(0.000390000000), 0.0005); + expect(Utils().roundInterval(0.000990000000), 0.001); + expect(Utils().roundInterval(0.00000990000), 0.00001000); + expect(Utils().roundInterval(0.0000009), 0.0000009); + expect( + Utils().roundInterval(0.000000000000000000990000000), + 0.000000000000000000990000000, + ); + expect(Utils().roundInterval(0.000004901960784313726), 0.000005); + }); + + test('test Utils().getEfficientInterval', () { + expect(Utils().getEfficientInterval(472, 340, pixelPerInterval: 10), 5); + expect(Utils().getEfficientInterval(820, 10000, pixelPerInterval: 10), 100); + expect( + Utils().getEfficientInterval(1024, 412345234, pixelPerInterval: 10), + 5000000, + ); + expect( + Utils().getEfficientInterval(720, 812394712349, pixelPerInterval: 10), + 10000000000, + ); + expect( + Utils().getEfficientInterval(1024, 0.01, pixelPerInterval: 100), + 0.001, + ); + expect( + Utils().getEfficientInterval(1024, 0.0005, pixelPerInterval: 10), + 0.000005, + ); + expect(Utils().getEfficientInterval(200, 0.5, pixelPerInterval: 20), 0.05); + expect(Utils().getEfficientInterval(200, 1, pixelPerInterval: 20), 0.1); + expect(Utils().getEfficientInterval(100, 0.5, pixelPerInterval: 20), 0.1); + expect(Utils().getEfficientInterval(10, 10), 10); + expect(Utils().getEfficientInterval(10, 0), 1); + }); + + test('test getFractionDigits', () { + expect(Utils().getFractionDigits(1), 1); + expect(Utils().getFractionDigits(343), 1); + expect(Utils().getFractionDigits(22), 1); + expect(Utils().getFractionDigits(444444), 1); + expect(Utils().getFractionDigits(0.9), 2); + expect(Utils().getFractionDigits(0.1), 2); + expect(Utils().getFractionDigits(0.01), 3); + expect(Utils().getFractionDigits(0.001), 4); + expect(Utils().getFractionDigits(0.009), 4); + expect(Utils().getFractionDigits(0.008), 4); + expect(Utils().getFractionDigits(0.0001), 5); + expect(Utils().getFractionDigits(0.0009), 5); + expect(Utils().getFractionDigits(0.00001), 6); + expect(Utils().getFractionDigits(0.000001), 7); + expect(Utils().getFractionDigits(0.0000001), 8); + expect(Utils().getFractionDigits(0.00000001), 9); + }); + + test('test formatNumber', () { + expect(Utils().formatNumber(0, 10, 0), '0'); + expect(Utils().formatNumber(0, 10, -0), '0'); + expect(Utils().formatNumber(0, 10, -0.01), '0'); + expect(Utils().formatNumber(0, 10, 0.01), '0'); + expect(Utils().formatNumber(0, 10, -0.1), '-0.1'); + expect(Utils().formatNumber(0, 10, 423), '423'); + expect(Utils().formatNumber(0, 10, -423), '-423'); + expect(Utils().formatNumber(0, 10, 1000), '1K'); + expect(Utils().formatNumber(0, 10, 1234), '1.2K'); + expect(Utils().formatNumber(0, 10, 10000), '10K'); + expect(Utils().formatNumber(0, 10, 41234), '41.2K'); + expect(Utils().formatNumber(0, 10, 82349), '82.3K'); + expect(Utils().formatNumber(0, 10, 82350), '82.3K'); + expect(Utils().formatNumber(0, 10, 82351), '82.4K'); + expect(Utils().formatNumber(0, 10, -82351), '-82.4K'); + expect(Utils().formatNumber(0, 10, 100000), '100K'); + expect(Utils().formatNumber(0, 10, 101000), '101K'); + expect(Utils().formatNumber(0, 10, 2345123), '2.3M'); + expect(Utils().formatNumber(0, 10, 2352123), '2.4M'); + expect(Utils().formatNumber(0, 10, -2352123), '-2.4M'); + expect(Utils().formatNumber(0, 10, 521000000), '521M'); + expect(Utils().formatNumber(0, 10, 4324512345), '4.3B'); + expect(Utils().formatNumber(0, 10, 4000000000), '4B'); + expect(Utils().formatNumber(0, 10, -4000000000), '-4B'); + expect(Utils().formatNumber(0, 10, 823147521343), '823.1B'); + expect(Utils().formatNumber(0, 10, 8231475213435), '8231.5B'); + expect(Utils().formatNumber(0, 10, -8231475213435), '-8231.5B'); + }); + + group('test getThemeAwareTextStyle', () { + testWidgets('test 1', (WidgetTester tester) async { + const style = TextStyle(color: Colors.brown); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DefaultTextStyle( + style: style, + child: Container(), + ), + ), + ), + ); + await tester.pumpAndSettle(); + final context = tester.element(find.byType(Container)); + + expect( + Utils().getThemeAwareTextStyle(context, style), + style, + ); + }); + + testWidgets('test 2', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: MediaQuery( + data: const MediaQueryData(boldText: true), + child: Container(), + ), + ), + ), + ); + await tester.pumpAndSettle(); + final context = tester.element(find.byType(Container)); + + expect( + Utils() + .getThemeAwareTextStyle( + context, + MockData.textStyle1, + ) + .fontWeight, + FontWeight.bold, + ); + }); + }); + + test('test getInitialIntervalValue()', () { + expect(Utils().getBestInitialIntervalValue(-3, 3, 2), -2); + expect(Utils().getBestInitialIntervalValue(-3, 3, 1), -3); + expect(Utils().getBestInitialIntervalValue(-30, -20, 13), -26); + expect(Utils().getBestInitialIntervalValue(0, 13, 8), 0); + expect(Utils().getBestInitialIntervalValue(1, 13, 7), 7); + expect(Utils().getBestInitialIntervalValue(1, 13, 3), 3); + expect(Utils().getBestInitialIntervalValue(-1, 13, 3), 0); + expect(Utils().getBestInitialIntervalValue(-2, 13, 3), 0); + expect(Utils().getBestInitialIntervalValue(-3, 13, 3), -3); + expect(Utils().getBestInitialIntervalValue(-4, 13, 3), -3); + expect(Utils().getBestInitialIntervalValue(-5, 13, 3), -3); + expect(Utils().getBestInitialIntervalValue(-6, 13, 3), -6); + expect(Utils().getBestInitialIntervalValue(-6.5, 13, 3), -6); + expect(Utils().getBestInitialIntervalValue(-1, 1, 2), 0); + expect(Utils().getBestInitialIntervalValue(-1, 2, 2), 0); + expect(Utils().getBestInitialIntervalValue(-2, 0, 2), -2); + expect(Utils().getBestInitialIntervalValue(-3, 0, 2), -2); + expect(Utils().getBestInitialIntervalValue(-4, 0, 2), -4); + expect(Utils().getBestInitialIntervalValue(-0.5, 0.5, 2), 0); + expect(Utils().getBestInitialIntervalValue(35, 130, 50), 50); + expect(Utils().getBestInitialIntervalValue(49, 130, 50), 50); + expect(Utils().getBestInitialIntervalValue(50, 130, 50), 50); + expect(Utils().getBestInitialIntervalValue(60, 130, 50), 100); + expect(Utils().getBestInitialIntervalValue(110, 130, 50), 110); + expect(Utils().getBestInitialIntervalValue(90, 180, 50), 100); + expect(Utils().getBestInitialIntervalValue(100, 180, 50), 100); + expect(Utils().getBestInitialIntervalValue(110, 180, 50), 150); + expect(Utils().getBestInitialIntervalValue(170, 180, 50), 170); + expect(Utils().getBestInitialIntervalValue(-120, -10, 50), -100); + expect(Utils().getBestInitialIntervalValue(-110, -10, 50), -100); + expect(Utils().getBestInitialIntervalValue(-100, -10, 50), -100); + expect(Utils().getBestInitialIntervalValue(-90, -10, 50), -50); + expect(Utils().getBestInitialIntervalValue(-80, -10, 50), -50); + expect(Utils().getBestInitialIntervalValue(-150, -10, 50), -150); + expect(Utils().getBestInitialIntervalValue(-10, 10, 2, baseline: -1), -9); + expect(Utils().getBestInitialIntervalValue(-10, 10, 2, baseline: -20), -10); + expect(Utils().getBestInitialIntervalValue(-10, 10, 15, baseline: -30), 0); + expect(Utils().getBestInitialIntervalValue(0, 20, 8, baseline: 28), 4); + expect(Utils().getBestInitialIntervalValue(130, 140, 50), 130); + expect(Utils().getBestInitialIntervalValue(145, 155, 50), 150); + expect( + Utils().getBestInitialIntervalValue(-200, -180, 30), + -200, + ); + expect( + Utils().getBestInitialIntervalValue(-190, -170, 30), + -180, + ); + expect( + Utils().getBestInitialIntervalValue(-2000, 2000, 100, baseline: -10000), + -2000, + ); + expect( + Utils().getBestInitialIntervalValue(-120, 120, 33, baseline: -200), + -101, + ); + expect( + Utils().getBestInitialIntervalValue(120, 180, 60, baseline: 2000), + 140, + ); + expect(Utils().getBestInitialIntervalValue(-10, 10, 4, baseline: 3), -9); + }); + + test('test convertRadiusToSigma()', () { + expect(Utils().convertRadiusToSigma(10), closeTo(6.2735, tolerance)); + expect(Utils().convertRadiusToSigma(42), closeTo(24.7487, tolerance)); + expect(Utils().convertRadiusToSigma(26), closeTo(15.5111, tolerance)); + }); +} diff --git a/test/utils/utils_test.mocks.dart b/test/utils/utils_test.mocks.dart new file mode 100644 index 0000000..320b49e --- /dev/null +++ b/test/utils/utils_test.mocks.dart @@ -0,0 +1,359 @@ +// Mocks generated by Mockito 5.4.5 from annotations +// in fl_chart/test/utils/utils_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:ui' as _i2; + +import 'package:fl_chart/src/utils/utils.dart' as _i5; +import 'package:flutter/foundation.dart' as _i4; +import 'package:flutter/material.dart' as _i3; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i6; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeOffset_0 extends _i1.SmartFake implements _i2.Offset { + _FakeOffset_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeBorderSide_1 extends _i1.SmartFake implements _i3.BorderSide { + _FakeBorderSide_1(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeTextStyle_2 extends _i1.SmartFake implements _i3.TextStyle { + _FakeTextStyle_2(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeWidget_3 extends _i1.SmartFake implements _i3.Widget { + _FakeWidget_3(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeInheritedWidget_4 extends _i1.SmartFake + implements _i3.InheritedWidget { + _FakeInheritedWidget_4(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({_i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeDiagnosticsNode_5 extends _i1.SmartFake + implements _i3.DiagnosticsNode { + _FakeDiagnosticsNode_5(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); + + @override + String toString({ + _i4.TextTreeConfiguration? parentConfiguration, + _i3.DiagnosticLevel? minLevel = _i3.DiagnosticLevel.info, + }) => super.toString(); +} + +/// A class which mocks [Utils]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockUtils extends _i1.Mock implements _i5.Utils { + MockUtils() { + _i1.throwOnMissingStub(this); + } + + @override + double radians(double? degrees) => + (super.noSuchMethod( + Invocation.method(#radians, [degrees]), + returnValue: 0.0, + ) + as double); + + @override + double degrees(double? radians) => + (super.noSuchMethod( + Invocation.method(#degrees, [radians]), + returnValue: 0.0, + ) + as double); + + @override + double translateRotatedPosition(double? size, double? degree) => + (super.noSuchMethod( + Invocation.method(#translateRotatedPosition, [size, degree]), + returnValue: 0.0, + ) + as double); + + @override + _i2.Offset calculateRotationOffset(_i2.Size? size, double? degree) => + (super.noSuchMethod( + Invocation.method(#calculateRotationOffset, [size, degree]), + returnValue: _FakeOffset_0( + this, + Invocation.method(#calculateRotationOffset, [size, degree]), + ), + ) + as _i2.Offset); + + @override + _i3.BorderRadius? normalizeBorderRadius( + _i3.BorderRadius? borderRadius, + double? width, + ) => + (super.noSuchMethod( + Invocation.method(#normalizeBorderRadius, [borderRadius, width]), + ) + as _i3.BorderRadius?); + + @override + _i3.BorderSide normalizeBorderSide( + _i3.BorderSide? borderSide, + double? width, + ) => + (super.noSuchMethod( + Invocation.method(#normalizeBorderSide, [borderSide, width]), + returnValue: _FakeBorderSide_1( + this, + Invocation.method(#normalizeBorderSide, [borderSide, width]), + ), + ) + as _i3.BorderSide); + + @override + double getEfficientInterval( + double? axisViewSize, + double? diffInAxis, { + double? pixelPerInterval = 40.0, + }) => + (super.noSuchMethod( + Invocation.method( + #getEfficientInterval, + [axisViewSize, diffInAxis], + {#pixelPerInterval: pixelPerInterval}, + ), + returnValue: 0.0, + ) + as double); + + @override + double roundInterval(double? input) => + (super.noSuchMethod( + Invocation.method(#roundInterval, [input]), + returnValue: 0.0, + ) + as double); + + @override + int getFractionDigits(double? value) => + (super.noSuchMethod( + Invocation.method(#getFractionDigits, [value]), + returnValue: 0, + ) + as int); + + @override + String formatNumber(double? axisMin, double? axisMax, double? axisValue) => + (super.noSuchMethod( + Invocation.method(#formatNumber, [axisMin, axisMax, axisValue]), + returnValue: _i6.dummyValue( + this, + Invocation.method(#formatNumber, [axisMin, axisMax, axisValue]), + ), + ) + as String); + + @override + _i3.TextStyle getThemeAwareTextStyle( + _i3.BuildContext? context, + _i3.TextStyle? providedStyle, + ) => + (super.noSuchMethod( + Invocation.method(#getThemeAwareTextStyle, [ + context, + providedStyle, + ]), + returnValue: _FakeTextStyle_2( + this, + Invocation.method(#getThemeAwareTextStyle, [ + context, + providedStyle, + ]), + ), + ) + as _i3.TextStyle); + + @override + double getBestInitialIntervalValue( + double? min, + double? max, + double? interval, { + double? baseline = 0.0, + }) => + (super.noSuchMethod( + Invocation.method( + #getBestInitialIntervalValue, + [min, max, interval], + {#baseline: baseline}, + ), + returnValue: 0.0, + ) + as double); + + @override + double convertRadiusToSigma(double? radius) => + (super.noSuchMethod( + Invocation.method(#convertRadiusToSigma, [radius]), + returnValue: 0.0, + ) + as double); +} + +/// A class which mocks [BuildContext]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockBuildContext extends _i1.Mock implements _i3.BuildContext { + MockBuildContext() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Widget get widget => + (super.noSuchMethod( + Invocation.getter(#widget), + returnValue: _FakeWidget_3(this, Invocation.getter(#widget)), + ) + as _i3.Widget); + + @override + bool get mounted => + (super.noSuchMethod(Invocation.getter(#mounted), returnValue: false) + as bool); + + @override + bool get debugDoingBuild => + (super.noSuchMethod( + Invocation.getter(#debugDoingBuild), + returnValue: false, + ) + as bool); + + @override + _i3.InheritedWidget dependOnInheritedElement( + _i3.InheritedElement? ancestor, { + Object? aspect, + }) => + (super.noSuchMethod( + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + returnValue: _FakeInheritedWidget_4( + this, + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + ), + ) + as _i3.InheritedWidget); + + @override + void visitAncestorElements(_i3.ConditionalElementVisitor? visitor) => + super.noSuchMethod( + Invocation.method(#visitAncestorElements, [visitor]), + returnValueForMissingStub: null, + ); + + @override + void visitChildElements(_i3.ElementVisitor? visitor) => super.noSuchMethod( + Invocation.method(#visitChildElements, [visitor]), + returnValueForMissingStub: null, + ); + + @override + void dispatchNotification(_i3.Notification? notification) => + super.noSuchMethod( + Invocation.method(#dispatchNotification, [notification]), + returnValueForMissingStub: null, + ); + + @override + _i3.DiagnosticsNode describeElement( + String? name, { + _i4.DiagnosticsTreeStyle? style = _i4.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method(#describeElement, [name], {#style: style}), + returnValue: _FakeDiagnosticsNode_5( + this, + Invocation.method(#describeElement, [name], {#style: style}), + ), + ) + as _i3.DiagnosticsNode); + + @override + _i3.DiagnosticsNode describeWidget( + String? name, { + _i4.DiagnosticsTreeStyle? style = _i4.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method(#describeWidget, [name], {#style: style}), + returnValue: _FakeDiagnosticsNode_5( + this, + Invocation.method(#describeWidget, [name], {#style: style}), + ), + ) + as _i3.DiagnosticsNode); + + @override + List<_i3.DiagnosticsNode> describeMissingAncestor({ + required Type? expectedAncestorType, + }) => + (super.noSuchMethod( + Invocation.method(#describeMissingAncestor, [], { + #expectedAncestorType: expectedAncestorType, + }), + returnValue: <_i3.DiagnosticsNode>[], + ) + as List<_i3.DiagnosticsNode>); + + @override + _i3.DiagnosticsNode describeOwnershipChain(String? name) => + (super.noSuchMethod( + Invocation.method(#describeOwnershipChain, [name]), + returnValue: _FakeDiagnosticsNode_5( + this, + Invocation.method(#describeOwnershipChain, [name]), + ), + ) + as _i3.DiagnosticsNode); +}