Skip to content

Commit 5d51a28

Browse files
jbbjarnasonrudi-q
authored andcommitted
release 1.12.0 feat: boundary clamping, option to disable infinite panning (#62)
1 parent 8a3dac1 commit 5d51a28

File tree

9 files changed

+152
-18
lines changed

9 files changed

+152
-18
lines changed

CHANGELOG.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,47 @@
1+
## 1.12.0 - 2025-11-01
2+
3+
#### 🏔️ Boundary Clamping for Pan Operations
4+
5+
**Authored by [@jbbjarnason](https://github.com/jbbjarnason)** - Thank you for this contribution!
6+
7+
**New Features:**
8+
- **Boundary Clamping**: Prevent infinite panning beyond data boundaries
9+
- New `boundaryClampingX` and `boundaryClampingY` options in `PanConfig`
10+
- Clamps pan domain within calculated scale boundaries
11+
- Perfect for constrained data exploration and guided navigation
12+
13+
**Technical Implementation:**
14+
- Scale boundaries tracked via `valuesBoundaries` in `LinearScale`
15+
- Computed in `LinearScale.computeDomain()` from data values
16+
- Pan domain clamping applied in `_updatePanDomain()` during interaction
17+
- Seamless integration with existing pan callbacks and pan controller
18+
- No changes to default behavior - opt-in feature
19+
20+
**API Example:**
21+
```dart
22+
CristalyseChart()
23+
.data(timeSeriesData)
24+
.mapping(x: 'time', y: 'value')
25+
.geomLine()
26+
.interaction(
27+
pan: PanConfig(
28+
enabled: true,
29+
boundaryClampingX: true, // Clamp X-axis panning
30+
boundaryClampingY: true, // Clamp Y-axis panning
31+
),
32+
)
33+
.build()
34+
```
35+
36+
#### 🧪 Quality Assurance
37+
38+
- Zero breaking changes - fully backward compatible
39+
- Default clamping disabled (infinite panning by default)
40+
- Tested with pan controller and manual pan interactions
41+
- Production ready
42+
43+
---
44+
145
## 1.11.1 - 2025-10-24
246

347
#### 🐛 Bug Fixes

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1101,7 +1101,7 @@ chart
11011101

11021102
## 🧪 Development Status
11031103

1104-
**Current Version: 1.11.1** - Production ready with programmatic pan controller, interactive floating legends, and intelligent axis bounds
1104+
**Current Version: 1.12.0** - Production ready with boundary clamping for pan operations, programmatic pan controller, interactive floating legends, and intelligent axis bounds
11051105

11061106
We're shipping progressively! Each release adds new visualization types while maintaining backward compatibility.
11071107

@@ -1126,6 +1126,7 @@ We're shipping progressively! Each release adds new visualization types while ma
11261126
-**v1.9.0** - **Interactive & floating legends** with click-to-toggle visibility, custom positioning with offsets, color consistency preservation, and overflow rendering support
11271127
-**v1.10.0** - **Axis titles & bubble size guide** with optional titles for all axes, visual bubble size legends, smart spacing calculations, and bubble legend validation fixes (by [@davidlrichmond](https://github.com/davidlrichmond))
11281128
-**v1.11.0** - **Programmatic pan controller** for external chart panning control via PanController with panTo() and panReset() methods (by [@jbbjarnason](https://github.com/jbbjarnason))
1129+
-**v1.12.0** - **Boundary clamping for pan operations** with optional boundaryClampingX and boundaryClampingY to prevent infinite panning beyond data boundaries (by [@jbbjarnason](https://github.com/jbbjarnason))
11291130

11301131
## Support This Project
11311132

doc/installation.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ If you prefer to add the dependency manually, update your `pubspec.yaml`:
2626
dependencies:
2727
flutter:
2828
sdk: flutter
29-
cristalyse: ^1.11.1 # Latest version
29+
cristalyse: ^1.12.0 # Latest version
3030
```
3131
3232
Then run:
@@ -105,6 +105,7 @@ Cristalyse has minimal dependencies to keep your app lightweight:
105105

106106
| Version | Release Date | Key Features |
107107
|---------|-------------|--------------|
108+
| 1.12.0 | November 2025 | Boundary clamping for pan operations |
108109
| 1.11.1 | October 2025 | Fixed Y-axis bounds during X-axis panning |
109110
| 1.11.0 | October 2025 | Programmatic pan controller with panTo() and panReset() methods |
110111
| 1.10.3 | October 2025 | Scale padding fix for consistent chart sizing during pan operations |

doc/updates.mdx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,53 @@ seo:
1212

1313
Stay informed about the latest features, improvements, and fixes in Cristalyse.
1414

15+
<Update label="November 1, 2025" description="v1.12.0">
16+
### 🏔️ Boundary Clamping for Pan Operations
17+
18+
**Authored by [@jbbjarnason](https://github.com/jbbjarnason)** - Thank you for this contribution!
19+
20+
**Control infinite panning with boundary clamping!**
21+
22+
**Boundary Clamping:**
23+
- New `boundaryClampingX` and `boundaryClampingY` options in `PanConfig`
24+
- Prevents panning beyond data boundaries when enabled
25+
- Maintains intuitive pan behavior within configured domain limits
26+
- Perfect for constrained data exploration and guided navigation
27+
28+
**Use Cases:**
29+
- Prevent users from panning too far from relevant data
30+
- Create bounded exploration areas for large datasets
31+
- Maintain data context during navigation
32+
- Improve UX for time-series and scientific visualizations
33+
34+
```dart
35+
CristalyseChart()
36+
.data(timeSeriesData)
37+
.mapping(x: 'time', y: 'value')
38+
.geomLine()
39+
.interaction(
40+
pan: PanConfig(
41+
enabled: true,
42+
boundaryClampingX: true, // Clamp X-axis panning
43+
boundaryClampingY: true, // Clamp Y-axis panning
44+
),
45+
)
46+
.build()
47+
```
48+
49+
**Technical Implementation:**
50+
- Scale boundaries tracked via `valuesBoundaries` in `LinearScale`
51+
- Pan domain clamping applied during interaction handling
52+
- Seamless integration with existing pan callbacks and pan controller
53+
- No changes to default behavior - opt-in feature
54+
55+
**Quality Assurance:**
56+
- Zero breaking changes - fully backward compatible
57+
- Default clamping disabled (infinite panning by default)
58+
- Tested with pan controller and manual pan interactions
59+
- Production ready
60+
</Update>
61+
1562
<Update label="October 24, 2025" description="v1.11.1">
1663
### 🐛 Y-axis Bounds Fix During Panning
1764

example/lib/graphs/pan_example.dart

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class _PanExampleWidgetState extends State<_PanExampleWidget> {
3030
int totalPanEvents = 0;
3131
int activeDataPoints = 0;
3232
PanController panController = PanController();
33+
bool boundaryClamping = false;
3334

3435
@override
3536
void initState() {
@@ -207,16 +208,32 @@ class _PanExampleWidgetState extends State<_PanExampleWidget> {
207208
));
208209
},
209210
icon: Icon(Icons.refresh),
210-
color: Theme.of(context).primaryColor,
211+
color: Theme.of(context).colorScheme.onPrimary,
211212
),
212213
const SizedBox(width: 16),
213214
IconButton(
214215
onPressed: () {
215216
panController.panReset();
216217
},
217218
icon: Icon(Icons.undo),
218-
color: Theme.of(context).primaryColor,
219+
color: Theme.of(context).colorScheme.onPrimary,
219220
),
221+
Expanded(
222+
child: SwitchListTile(
223+
title: Text(
224+
'Boundary Clamping',
225+
style: TextStyle(
226+
fontWeight: FontWeight.w500,
227+
color: Theme.of(context).colorScheme.onPrimary),
228+
textAlign: TextAlign.right,
229+
),
230+
value: boundaryClamping,
231+
onChanged: (value) {
232+
setState(() {
233+
boundaryClamping = value;
234+
});
235+
}),
236+
)
220237
],
221238
),
222239
),
@@ -262,6 +279,8 @@ class _PanExampleWidgetState extends State<_PanExampleWidget> {
262279
onPanEnd: _handlePanEnd,
263280
throttle: const Duration(milliseconds: 50),
264281
controller: panController,
282+
boundaryClampingX: boundaryClamping,
283+
boundaryClampingY: boundaryClamping,
265284
),
266285
)
267286
.legend(

lib/src/core/scale.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'dart:math' as math;
12
import 'package:flutter/material.dart';
23

34
import 'geometry.dart';
@@ -87,13 +88,16 @@ abstract class Scale {
8788
class LinearScale extends Scale {
8889
List<double> _domain = [0, 1];
8990
List<double>? _ticks; // Cached ticks from Wilkinson algorithm
91+
List<double> _valuesBoundaries = [0, 1];
9092

9193
LinearScale({super.limits, super.labelFormatter, super.title});
9294

9395
@override
9496
List<double> get domain => _domain;
9597
set domain(List<double> value) => _domain = List.from(value);
9698

99+
List<double> get valuesBoundaries => _valuesBoundaries;
100+
97101
@override
98102
double scale(dynamic value) {
99103
// normalize, but do not clamp any values out of bounds
@@ -114,6 +118,10 @@ class LinearScale extends Scale {
114118
values, effectiveLimits, geometries,
115119
applyPadding: true);
116120

121+
if (values.isNotEmpty) {
122+
_valuesBoundaries = [values.reduce(math.min), values.reduce(math.max)];
123+
}
124+
117125
if (bounds != const Bounds.ignored()) {
118126
// Use Wilkinson algorithm to extend bounds to nice round numbers
119127
final screenLength = (range[1] - range[0]).abs();

lib/src/interaction/chart_interactions.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ class PanConfig {
136136
/// The widget will listen/unlisten to this in its lifecycle.
137137
final PanController? controller;
138138

139+
/// Whether to clamp the pan to the boundaries of the chart.
140+
final bool boundaryClampingX;
141+
final bool boundaryClampingY;
142+
139143
const PanConfig({
140144
this.enabled = true,
141145
this.onPanUpdate,
@@ -145,6 +149,8 @@ class PanConfig {
145149
this.updateXDomain = false,
146150
this.updateYDomain = false,
147151
this.controller,
152+
this.boundaryClampingX = false,
153+
this.boundaryClampingY = false,
148154
});
149155
}
150156

lib/src/widgets/animated_chart_widget.dart

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1252,6 +1252,23 @@ class _AnimatedCristalyseChartWidgetState
12521252
return sizeScale;
12531253
}
12541254

1255+
void _updatePanDomain(
1256+
List<double> domain, double delta, bool clamp, Scale? scale) {
1257+
final newMin = domain[0] + delta;
1258+
final newMax = domain[1] + delta;
1259+
if (clamp && scale != null && scale is LinearScale) {
1260+
final clampMin = math.min(scale.valuesBoundaries[0], scale.domain[0]);
1261+
final clampMax = math.max(scale.valuesBoundaries[1], scale.domain[1]);
1262+
final shouldClamp = newMax > clampMax || newMin < clampMin;
1263+
if (shouldClamp) {
1264+
return;
1265+
}
1266+
}
1267+
1268+
domain[0] = newMin;
1269+
domain[1] = newMax;
1270+
}
1271+
12551272
/// Update pan domains based on delta movement
12561273
void _updatePanDomains(Rect plotArea, Offset delta) {
12571274
if (_panXDomain == null) return;
@@ -1266,13 +1283,8 @@ class _AnimatedCristalyseChartWidgetState
12661283

12671284
// Update the pan domain progressively - allow infinite panning
12681285
if (widget.interaction.pan?.updateXDomain != false) {
1269-
// Default to true if not specified
1270-
final newXMin = _panXDomain![0] + xDataDelta;
1271-
final newXMax = _panXDomain![1] + xDataDelta;
1272-
1273-
// Always allow panning - no blocking, visual clipping will handle boundaries
1274-
_panXDomain![0] = newXMin;
1275-
_panXDomain![1] = newXMax;
1286+
_updatePanDomain(_panXDomain!, xDataDelta,
1287+
widget.interaction.pan?.boundaryClampingX == true, widget.xScale);
12761288
}
12771289

12781290
// Optionally handle Y panning too - allow infinite panning
@@ -1282,12 +1294,8 @@ class _AnimatedCristalyseChartWidgetState
12821294
final yDataDelta =
12831295
delta.dy / pixelsPerYUnit; // Positive for natural pan direction
12841296

1285-
final newYMin = _panYDomain![0] + yDataDelta;
1286-
final newYMax = _panYDomain![1] + yDataDelta;
1287-
1288-
// Always allow panning - visual clipping will handle boundaries
1289-
_panYDomain![0] = newYMin;
1290-
_panYDomain![1] = newYMax;
1297+
_updatePanDomain(_panYDomain!, yDataDelta,
1298+
widget.interaction.pan?.boundaryClampingY == true, widget.yScale);
12911299
}
12921300
}
12931301

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: cristalyse
22
description: "Cristalyse is a high-performance data visualization library for Dart/Flutter that implements grammar of graphics principles with native rendering capabilities."
3-
version: 1.11.1
3+
version: 1.12.0
44
homepage: https://cristalyse.com
55
documentation: https://docs.cristalyse.com
66
repository: https://github.com/rudi-q/cristalyse

0 commit comments

Comments
 (0)