Skip to content
Open
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
6 changes: 3 additions & 3 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ EXTERNAL SOURCES:

SPEC CHECKSUMS:
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b

PODFILE CHECKSUM: 0dbd5a87e0ace00c9610d2037ac22083a01f861d

Expand Down
2 changes: 2 additions & 0 deletions example/lib/presentation/samples/chart_samples.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'bar/bar_chart_sample6.dart';
import 'bar/bar_chart_sample7.dart';
import 'bar/bar_chart_sample8.dart';
import 'chart_sample.dart';
import 'line/draggable_line_chart_sample.dart';
import 'line/line_chart_sample1.dart';
import 'line/line_chart_sample10.dart';
import 'line/line_chart_sample11.dart';
Expand Down Expand Up @@ -46,6 +47,7 @@ class ChartSamples {
LineChartSample(11, (context) => const LineChartSample11()),
LineChartSample(12, (context) => const LineChartSample12()),
LineChartSample(13, (context) => const LineChartSample13()),
LineChartSample(14, (context) => const DraggableLineChartSample()),
],
ChartType.bar: [
BarChartSample(1, (context) => BarChartSample1()),
Expand Down
241 changes: 241 additions & 0 deletions example/lib/presentation/samples/line/draggable_line_chart_sample.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';

/// Example demonstrating drag-to-edit functionality with DraggableLineChart
/// Launches a fullscreen demo to avoid scrollable context issues
class DraggableLineChartSample extends StatelessWidget {
const DraggableLineChartSample({super.key});

@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Padding(
padding: EdgeInsets.all(24.0),
child: Text(
'Interactive Draggable Chart',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 24.0),
child: Text(
'Point dragging unsupported in scrollable context - Tap the button to try the draggable chart in fullscreen mode.',
style: TextStyle(fontSize: 14),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => const _DraggableLineChartDemo(),
),
);
},
icon: const Icon(Icons.touch_app),
label: const Text('Launch Interactive Demo'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
),
),
],
),
);
}
}

/// Fullscreen demo of the draggable chart
class _DraggableLineChartDemo extends StatefulWidget {
const _DraggableLineChartDemo();

@override
State<_DraggableLineChartDemo> createState() =>
_DraggableLineChartDemoState();
}

class _DraggableLineChartDemoState extends State<_DraggableLineChartDemo> {
// Mutable data that can be edited by dragging
List<FlSpot> dataPoints = [
const FlSpot(0, 3),
const FlSpot(2, 5),
const FlSpot(4, 2),
const FlSpot(6, 7),
const FlSpot(8, 5),
const FlSpot(10, 8),
];

int? selectedPointIndex;

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Draggable Line Chart'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
setState(() {
// Reset to initial data
dataPoints = [
const FlSpot(0, 3),
const FlSpot(2, 5),
const FlSpot(4, 2),
const FlSpot(6, 7),
const FlSpot(8, 5),
const FlSpot(10, 8),
];
selectedPointIndex = null;
});
},
tooltip: 'Reset Data',
),
],
),
body: Column(
children: [
const Padding(
padding: EdgeInsets.all(16.0),
child: Card(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
Text(
'Try it out!',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Text(
'• Tap any point to select it (turns red)\n'
'• Drag points to move them around\n'
'• Use the refresh button to reset',
style: TextStyle(fontSize: 14),
),
],
),
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: DraggableLineChart(
dragTolerance: 10.0,
// Add constraints to keep points in order and within bounds
constrainDrag: (barIndex, spotIndex, oldSpot, proposedSpot) {
// Get the previous and next points to constrain X movement
// Add padding to prevent points from getting too close
const minSpacing = 0.8;
final prevX = spotIndex > 0
? dataPoints[spotIndex - 1].x + minSpacing
: 0.0;
final nextX = spotIndex < dataPoints.length - 1
? dataPoints[spotIndex + 1].x - minSpacing
: 10.0;

// Constrain X to stay between neighbors with spacing
final constrainedX = proposedSpot.x.clamp(prevX, nextX);
// Constrain Y within chart bounds
final constrainedY = proposedSpot.y.clamp(0.0, 10.0);

return FlSpot(constrainedX, constrainedY);
},
data: LineChartData(
minX: 0,
maxX: 10,
minY: 0,
maxY: 10,
gridData: FlGridData(
show: true,
drawVerticalLine: true,
drawHorizontalLine: true,
),
titlesData: FlTitlesData(
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 40,
getTitlesWidget: (value, meta) {
return Text(
value.toInt().toString(),
style: const TextStyle(fontSize: 12),
);
},
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
getTitlesWidget: (value, meta) {
return Text(
value.toInt().toString(),
style: const TextStyle(fontSize: 12),
);
},
),
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
),
borderData: FlBorderData(show: true),
lineBarsData: [
LineChartBarData(
spots: dataPoints,
isCurved: true,
color: Colors.blue,
barWidth: 3,
dotData: FlDotData(
show: true,
getDotPainter: (spot, percent, barData, index) {
final isSelected = index == selectedPointIndex;
return FlDotCirclePainter(
radius: isSelected ? 10 : 6,
color: isSelected ? Colors.red : Colors.blue,
strokeWidth: 2,
strokeColor: Colors.white,
);
},
),
),
],
),
onSpotTap: (barIndex, spotIndex, spot) {
setState(() {
selectedPointIndex = spotIndex;
});
},
onDragStart: (barIndex, spotIndex, spot) {
setState(() {
selectedPointIndex = spotIndex;
});
},
onDragUpdate: (barIndex, spotIndex, oldSpot, newSpot) {
setState(() {
dataPoints[spotIndex] = FlSpot(
newSpot.x.clamp(0.0, 10.0),
newSpot.y.clamp(0.0, 10.0),
);
});
},
onDragEnd: () {
// Drag complete
},
),
),
),
],
),
);
}
}
1 change: 1 addition & 0 deletions lib/fl_chart.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ 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/draggable_line_chart.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';
Expand Down
65 changes: 37 additions & 28 deletions lib/src/chart/base/axis_chart/side_titles/side_titles_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ class SideTitlesWidget extends StatefulWidget {
}

