Skip to content

Commit b1d6a72

Browse files
committed
feat: Major refactor v2.0.0 - Implement formattedValue, showValue flag, and dynamic opacity
1 parent aece299 commit b1d6a72

13 files changed

Lines changed: 171 additions & 121 deletions

README.md

Lines changed: 15 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# FlutterCirclePackChart
22

3-
A powerful, interactive, and highly customizable Circle Pack Chart (Circular Treemap) library for Flutter. Built with performance and user experience in mind, it features immersive drill-down navigation and a responsive, structured label system.
3+
A powerful, interactive, and highly customizable Circle Pack Chart (Circular Treemap) library for Flutter. Built with performance and user experience in mind, it features immersive drill-down navigation and a responsive, data-driven label system.
44

55
**[Live Demo](https://fabiocarneiro.github.io/FlutterCirclePackChart/)**
66

@@ -10,12 +10,12 @@ A powerful, interactive, and highly customizable Circle Pack Chart (Circular Tre
1010
- **♾️ Infinite Zoom Context:** Sibling nodes remain visible and partially overflow the square viewport during transitions, maintaining clear hierarchical context.
1111
- **🔄 Bidirectional Navigation:** Seamlessly supports both drill-in (explosion) and drill-out (implosion) animations for a natural, physical feel.
1212
- **📏 Professional Label System:**
13-
- **Structured Data:** Use `label` for the name and `upperLabel` for monetary values or subtitles (rendered larger and bold).
13+
- **Structured Data:** Use `label` for the name and `formattedValue` for monetary values, percentages, or custom strings (rendered larger and bold on top).
1414
- **Guaranteed Visibility:** Enforces a minimum circle size to ensure every item has a legible label.
1515
- **Anti-Scaled Consistency:** Labels maintain a constant visual size on screen regardless of the zoom level.
16-
- **Clean Aesthetic:** Flat, normal-weight text with automatic single-line ellipsis for long names.
17-
- **📋 Dynamic Legend:** Includes a built-in vertical legend component that automatically updates to reflect the items, colors, and values of the currently focused level.
18-
- **🎨 Highly Customizable:** Adjust animation speed, minimum radius ratios, font size factors, and colors to match your app's brand.
16+
- **Visibility Toggle:** Control whether values are shown inside circles using the `showValue` flag.
17+
- ** Dynamic Opacity:** Automatically scales child circle opacity based on their relative values, visually highlighting more important data points.
18+
- **📋 Dynamic Legend:** Includes a built-in vertical legend component that automatically updates to reflect labels and formatted values of the currently focused level.
1919

2020
## 📦 Installation
2121

@@ -25,12 +25,10 @@ To add **flutter_circle_pack_chart** to your project, run:
2525
flutter pub add flutter_circle_pack_chart
2626
```
2727

28-
This will add the latest version of the package to your `pubspec.yaml`.
29-
3028
## 🚀 Getting Started
3129

3230
### 1. Define your data
33-
Create a hierarchy of `CircleNode` objects. You can use both `label` and `upperLabel` to display structured data on two lines:
31+
Create a hierarchy of `CircleNode` objects. Use `formattedValue` to display custom-formatted data like currency:
3432

3533
```dart
3634
final root = CircleNode(
@@ -39,54 +37,39 @@ final root = CircleNode(
3937
children: [
4038
CircleNode(
4139
label: 'Needs',
42-
upperLabel: '\$2500',
40+
formattedValue: '\$2500',
4341
color: Colors.orange,
4442
children: [
45-
CircleNode(label: 'Rent', upperLabel: '\$1500', value: 1500.0),
46-
CircleNode(label: 'Groceries', upperLabel: '\$400', value: 400.0),
47-
],
48-
),
49-
CircleNode(
50-
label: 'Wants',
51-
upperLabel: '\$1100',
52-
color: Colors.pink,
53-
children: [
54-
CircleNode(label: 'Dining', upperLabel: '\$300', value: 300.0),
43+
CircleNode(label: 'Rent', formattedValue: '\$1500', value: 1500.0),
44+
CircleNode(label: 'Groceries', formattedValue: '\$400', value: 400.0),
5545
],
5646
),
5747
],
5848
);
5949
```
6050

6151
### 2. Add the Widget
62-
Place the `FlutterCirclePackChart` in your widget tree. Optionally use a `FlutterCirclePackChartController` for advanced navigation and legend support.
52+
Place the `FlutterCirclePackChart` in your widget tree.
6353

6454
```dart
6555
// Initialize the controller
6656
final controller = FlutterCirclePackChartController(root: root);
6757
6858
// In your build method
69-
Column(
70-
children: [
71-
Expanded(
72-
child: FlutterCirclePackChart(
73-
root: root,
74-
controller: controller,
75-
),
76-
),
77-
// Add the dynamic legend (shows primary labels only)
78-
FlutterCirclePackChartLegend(controller: controller),
79-
],
59+
FlutterCirclePackChart(
60+
root: root,
61+
controller: controller,
62+
showValue: true, // Toggle value visibility in circles
8063
)
8164
```
8265

8366
## 🛠️ Customization
8467

8568
| Property | Description | Default |
8669
| :--- | :--- | :--- |
70+
| `showValue` | Whether to display values/formatted values inside the circles. | `true` |
8771
| `minRadiusRatio` | Minimum radius of a child circle as a fraction of its parent. | `0.20` |
8872
| `fontSizeFactor` | A multiplier to adjust the responsive font size globally. | `1.0` |
89-
| `controller` | Custom controller to manage focus and navigation state. | `Auto-generated` |
9073

9174
## 🤝 Contributing
9275

conductor/tracks.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ This file tracks all major tracks for the project. Each track has its own detail
44

55
---
66

7+
- [~] **Track: Major Refactor: Value Formatting & Dynamic Opacity (v2.0.0)**
8+
*Link: [./tracks/v2_refactor_20260423/](./tracks/v2_refactor_20260423/)*
9+
710
- [x] **Track: Rename project to FlutterCirclePackChart**
811
*Link: [./tracks/rename_to_flutter_20260423/](./tracks/rename_to_flutter_20260423/)*
912

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Track v2_refactor_20260423 Context
2+
3+
- [Specification](./spec.md)
4+
- [Implementation Plan](./plan.md)
5+
- [Metadata](./metadata.json)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"track_id": "v2_refactor_20260423",
3+
"type": "feature",
4+
"status": "new",
5+
"created_at": "2026-04-23T16:00:00Z",
6+
"updated_at": "2026-04-23T16:00:00Z",
7+
"description": "Major refactor: Implement value formatting, dynamic opacity, and refined data model (v2.0.0)"
8+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Implementation Plan - v2.0.0 Refactor
2+
3+
## Phase 1: Breaking Data Model Changes
4+
- [x] Task: Refactor `CircleNode`
5+
- [x] Rename `upperLabel` to `formattedValue`.
6+
- [x] Update `fromMap` factory.
7+
- [x] Update all library files and tests to use new field names.
8+
- [ ] Task: Conductor - User Manual Verification 'Phase 1: Breaking Data Model Changes' (Protocol in workflow.md)
9+
10+
## Phase 2: Painter & Rendering Logic
11+
- [x] Task: Implement Dynamic Opacity [df36ffe]
12+
- [x] Calculate relative opacity in `_drawNode` based on child values.
13+
- [x] Blend with animation opacity.
14+
- [x] Task: Implement Value Visibility Flag [df36ffe]
15+
- [x] Add `showValue` to `FlutterCirclePackChart` and `FlutterCirclePackChartPainter`.
16+
- [x] Update `_drawLabel` to respect the flag and prefer `formattedValue`.
17+
- [ ] Task: Conductor - User Manual Verification 'Phase 2: Painter & Rendering Logic' (Protocol in workflow.md)
18+
19+
## Phase 3: Sync & Examples
20+
- [~] Task: Update Legend and Examples
21+
- [ ] Update `FlutterCirclePackChartLegend` to use `formattedValue`.
22+
- [ ] Refactor all 3 examples to use the new API.
23+
- [ ] Task: Update README and Documentation
24+
- [ ] Document breaking changes and new features.
25+
- [ ] Task: Conductor - User Manual Verification 'Phase 3: Sync & Examples' (Protocol in workflow.md)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Specification: Major Refactor (v2.0.0)
2+
3+
## Overview
4+
This major release refactors the data model and rendering logic to support structured value display and dynamic visual effects based on data importance. It introduces breaking changes to `CircleNode` for a cleaner API.
5+
6+
## Functional Requirements
7+
- **Data Model Refactor (`CircleNode`):**
8+
- Remove `upperLabel`.
9+
- Add `formattedValue` (String) for displaying custom-formatted data (currency, percentages, etc.).
10+
- Primary `label` remains for the descriptive name.
11+
- `value` (double) continues to drive sizing and legend totals.
12+
- **Rendering Refinements:**
13+
- **Value Visibility Flag:** Add `showValue` flag to the widget to toggle displaying the value (preferring `formattedValue`) inside the circles.
14+
- **Dynamic Opacity:** Implement opacity scaling for children based on their relative values (larger items are more opaque, smaller items are more subtle).
15+
- **Independent Styling:** Maintain larger/bold styling for the value line when displayed in the circle.
16+
- **Legend Update:** Sync with the new model, ensuring the value shown matches the numeric `value` or `formattedValue`.
17+
18+
## Breaking Changes
19+
- `CircleNode` constructor parameters have changed.
20+
- `secondaryLabel` / `upperLabel` are replaced by `formattedValue`.

example/lib/main.dart

Lines changed: 37 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -169,12 +169,14 @@ class ChartExampleScaffold extends StatelessWidget {
169169
final String title;
170170
final CircleNode root;
171171
final String subtitle;
172+
final bool showValue;
172173

173174
const ChartExampleScaffold({
174175
super.key,
175176
required this.title,
176177
required this.root,
177178
required this.subtitle,
179+
this.showValue = true,
178180
});
179181

180182
@override
@@ -216,6 +218,7 @@ class ChartExampleScaffold extends StatelessWidget {
216218
child: FlutterCirclePackChart(
217219
root: root,
218220
controller: controller,
221+
showValue: showValue,
219222
),
220223
),
221224
const SizedBox(height: 24),
@@ -256,52 +259,30 @@ class WorldPopulationExample extends StatelessWidget {
256259
label: 'Asia',
257260
color: Colors.red,
258261
children: [
259-
CircleNode(label: 'China', value: 1400.0),
260-
CircleNode(label: 'India', value: 1300.0),
261-
CircleNode(label: 'Japan', value: 125.0),
262-
CircleNode(label: 'Indonesia', value: 270.0),
263-
CircleNode(label: 'Pakistan', value: 220.0),
264-
CircleNode(label: 'Bangladesh', value: 170.0),
265-
CircleNode(label: 'Vietnam', value: 98.0),
262+
CircleNode(label: 'China', formattedValue: '1.4B', value: 1400.0),
263+
CircleNode(label: 'India', formattedValue: '1.3B', value: 1300.0),
264+
CircleNode(label: 'Japan', formattedValue: '125M', value: 125.0),
265+
CircleNode(label: 'Indonesia', formattedValue: '270M', value: 270.0),
266+
CircleNode(label: 'Pakistan', formattedValue: '220M', value: 220.0),
266267
],
267268
),
268269
CircleNode(
269270
label: 'Europe',
270271
color: Colors.blue,
271272
children: [
272-
CircleNode(label: 'Germany', value: 83.0),
273-
CircleNode(label: 'France', value: 67.0),
274-
CircleNode(label: 'UK', value: 66.0),
275-
CircleNode(label: 'Italy', value: 60.0),
276-
CircleNode(label: 'Spain', value: 47.0),
277-
CircleNode(label: 'Ukraine', value: 44.0),
278-
CircleNode(label: 'Poland', value: 38.0),
273+
CircleNode(label: 'Germany', formattedValue: '83M', value: 83.0),
274+
CircleNode(label: 'France', formattedValue: '67M', value: 67.0),
275+
CircleNode(label: 'UK', formattedValue: '66M', value: 66.0),
276+
CircleNode(label: 'Italy', formattedValue: '60M', value: 60.0),
279277
],
280278
),
281279
CircleNode(
282280
label: 'Americas',
283281
color: Colors.green,
284282
children: [
285-
CircleNode(label: 'USA', value: 330.0),
286-
CircleNode(label: 'Brazil', value: 210.0),
287-
CircleNode(label: 'Mexico', value: 128.0),
288-
CircleNode(label: 'Colombia', value: 50.0),
289-
CircleNode(label: 'Argentina', value: 45.0),
290-
CircleNode(label: 'Canada', value: 38.0),
291-
CircleNode(label: 'Peru', value: 33.0),
292-
],
293-
),
294-
CircleNode(
295-
label: 'Africa',
296-
color: Colors.orange,
297-
children: [
298-
CircleNode(label: 'Nigeria', value: 200.0),
299-
CircleNode(label: 'Ethiopia', value: 110.0),
300-
CircleNode(label: 'Egypt', value: 100.0),
301-
CircleNode(label: 'DRC', value: 90.0),
302-
CircleNode(label: 'Tanzania', value: 60.0),
303-
CircleNode(label: 'South Africa', value: 59.0),
304-
CircleNode(label: 'Kenya', value: 53.0),
283+
CircleNode(label: 'USA', formattedValue: '330M', value: 330.0),
284+
CircleNode(label: 'Brazil', formattedValue: '210M', value: 210.0),
285+
CircleNode(label: 'Canada', formattedValue: '38M', value: 38.0),
305286
],
306287
),
307288
],
@@ -310,7 +291,7 @@ class WorldPopulationExample extends StatelessWidget {
310291
return ChartExampleScaffold(
311292
title: 'POPULATION STATISTICS',
312293
root: root,
313-
subtitle: 'Drill down into continents to see population by country.',
294+
subtitle: 'Drill down into continents to see population. Dynamic opacity highlights larger nations.',
314295
);
315296
}
316297
}
@@ -326,37 +307,37 @@ class BudgetTrackerExample extends StatelessWidget {
326307
children: [
327308
CircleNode(
328309
label: 'Needs',
329-
upperLabel: '\$2500',
310+
formattedValue: '\$2500',
330311
color: Colors.orange,
331312
children: [
332-
CircleNode(label: 'Rent', upperLabel: '\$1500', value: 1500.0),
333-
CircleNode(label: 'Groceries', upperLabel: '\$400', value: 400.0),
334-
CircleNode(label: 'Utilities', upperLabel: '\$250', value: 250.0),
335-
CircleNode(label: 'Insurance', upperLabel: '\$200', value: 200.0),
336-
CircleNode(label: 'Transport', upperLabel: '\$150', value: 150.0),
313+
CircleNode(label: 'Rent', formattedValue: '\$1500', value: 1500.0),
314+
CircleNode(label: 'Groceries', formattedValue: '\$400', value: 400.0),
315+
CircleNode(label: 'Utilities', formattedValue: '\$250', value: 250.0),
316+
CircleNode(label: 'Insurance', formattedValue: '\$200', value: 200.0),
317+
CircleNode(label: 'Transport', formattedValue: '\$150', value: 150.0),
337318
],
338319
),
339320
CircleNode(
340321
label: 'Wants',
341-
upperLabel: '\$1100',
322+
formattedValue: '\$1100',
342323
color: Colors.pink,
343324
children: [
344-
CircleNode(label: 'Dining', upperLabel: '\$300', value: 300.0),
345-
CircleNode(label: 'Subs', upperLabel: '\$50', value: 50.0),
346-
CircleNode(label: 'Shopping', upperLabel: '\$200', value: 200.0),
347-
CircleNode(label: 'Hobbies', upperLabel: '\$150', value: 150.0),
348-
CircleNode(label: 'Travel', upperLabel: '\$400', value: 400.0),
325+
CircleNode(label: 'Dining', formattedValue: '\$300', value: 300.0),
326+
CircleNode(label: 'Subs', formattedValue: '\$50', value: 50.0),
327+
CircleNode(label: 'Shopping', formattedValue: '\$200', value: 200.0),
328+
CircleNode(label: 'Hobbies', formattedValue: '\$150', value: 150.0),
329+
CircleNode(label: 'Travel', formattedValue: '\$400', value: 400.0),
349330
],
350331
),
351332
CircleNode(
352333
label: 'Savings',
353-
upperLabel: '\$1400',
334+
formattedValue: '\$1400',
354335
color: Colors.teal,
355336
children: [
356-
CircleNode(label: 'Emergency', upperLabel: '\$500', value: 500.0),
357-
CircleNode(label: 'Retire', upperLabel: '\$400', value: 400.0),
358-
CircleNode(label: 'Invest', upperLabel: '\$300', value: 300.0),
359-
CircleNode(label: 'Debt', upperLabel: '\$200', value: 200.0),
337+
CircleNode(label: 'Emergency', formattedValue: '\$500', value: 500.0),
338+
CircleNode(label: 'Retire', formattedValue: '\$400', value: 400.0),
339+
CircleNode(label: 'Invest', formattedValue: '\$300', value: 300.0),
340+
CircleNode(label: 'Debt', formattedValue: '\$200', value: 200.0),
360341
],
361342
),
362343
],
@@ -365,7 +346,7 @@ class BudgetTrackerExample extends StatelessWidget {
365346
return ChartExampleScaffold(
366347
title: 'HOUSEHOLD BUDGET',
367348
root: root,
368-
subtitle: 'Manage your monthly spending using the 50/30/20 rule.',
349+
subtitle: 'Manage your monthly spending. Higher expenses are more opaque.',
369350
);
370351
}
371352
}
@@ -395,21 +376,14 @@ class StressTestExample extends StatelessWidget {
395376
CircleNode(label: 'The United Kingdom of Great Britain and Northern Ireland', value: 20.0),
396377
],
397378
),
398-
CircleNode(
399-
label: 'High Density',
400-
color: Colors.deepOrange,
401-
children: List.generate(10, (i) => CircleNode(
402-
label: 'Group $i',
403-
children: List.generate(5, (j) => CircleNode(label: 'Item $i-$j', value: 5.0)),
404-
)),
405-
),
406379
],
407380
);
408381

409-
return ChartExampleScaffold(
382+
return const ChartExampleScaffold(
410383
title: 'LIBRARY LIMITS',
411384
root: root,
412-
subtitle: 'Testing minimum radii, anti-scaling, and label overflow.',
385+
showValue: false, // Stress test hidden values
386+
subtitle: 'Testing minimum radii and label overflow with hidden values.',
413387
);
414388
}
415389
}

example/pubspec.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ packages:
108108
path: ".."
109109
relative: true
110110
source: path
111-
version: "0.0.1"
111+
version: "1.0.4"
112112
flutter_lints:
113113
dependency: "direct dev"
114114
description:

0 commit comments

Comments
 (0)