Skip to content

Commit

Permalink
Improve and fix out-of-domain point drawing/clipping
Browse files Browse the repository at this point in the history
  • Loading branch information
CharlVS committed Aug 6, 2024
1 parent d3e3518 commit f0b7560
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 50 deletions.
11 changes: 7 additions & 4 deletions example/lib/ui/chart_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,18 @@ class ChartScreen extends StatelessWidget {
child: BlocBuilder<ChartBloc, ChartState>(
builder: (context, state) {
return LineChart(
domainExtent: const ChartExtent.tight(),
domainExtent:
const ChartExtent.withBounds(min: 4.1, max: 8.2),
// domainExtent: ChartExtent.tight(),
backgroundColor: Theme.of(context).cardColor,
elements: [
ChartGridLines(isVertical: false, count: 5),
ChartDataSeries(data: state.data1, color: Colors.blue),
ChartDataSeries(
data: state.data2,
color: Colors.red,
lineType: LineType.bezier),
data: state.data2,
color: Colors.red,
lineType: LineType.bezier,
),
ChartAxisLabels(
isVertical: false,
count: 5,
Expand Down
2 changes: 1 addition & 1 deletion example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ packages:
path: ".."
relative: true
source: path
version: "0.0.2"
version: "0.1.0"
equatable:
dependency: "direct main"
description:
Expand Down
16 changes: 16 additions & 0 deletions lib/src/chart_data_series.dart
Original file line number Diff line number Diff line change
Expand Up @@ -164,4 +164,20 @@ class ChartDataSeries extends ChartElement {

canvas.drawCircle(offset, nodeRadius!, paint);
}

ChartDataSeries copyWith({
List<ChartData>? data,
Color? color,
double? strokeWidth,
LineType? lineType,
double? nodeRadius,
}) {
return ChartDataSeries(
data: data ?? this.data,
color: color ?? this.color,
strokeWidth: strokeWidth ?? this.strokeWidth,
lineType: lineType ?? this.lineType,
nodeRadius: nodeRadius ?? this.nodeRadius,
);
}
}
2 changes: 2 additions & 0 deletions lib/src/chart_data_transform.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ class ChartDataTransform {

double transformX(double x) => (x - minX) / (maxX - minX) * width;

double reverseTransformX(double x) => minX + (x / width) * (maxX - minX);

double transformY(double y) => height - (y - minY) / (maxY - minY) * height;

double invertX(double dx) => minX + (dx / width) * (maxX - minX);
Expand Down
156 changes: 112 additions & 44 deletions lib/src/line_chart.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,30 @@ import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';

class ChartExtent {
const ChartExtent({
@Deprecated(
'Use the named constructors instead. '
'This constructor will be removed in the next release.',
)
ChartExtent({
this.auto = true,
this.padding = 0.1,
double padding = 0.1,
this.min,
this.max,
});
}) : paddingPortion = padding;

const ChartExtent.withBounds({
required this.min,
required this.max,
}) : auto = false,
paddingPortion = 0;

const ChartExtent.tight({this.paddingPortion = 0})
: auto = true,
min = null,
max = null;

const ChartExtent.tight() : this(auto: true, padding: 0);
final bool auto;
final double padding;
final double paddingPortion;
final double? min;
final double? max;
}
Expand Down Expand Up @@ -67,7 +81,7 @@ class LineChart extends StatefulWidget {
this.tooltipBuilder,
this.animationDuration = const Duration(milliseconds: 500),
this.domainExtent = const ChartExtent.tight(),
this.rangeExtent = const ChartExtent(),
this.rangeExtent = const ChartExtent.tight(paddingPortion: 0.1),
this.backgroundColor = Colors.black,
this.padding = const EdgeInsets.all(32),
this.markerSelectionStrategy,
Expand All @@ -88,7 +102,6 @@ class LineChart extends StatefulWidget {
/// A builder function to create custom tooltips for data points.
///
/// If not provided, a default tooltip will be used.
//TODO! Add an index parameter?
final Widget Function(BuildContext, List<ChartData>, List<Color>)?
tooltipBuilder;

Expand Down Expand Up @@ -157,27 +170,21 @@ class _LineChartState extends State<LineChart>

@override
void initState() {
// assert(
// (widget.tooltipBuilder == null) ==
// (widget.markerSelectionStrategy == null),
// 'If either tooltipBuilder or markerSelectionStrategy is provided, '
// 'both must be provided.',
// );

super.initState();
_controller =
AnimationController(vsync: this, duration: widget.animationDuration);
_animation = CurvedAnimation(parent: _controller, curve: Curves.easeInOut);
_controller.addListener(() {
setState(() {});
});
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
setState(() {
oldElements = List.from(widget.elements);
});
}
});
_controller
..addListener(() {
setState(() {});
})
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
setState(() {
oldElements = List.from(widget.elements);
});
}
});
oldElements = List.from(widget.elements);
currentElements = List.from(widget.elements);
_updateDomainRange();
Expand Down Expand Up @@ -245,23 +252,21 @@ class _LineChartState extends State<LineChart>

if (widget.domainExtent.auto) {
final domainPaddingValue =
(newMaxX - newMinX) * widget.domainExtent.padding;
(newMaxX - newMinX) * widget.domainExtent.paddingPortion;
newMinX -= domainPaddingValue;
newMaxX += domainPaddingValue;
} else {
newMinX = widget.domainExtent.min ?? newMinX;
newMaxX = widget.domainExtent.max ?? newMaxX;
}
newMinX = widget.domainExtent.min ?? newMinX;
newMaxX = widget.domainExtent.max ?? newMaxX;

if (widget.rangeExtent.auto) {
final rangePaddingValue =
(newMaxY - newMinY) * widget.rangeExtent.padding;
(newMaxY - newMinY) * widget.rangeExtent.paddingPortion;
newMinY -= rangePaddingValue;
newMaxY += rangePaddingValue;
} else {
newMinY = widget.rangeExtent.min ?? newMinY;
newMaxY = widget.rangeExtent.max ?? newMaxY;
}
newMinY = widget.rangeExtent.min ?? newMinY;
newMaxY = widget.rangeExtent.max ?? newMaxY;

minXAnimation =
Tween<double>(begin: minX, end: newMinX).animate(_controller);
Expand Down Expand Up @@ -581,35 +586,98 @@ class _LineChartPainter extends CustomPainter {

@override
void paint(Canvas canvas, Size size) {
for (final element in elements) {
final dataElements = elements.whereType<ChartDataSeries>();
final nonDataElements =
elements.where((element) => element is! ChartDataSeries);

// Paint non-data elements (e.g., grid lines, axis labels)
for (final element in nonDataElements) {
element.paint(canvas, size, transform, animation);
}

// Save the canvas state before applying the clip
canvas.save();
canvas.clipRect(
Rect.fromLTWH(
0,
0,
size.width,
size.height,
),
);

// Paint data elements (e.g., data series) within the clipped area
for (final element in dataElements) {
final visibleData = _getVisibleData(element.data);
element
.copyWith(data: visibleData)
.paint(canvas, size, transform, animation);
}

// Restore the canvas state to remove the clip
canvas.restore();

// Filter highlighted points to only include those within the visible domain
final filteredHighlightedPoints = _getFilteredHighlightedPoints();

// Paint markers outside the clipped area
if (markerSelectionStrategy != null) {
markerSelectionStrategy!.paint(
canvas,
size,
transform,
highlightedPoints,
filteredHighlightedPoints,
highlightedColors,
hoverPosition,
);
}
}

// TODO: Make clip mode configurable
canvas.clipRect(
Rect.fromLTWH(
0,
0,
size.width,
size.height,
),
);
List<Offset> _getFilteredHighlightedPoints() {
if (highlightedPoints == null) return [];

final minX = transform.minX;
final maxX = transform.maxX;

return highlightedPoints!.where((point) {
final xValue = transform.reverseTransformX(point.dx);
return xValue >= minX && xValue <= maxX;
}).toList();
}

List<ChartData> _getVisibleData(List<ChartData> data) {
final visibleData = <ChartData>[];
ChartData? firstOutOfDomain;
ChartData? lastOutOfDomain;
final minX = transform.minX;
final maxX = transform.maxX;

for (final dataPoint in data) {
final xValue = dataPoint.x;
if (xValue >= minX && xValue <= maxX) {
visibleData.add(dataPoint);
} else if (xValue < minX &&
(firstOutOfDomain == null || xValue > firstOutOfDomain.x)) {
firstOutOfDomain = dataPoint;
} else if (xValue > maxX &&
(lastOutOfDomain == null || xValue < lastOutOfDomain.x)) {
lastOutOfDomain = dataPoint;
}
}

if (firstOutOfDomain != null) {
visibleData.insert(0, firstOutOfDomain);
}

if (lastOutOfDomain != null) {
visibleData.add(lastOutOfDomain);
}

return visibleData;
}

@override
bool shouldRepaint(covariant _LineChartPainter oldDelegate) {
// return true; //!
return oldDelegate.animation != animation ||
oldDelegate.hoverPosition != hoverPosition ||
!listEquals(oldDelegate.elements, elements) ||
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: dragon_charts_flutter
description: A lightweight and highly customizable charting library for Flutter.
version: 0.1.0
version: 0.1.1-dev.1
homepage: https://komodoplatform.com
repository: https://github.com/KomodoPlatform/dragon_charts_flutter

Expand Down

0 comments on commit f0b7560

Please sign in to comment.