class _SideTitlesWidgetState extends State<SideTitlesWidget> {
bool get isHorizontal =>
widget.side == AxisSide.top || widget.side == AxisSide.bottom;
bool get isHorizontal => widget.side == AxisSide.top || widget.side == AxisSide.bottom;

bool get isVertical => !isHorizontal;

Expand All @@ -45,19 +44,42 @@ class _SideTitlesWidgetState extends State<SideTitlesWidget> {

double get baselineY => widget.axisChartData.baselineY;

double get axisMin => isHorizontal ? minX : minY;
// For LineChartData, use minRightY/maxRightY for the right axis
double get minRightY {
if (widget.axisChartData is LineChartData) {
return (widget.axisChartData as LineChartData).minRightY;
}
return minY;
}

double get maxRightY {
if (widget.axisChartData is LineChartData) {
return (widget.axisChartData as LineChartData).maxRightY;
}
return maxY;
}

double get axisMin {
if (isHorizontal) return minX;
// Use right Y-axis values for the right side
if (widget.side == AxisSide.right) return minRightY;
return minY;
}

double get axisMax => isHorizontal ? maxX : maxY;
double get axisMax {
if (isHorizontal) return maxX;
// Use right Y-axis values for the right side
if (widget.side == AxisSide.right) return maxRightY;
return 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 isLeftOrTop => widget.side == AxisSide.left || widget.side == AxisSide.top;

bool get isRightOrBottom =>
widget.side == AxisSide.right || widget.side == AxisSide.bottom;
bool get isRightOrBottom => widget.side == AxisSide.right || widget.side == AxisSide.bottom;

AxisTitles get axisTitles => switch (widget.side) {
AxisSide.left => titlesData.leftTitles,
Expand All @@ -83,25 +105,17 @@ class _SideTitlesWidgetState extends State<SideTitlesWidget> {
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,
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,
AxisSide.right || AxisSide.left => titlesPadding.vertical + borderPadding.vertical,
AxisSide.top || AxisSide.bottom => titlesPadding.horizontal + borderPadding.horizontal,
};
}

Expand All @@ -111,8 +125,7 @@ class _SideTitlesWidgetState extends State<SideTitlesWidget> {
if (chartVirtualRect == null) {
size = widget.parentSize;
} else {
size = chartVirtualRect.size +
Offset(thisSidePaddingTotal, thisSidePaddingTotal);
size = chartVirtualRect.size + Offset(thisSidePaddingTotal, thisSidePaddingTotal);
}

return size.rotateByQuarterTurns(
Expand Down Expand Up @@ -223,12 +236,8 @@ class _SideTitlesWidgetState extends State<SideTitlesWidget> {
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)),
AxisSide.left || AxisSide.right => chartRect.contains(Offset(0, location)),
AxisSide.top || AxisSide.bottom => chartRect.contains(Offset(location, 0)),
};
}).toList();
}
Expand Down
Loading