Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

### Bug fixes

* flet-charts: Fix `ScatterChart` failing to render when a `ScatterChartSpot` has `x_error` or `y_error` set. Error ranges are now set with the new `ChartErrorRange` type and drawn as error bars in the color of their spot ([#6885](https://github.com/flet-dev/flet/pull/6885)) by @ndonkoHenri.
* Fix iOS apps that use `sqlite3` stopping with `Assertion failed: (0), function isBulkReadStatement` when run from Xcode on iOS 18 and later, by turning off Xcode's Thread Performance Checker in the scheme generated by `flet build` ([#5480](https://github.com/flet-dev/flet/issues/5480), [#6882](https://github.com/flet-dev/flet/pull/6882)) by @ndonkoHenri.
* Fix `Button(icon=...)` and other material buttons rendering an error box when `content` is not set. Icon-only buttons now render the icon centered ([#6886](https://github.com/flet-dev/flet/issues/6886), [#6889](https://github.com/flet-dev/flet/pull/6889)) by @FeodorFitsner.
* Fix `flet debug`, `flet build`, `flet test`, `flet emulators` and `flet run` rejecting a positional argument typed after an option on Python 3.10, 3.11, 3.12.0-3.12.6 and 3.13.0 - `flet debug ios --device-id X my_app` failed with `unrecognized arguments: my_app` - by backporting the upstream `argparse` fix (CPython gh-59317). Also, an app path pointing to a file such as `main.py` is now rejected with a clear error instead of crashing on `main.py/build`, and a bad app path fails before the Flutter toolchain is set up ([#6840](https://github.com/flet-dev/flet/issues/6840), [#6875](https://github.com/flet-dev/flet/pull/6875)) by @ndonkoHenri.
Expand Down
3 changes: 3 additions & 0 deletions sdk/python/Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ tasks:
desc: "Start Docusaurus dev server for the documentation website with hot reload."
aliases:
- docs
env:
YARN_ENABLE_TELEMETRY: "0"
cmds:
- cd ../../website && yarn install --immutable
- cd ../../website && yarn start
Expand All @@ -78,6 +80,7 @@ tasks:
- docs-fast
env:
FLET_DOCS_FAST: "1"
YARN_ENABLE_TELEMETRY: "0"
cmds:
- cd ../../website && yarn install --immutable
- cd ../../website && yarn start
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import statistics

import flet as ft
import flet_charts as fch

# Five timings of one swing (in seconds) for each pendulum string length (in cm).
TIMINGS = {
20: [0.84, 0.95, 0.90, 0.88, 0.93],
40: [1.22, 1.33, 1.27, 1.25, 1.30],
60: [1.50, 1.62, 1.55, 1.52, 1.58],
80: [1.73, 1.86, 1.80, 1.76, 1.82],
100: [1.94, 2.09, 2.00, 1.98, 2.03],
120: [2.13, 2.29, 2.19, 2.17, 2.24],
}

# How far off each measured string length may be, in cm.
LENGTH_UNCERTAINTY = 3


def main(page: ft.Page):
grid_color = ft.Colors.with_opacity(0.2, ft.Colors.ON_SURFACE)

spots = []
for length, timings in TIMINGS.items():
mean = statistics.fmean(timings)
spots.append(
fch.ScatterChartSpot(
x=length,
y=mean,
radius=6,
color=ft.Colors.INDIGO,
tooltip=f"{mean:.2f} s",
x_error=fch.ChartErrorRange.symmetric(LENGTH_UNCERTAINTY),
y_error=fch.ChartErrorRange(
lower_by=mean - min(timings),
upper_by=max(timings) - mean,
),
)
)

page.add(
ft.SafeArea(
expand=True,
content=ft.Column(
expand=True,
controls=[
ft.Text(
"Mean period of a pendulum over five timings. Vertical bars "
"span the fastest and slowest timing, horizontal bars the "
f"±{LENGTH_UNCERTAINTY} cm uncertainty of the string length."
),
fch.ScatterChart(
expand=True,
min_x=0,
max_x=140,
min_y=0.5,
max_y=2.5,
border=ft.Border.all(1, grid_color),
horizontal_grid_lines=fch.ChartGridLines(
interval=0.5, color=grid_color, width=1
),
vertical_grid_lines=fch.ChartGridLines(
interval=20, color=grid_color, width=1
),
left_axis=fch.ChartAxis(
title=ft.Text("Period (s)"),
title_size=24,
label_spacing=0.5,
label_size=40,
),
bottom_axis=fch.ChartAxis(
title=ft.Text("String length (cm)"),
title_size=24,
label_spacing=20,
label_size=28,
show_max=False,
),
spots=spots,
),
],
),
)
)


if __name__ == "__main__":
ft.run(main)
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[project]
name = "charts-scatter-chart-with-error-bars"
version = "1.0.0"
description = "Scatter chart of pendulum measurements with horizontal and vertical error bars showing the uncertainty of each point."
requires-python = ">=3.10"
keywords = ["charts", "scatter-chart", "error-bars", "flet"]
authors = [{ name = "Flet team", email = "hello@flet.dev" }]
dependencies = ["flet", "flet-charts"]

[dependency-groups]
dev = ["flet-cli", "flet-desktop", "flet-web"]

[tool.flet.gallery]
categories = ["Charts/ScatterChart"]

[tool.flet.metadata]
title = "Scatter chart with error bars"
controls = ["SafeArea", "ScatterChart"]
layout_pattern = "single-chart"
complexity = "basic"
features = ["chart visualization", "error bars"]

[tool.flet]
org = "dev.flet"
company = "Flet"
copyright = "Copyright (C) 2023-2026 by Flet"
6 changes: 6 additions & 0 deletions sdk/python/packages/flet-charts/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## 1.0.2

### Fixed

- Fixed `ScatterChart` failing to render when a `ScatterChartSpot` has `x_error` or `y_error` set. Error ranges are now set with the new `ChartErrorRange` type and drawn as error bars in the color of their spot ([#6885](https://github.com/flet-dev/flet/pull/6885)) by @ndonkoHenri.

## 1.0.0

### Fixed
Expand Down
2 changes: 2 additions & 0 deletions sdk/python/packages/flet-charts/src/flet_charts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
ChartCirclePoint,
ChartCrossPoint,
ChartDataPointTooltip,
ChartErrorRange,
ChartEventType,
ChartGridLines,
ChartPointLine,
Expand Down Expand Up @@ -85,6 +86,7 @@
"ChartCirclePoint",
"ChartCrossPoint",
"ChartDataPointTooltip",
"ChartErrorRange",
"ChartEventType",
"ChartGridLines",
"ChartPointLine",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from dataclasses import field
from typing import Any, Optional, Union
from typing import Optional, Union

import flet as ft
from flet_charts.types import ChartDataPointTooltip, ChartPointShape
from flet_charts.types import ChartDataPointTooltip, ChartErrorRange, ChartPointShape

__all__ = ["ScatterChartSpot", "ScatterChartSpotTooltip"]

Expand Down Expand Up @@ -86,18 +86,24 @@ class ScatterChartSpot(ft.BaseControl):
Sort by this to manage overlap.
"""

x_error: Optional[Any] = None
x_error: Optional[ChartErrorRange] = None
"""
Determines the error range of the data point using
[FlErrorRange](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#flerrorrange)
(which contains lowerBy and upperValue) for the `X` axis.
The error range of this spot on the `X` axis, drawn as a horizontal error bar
in the color of this spot.

Automatic axis bounds cover spot positions only, so set
:attr:`flet_charts.ScatterChart.min_x` and
:attr:`flet_charts.ScatterChart.max_x` to keep the error bar inside the chart.
"""

y_error: Optional[Any] = None
y_error: Optional[ChartErrorRange] = None
"""
Determines the error range of the data point using
[FlErrorRange](https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/base_chart.md#flerrorrange)
(which contains lowerBy and upperValue) for the `Y` axis.
The error range of this spot on the `Y` axis, drawn as a vertical error bar
in the color of this spot.

Automatic axis bounds cover spot positions only, so set
:attr:`flet_charts.ScatterChart.min_y` and
:attr:`flet_charts.ScatterChart.max_y` to keep the error bar inside the chart.
"""

selected: bool = False
Expand Down
61 changes: 61 additions & 0 deletions sdk/python/packages/flet-charts/src/flet_charts/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"ChartCirclePoint",
"ChartCrossPoint",
"ChartDataPointTooltip",
"ChartErrorRange",
"ChartEventType",
"ChartGridLines",
"ChartPointLine",
Expand Down Expand Up @@ -414,6 +415,66 @@ def copy(
)


@ft.value
class ChartErrorRange:
"""
The uncertainty of a data point's value on one axis, drawn as an error bar.

Both bounds are measured from the value, so the range spans from
`value - lower_by` to `value + upper_by`.
"""

lower_by: ft.Number
"""
How far the range extends below the value.

Raises:
ValueError: If it is not greater than or equal to `0`.
"""

upper_by: ft.Number
"""
How far the range extends above the value.

Raises:
ValueError: If it is not greater than or equal to `0`.
"""

def __post_init__(self):
Comment thread
ndonkoHenri marked this conversation as resolved.
if not self.lower_by >= 0:
raise ValueError(
f"lower_by must be greater than or equal to 0, got {self.lower_by}"
)
if not self.upper_by >= 0:
raise ValueError(
f"upper_by must be greater than or equal to 0, got {self.upper_by}"
)

@classmethod
def symmetric(cls, value: ft.Number) -> "ChartErrorRange":
"""
Creates a range that extends equally below and above the value.

Args:
value: The distance of both bounds from the value.
"""
return cls(lower_by=value, upper_by=value)

def copy(
self,
*,
lower_by: Optional[ft.Number] = None,
upper_by: Optional[ft.Number] = None,
) -> "ChartErrorRange":
"""
Returns a copy of this object with the specified properties overridden.
"""
return ChartErrorRange(
lower_by=lower_by if lower_by is not None else self.lower_by,
upper_by=upper_by if upper_by is not None else self.upper_by,
)


class HorizontalAlignment(Enum):
"""Defines an element's horizontal alignment to given point."""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ class _ScatterChartControlState extends State<ScatterChartControl> {
return ScatterSpot(x, y,
show: spot.visible,
renderPriority: spot.getInt('render_priority', 0)!,
xError: spot.get('x_error'),
yError: spot.get('y_error'),
xError: parseFlErrorRange(spot.get('x_error')),
yError: parseFlErrorRange(spot.get('y_error')),
dotPainter: spot.get("point") != null
? parseChartDotPainter(spot.get("point"), theme, 0, null, null)
: FlDotCirclePainter(
Expand Down Expand Up @@ -122,7 +122,8 @@ class _ScatterChartControlState extends State<ScatterChartControl> {
.toList(),
rotationQuarterTurns:
widget.control.getInt('rotation_quarter_turns', 0)!,
//errorIndicatorData: widget.control.get('error_indicator_data'),
errorIndicatorData:
const FlErrorIndicatorData(painter: getScatterSpotErrorPainter),
),
duration: animation.duration,
curve: animation.curve,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,14 @@ AxisTitles parseAxisTitles(Control? control) {
));
}

FlErrorRange? parseFlErrorRange(dynamic value, [FlErrorRange? defaultValue]) {
if (value == null) return defaultValue;
return FlErrorRange(
lowerBy: parseDouble(value["lower_by"], 0)!,
upperBy: parseDouble(value["upper_by"], 0)!,
);
Comment thread
ndonkoHenri marked this conversation as resolved.
}

FLHorizontalAlignment? parseFLHorizontalAlignment(String? value,
[FLHorizontalAlignment? defaultValue]) {
if (value == null) return defaultValue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ class ScatterChartEventData extends Equatable {
List<Object?> get props => [eventType, spotIndex];
}

/// Draws the error bars of a spot in the color of that spot.
///
/// The default painter of fl_chart draws them in white, which is invisible on
/// light backgrounds.
FlSpotErrorRangePainter getScatterSpotErrorPainter(
ScatterChartSpotErrorRangeCallbackInput input) =>
FlSimpleErrorPainter(lineColor: input.spot.dotPainter.mainColor);

ScatterTouchTooltipData parseScatterTouchTooltipData(
BuildContext context, Control control, List<ScatterSpot> spots) {
var tooltip = control.get("tooltip") ?? {};
Expand Down
2 changes: 2 additions & 0 deletions website/docs/controls/charts/scatterchart.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ import {ClassMembers, ClassSummary, CodeExample, Image} from '@site/src/componen

<Image src={frontMatter.example_images + '/example_1.png'} width="65%" />

<CodeExample path={frontMatter.examples + '/scatter_chart_with_error_bars/main.py'} language="python" />

<ClassMembers name={frontMatter.class_name} />
7 changes: 7 additions & 0 deletions website/docs/controls/charts/types/charterrorrange.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
title: "ChartErrorRange"
---

import {ClassAll} from '@site/src/components/crocodocs';

<ClassAll name="flet_charts.types.ChartErrorRange" />
1 change: 1 addition & 0 deletions website/sidebars.yml
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,7 @@ docs:
- controls/charts/types/chartcirclepoint.md
- controls/charts/types/chartcrosspoint.md
- controls/charts/types/chartdatapointtooltip.md
- controls/charts/types/charterrorrange.md
- controls/charts/types/charteventtype.md
- controls/charts/types/chartgridlines.md
- controls/charts/types/chartpointline.md
Expand Down
Loading