I am looking at charts_flutter package. I need to implement a gauge chart, with a single segment and its label value at the gauge's center. See the mockup file below, where three charts from the required type are placed in a row:
Using Google chart gauge sample, I was able to implement the required gauge, however, I am struggling to set the label as per the requirement.
Below is the class, I use for the gauge, any help / hints how to add the label would be greatly appreciated:
/// Gauge chart example, where the data does not cover a full revolution in the
/// chart.
import 'package:charts_flutter/flutter.dart' as charts;
import 'package:flutter/material.dart';
import 'dart:math';
class GaugeChart extends StatelessWidget {
final List<charts.Series> seriesList;
final bool animate;
GaugeChart(this.seriesList, {this.animate});
factory GaugeChart.fromValue(
{#required double value, #required Color color, bool animate}) {
return GaugeChart(
_createDataFromValue(value, color),
// Disable animations for image tests.
animate: animate,
);
}
#override
Widget build(BuildContext context) {
return charts.PieChart(
seriesList,
animate: animate,
// Configure the width of the pie slices to 30px. The remaining space in
// the chart will be left as a hole in the center. Adjust the start
// angle and the arc length of the pie so it resembles a gauge.
defaultRenderer: charts.ArcRendererConfig(
arcWidth: 20,
startAngle: 3 / 5 * pi,
arcLength: 9 / 5 * pi,
//arcRendererDecorators: [charts.ArcLabelDecorator(labelPosition: charts.ArcLabelPosition.outside)],
),
);
}
static List<charts.Series<GaugeSegment, String>> _createDataFromValue(
double value, Color color) {
double toShow = (1 + value) / 2;
final data = [
GaugeSegment('Main', toShow, color),
GaugeSegment('Rest', 1 - toShow, Colors.transparent),
];
return [
charts.Series<GaugeSegment, String>(
id: 'Segments',
domainFn: (GaugeSegment segment, _) => segment.segment,
measureFn: (GaugeSegment segment, _) => segment.value,
colorFn: (GaugeSegment segment, _) => segment.color,
// Set a label accessor to control the text of the arc label.
labelAccessorFn: (GaugeSegment segment, _) =>
segment.segment == 'Main' ? '${segment.value}' : null,
data: data,
)
];
}
}
/// data type.
class GaugeSegment {
final String segment;
final double value;
final charts.Color color;
GaugeSegment(this.segment, this.value, Color color)
: this.color = charts.Color(
r: color.red, g: color.green, b: color.blue, a: color.alpha);
}
This is how the class can be used:
// value can take values between -1 and 1
GaugeChart.fromValue(value: 0.34, color: Colors.red)
We achieved this using a Stack:
return Container(
width: 120,
height: 120,
child: Stack(children: [
GaugeChart(_getChartData()),
Center(
child: Text(
'$percent',
))
]));
Related
This is how a Slider's animation normally looks like:
This is how a Slider looks when I try to add the value label:
This is the sample code:
Slider(
value: sliderValue,
activeColor: color,
min: 0.0,
max: 100.0,
divisions: 2000, //TO COMMENT
label: sliderValue.toStringAsFixed(2), //TO COMMENT
onChanged: (value) {
setState(() {
sliderValue = value;
});
}),
In this code, if I comment out the marked //TO COMMENT lines which are the divisions and label properties, the `label goes away as expected, and the animation is smooth again.
I assume this is due to divisions, and any amount of it, even just 100 does not change the lag in any way.
Additionally, it seems that the label property does not work on its own.
It needs the divisions property to also be set so that the value
label can be displayed.
What is the workaround so that I can achieve a Slider with the smoothness shown in the first image, but have the default value label or what looks the same?
If you take a look on source code, you can find _positionAnimationDuration which is responsible to animate the slider
Change it to
static const Duration _positionAnimationDuration = Duration.zero;
Changing on source-code will affect on others project, instead create a local dart file, paste the full slider code and make changes.
Let say we have created customSlider.dart file
. Make sure to replace some(./xyz.dart) top imports with import 'package:flutter/cupertino.dart'; or material on our customSlider.dart.
Then replace _positionAnimationDuration.
To use this, import the file
import 'customSlider.dart' as my_slider;
...
//use case
my_slider.Slider(....)
// create class
// .yaml > another_xlider: ^1.0.0
import 'package:another_xlider/another_xlider.dart';
import '../res/res_controller.dart';
import '../utils/utils_controller.dart';
import 'package:flutter/material.dart';
class RangeBar extends StatelessWidget {
final List<double>? values;
final double? min;
final double? max;
final Function(int, dynamic, dynamic)? onDragging;
const RangeBar(
{Key? key,
#required this.values,
#required this.onDragging,
#required this.min,
#required this.max})
: super(key: key);
#override
Widget build(BuildContext context) {
return FlutterSlider(
values: values!,
// pre set values
rangeSlider: true,
handlerAnimation: FlutterSliderHandlerAnimation(
curve: Curves.elasticOut,
reverseCurve: Curves.bounceIn,
duration: Duration(milliseconds: 500),
scale: 1.5),
jump: true,
min: min ?? 0,
max: max ?? 0,
touchSize: Sizes.s13,
trackBar: FlutterSliderTrackBar(
activeTrackBar: BoxDecoration(color: AppColors.orange),
),
tooltip: FlutterSliderTooltip(
boxStyle: FlutterSliderTooltipBox(
decoration: BoxDecoration(
color: Colors.greenAccent,
borderRadius: BorderRadius.all(Radius.circular(Sizes.s5)),
border: Border.all(
color: AppColors.steelGray,
),
),
),
positionOffset: FlutterSliderTooltipPositionOffset(top: -Sizes.s15),
alwaysShowTooltip: true,
textStyle:
TextStyles.defaultRegular.copyWith(fontSize: FontSizes.s11),
),
onDragging: onDragging);
}
}
// try to call
Container(
child: _size(),
)
Widget _size() {
{
double sizeMin;
double sizeMax;
sizeMin = 0;
sizeMax = 0;
sizeMax = sizeMin.round() == sizeMax.round() ? sizeMax + 1 : sizeMax;
return RangeBar(
values: [
// add values
],
min: sizeMin,
max: sizeMax.ceilToDouble(),
onDragging: (p0, lowerValue, upperValue) {
log(lowerValue);
log(upperValue);
},
);
return C0();
}
}
I have a SliverPersistentHeader which contains a video. The desired behavior of this view is that as a user scrolls upward, the view should cover or minimize the size of the video. The video header is a widget containing a Chewie video player. The desired behavior works up to a certain point at which I get a pixel overflow as shown in this animation:
When the scroll reaches a certain point, the video can no longer resize and it results in a render overflow. The desired behavior would be for the video to continue to resize until it's gone, or to catch the error and hide or remove the video from the view. The code rendering this scroll view is:
Widget buildScollView(GenericScreenModel model) {
return CustomScrollView(
slivers: [
StandardHeader(),
SliverFillRemaining(
child: Container(
// color: Colors.transparent,
decoration: BoxDecoration(
border: Border.all(
color: Colors.white,
),
borderRadius: BorderRadius.only(topRight: radius, topLeft: radius)),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Text(model.model?.getContentText ?? 'Empty'),
)),
)
],
);
}
The StandardHeader class is a simple widget containing a Chewie video.
class _StandardHeaderState extends State<StandardHeader> {
#override
Widget build(BuildContext context) {
return SliverPersistentHeader(
floating: true,
delegate: Delegate(
Colors.blue,
'Header Title',
),
pinned: true,
);
}
}
Is there a way to catch this error and hide the video player? Can anyone help with this or point me to a resource? Thanks!
The issue seems to be with the Chewie and/or video player widget. If the header's height is less than the required height of the player, the overflow occurs.
You can achieve the desired effect by using a SingleChildRenderObjectWidget. I added an opacity factor that you can easily remove that gives it (in my opinion) an extra touch.
I named this widget: ClipBelowHeight
Output:
Source:
ClipBelowHeight is SingleChildRenderObjectWidget that adds the desired effect by using a clipHeight parameter to clamp the height of the child to one that does not overflow. It centers its child vertically (Chewie player in this case).
To understand more, read the comments inside the performLayout and paint method.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class ClipBelowHeight extends SingleChildRenderObjectWidget {
const ClipBelowHeight({
super.key,
super.child,
required this.clipHeight,
required this.opacityFactor,
});
/// The minimum height the [child] must have, as well as the height at which
/// clipping begins.
final double clipHeight;
/// The opacity factor to apply when the height decreases.
final double opacityFactor;
#override
RenderObject createRenderObject(BuildContext context) {
return RenderClipBelowHeight(clipHeight: clipHeight, factor: opacityFactor);
}
#override
void updateRenderObject(
BuildContext context,
RenderClipBelowHeight renderObject,
) {
renderObject
..clipHeight = clipHeight
..factor = opacityFactor;
}
}
class RenderClipBelowHeight extends RenderBox with RenderObjectWithChildMixin {
RenderClipBelowHeight({required double clipHeight, required double factor})
: _clipHeight = clipHeight,
_factor = factor;
double _clipHeight;
double get clipHeight => _clipHeight;
set clipHeight(double value) {
assert(value >= .0);
if (_clipHeight == value) return;
_clipHeight = value;
markNeedsLayout();
}
double _factor;
double get factor => _factor;
set factor(double value) {
assert(value >= .0);
if (_factor == value) return;
_factor = value;
markNeedsLayout();
}
#override
bool get sizedByParent => false;
#override
void performLayout() {
/// The child contraints depend on whether [constraints.maxHeight] is less
/// than [clipHeight]. This RenderObject's responsibility is to ensure that
/// the child's height is never below [clipHeight], because when the
/// child's height is below [clipHeight], then there will be visual
/// overflow.
final childConstraints = constraints.maxHeight < _clipHeight
? BoxConstraints.tight(Size(constraints.maxWidth, _clipHeight))
: constraints;
(child as RenderBox).layout(childConstraints, parentUsesSize: true);
size = Size(constraints.maxWidth, constraints.maxHeight);
}
#override
void paint(PaintingContext context, Offset offset) {
final theChild = child as RenderBox;
/// Clip the painted area to [size], which allows the [child] height to
/// be greater than [size] without overflowing.
context.pushClipRect(
true,
offset,
Offset.zero & size,
(PaintingContext context, Offset offset) {
/// (optional) Set the opacity by applying the specified factor.
context.pushOpacity(
offset,
/// The opacity begins to take effect at approximately half [size].
((255.0 + 128.0) * _factor).toInt(),
(context, offset) {
/// Ensure the child remains centered vertically based on [size].
final centeredOffset =
Offset(.0, (size.height - theChild.size.height) / 2.0);
context.paintChild(theChild, centeredOffset + offset);
},
);
},
);
}
#override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
final theChild = child as RenderBox;
var childParentData = theChild.parentData as BoxParentData;
final isHit = result.addWithPaintOffset(
offset: childParentData.offset,
position: position,
hitTest: (BoxHitTestResult result, Offset transformed) {
assert(transformed == position - childParentData.offset);
return theChild.hitTest(result, position: transformed);
},
);
return isHit;
}
#override
Size computeDryLayout(BoxConstraints constraints) => constraints.biggest;
#override
double computeMinIntrinsicWidth(double height) =>
(child as RenderBox).getMinIntrinsicWidth(height);
#override
double computeMaxIntrinsicWidth(double height) =>
(child as RenderBox).getMaxIntrinsicWidth(height);
#override
double computeMinIntrinsicHeight(double width) =>
(child as RenderBox).getMinIntrinsicHeight(width);
#override
double computeMaxIntrinsicHeight(double width) =>
(child as RenderBox).getMaxIntrinsicHeight(width);
}
The widget that uses the ClipBelowHeight widget is your header delegate. This widget should be self-explanatory and I think that you will be able to understand it.
class Delegate extends SliverPersistentHeaderDelegate {
Delegate(this.color, this.player);
final Color color;
final Chewie player;
#override
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent,
) {
return Container(
color: color,
child: ClipBelowHeight(
clipHeight: 80.0,
opacityFactor: 1.0 - shrinkOffset / maxExtent,
child: player,
),
);
}
#override
double get maxExtent => 150.0;
#override
double get minExtent => .0;
#override
bool shouldRebuild(Delegate oldDelegate) {
return color != oldDelegate.color || player != oldDelegate.player;
}
}
I am trying to build a pie chart that will look like this:
I've tried both Flutter_Charts and FL_Chart, but it seems none of them support a rounded corner and spaced items in the pie chart.
Does anyone know what is the best way to achieve this design as a pie chart?
Thank you!
A very similar version to your chart can easily be achieved with the CustomPaint widget.
Here is the resulting chart
To achieve this you will just need a very rudimentary CustomPainter that draws arcs across its canvas.
The rounding effect is achieved through the strokeCap attribute of the Paint that is used to draw the stroke. Sadly StrokeCap only supports
round and square stroke endings.
A rounded rectangle effect like the one in your screenshot cannot be achieved through this.
Colors are achieved by using a separate Paint for each stroke.
// this is used to pass data about chart values to the widget
class PieChartData {
const PieChartData(this.color, this.percent);
final Color color;
final double percent;
}
// our pie chart widget
class PieChart extends StatelessWidget {
PieChart({
required this.data,
required this.radius,
this.strokeWidth = 8,
this.child,
Key? key,
}) : // make sure sum of data is never ovr 100 percent
assert(data.fold<double>(0, (sum, data) => sum + data.percent) <= 100),
super(key: key);
final List<PieChartData> data;
// radius of chart
final double radius;
// width of stroke
final double strokeWidth;
// optional child; can be used for text for example
final Widget? child;
#override
Widget build(context) {
return CustomPaint(
painter: _Painter(strokeWidth, data),
size: Size.square(radius),
child: SizedBox.square(
// calc diameter
dimension: radius * 2,
child: Center(
child: child,
),
),
);
}
}
// responsible for painting our chart
class _PainterData {
const _PainterData(this.paint, this.radians);
final Paint paint;
final double radians;
}
class _Painter extends CustomPainter {
_Painter(double strokeWidth, List<PieChartData> data) {
// convert chart data to painter data
dataList = data
.map((e) => _PainterData(
Paint()
..color = e.color
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round,
// remove padding from stroke
(e.percent - _padding) * _percentInRadians,
))
.toList();
}
static const _percentInRadians = 0.062831853071796;
// this is the gap between strokes in percent
static const _padding = 4;
static const _paddingInRadians = _percentInRadians * _padding;
// 0 radians is to the right, but since we want to start from the top
// we'll use -90 degrees in radians
static const _startAngle = -1.570796 + _paddingInRadians / 2;
late final List<_PainterData> dataList;
#override
void paint(Canvas canvas, Size size) {
final rect = Offset.zero & size;
// keep track of start angle for next stroke
double startAngle = _startAngle;
for (final data in dataList) {
final path = Path()..addArc(rect, startAngle, data.radians);
startAngle += data.radians + _paddingInRadians;
canvas.drawPath(path, data.paint);
}
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return oldDelegate != this;
}
}
You can check out the dartpad to experiment with a working example.
I am positive that the same chart you provided in that picture can be
achieved with a CustomPainter but that will be a lot more complex.
I am trying to change the legend icon from circle to rectangle for my pie chart. I am using the below lines for that but am getting error right away.
defaultRenderer: new charts.ArcRendererConfig(
symbolRenderer: new IconRenderer(Icons.cloud)
),
I am getting the error (screenshot attached)
please help me to change the default icon for the legend in pie chart.
Thanks #Midhun MP for the hint, actually we need to use CustomSymbolRenderer instead of SymbolRenderer. The below code solved my issue
class IconRenderer extends charts.CustomSymbolRenderer {
final IconData iconData;
IconRenderer(this.iconData);
#override
Widget build(BuildContext context, {Size size, Color color, bool enabled}) {
// Lighten the color if the symbol is not enabled
// Example: If user has tapped on a Series deselecting it.
if (!enabled) {
color = color.withOpacity(0.26);
}
return new SizedBox.fromSize(
size: size, child: new Icon(iconData, color: color, size: 12.0));
}
}
You are getting that error because there is no IconRenderer class by default. You have to create a custom SymbolRenderer like below:
class IconRenderer extends charts.SymbolRenderer {
final IconData iconData;
IconRenderer(this.iconData);
#override
Widget build(BuildContext context,
{Size size, Color color, bool isSelected}) {
return new SizedBox.fromSize(
size: size, child: new Icon(iconData, color: color, size: 12.0));
}
}
Reference legend_custom_symbol
I am new to the flutter and i am trying implement Pie chart(" Donut chart") but is it not working well. I saw some tutorials, but they are used short cut way.
Please help me in implementing a pie chart
add dependency in pubspec.yaml
dependencies: charts_flutter: ^0.9.0
/// Donut chart example. This is a simple pie chart with a hole in the middle.
import 'package:charts_flutter/flutter.dart' as charts;
import 'package:flutter/material.dart';
class DonutPieChart extends StatelessWidget {
final List<charts.Series> seriesList;
final bool animate;
DonutPieChart(this.seriesList, {this.animate});
/// Creates a [PieChart] with sample data and no transition.
factory DonutPieChart.withSampleData() {
return new DonutPieChart(
_createSampleData(),
// Disable animations for image tests.
animate: false,
);
}
#override
Widget build(BuildContext context) {
return new charts.PieChart(seriesList,
animate: animate,
// Configure the width of the pie slices to 60px. The remaining space in
// the chart will be left as a hole in the center.
defaultRenderer: new charts.ArcRendererConfig(arcWidth: 60));
}
/// Create one series with sample hard coded data.
static List<charts.Series<LinearSales, int>> _createSampleData() {
final data = [
new LinearSales(0, 100),
new LinearSales(1, 75),
new LinearSales(2, 25),
new LinearSales(3, 5),
];
return [
new charts.Series<LinearSales, int>(
id: 'Sales',
domainFn: (LinearSales sales, _) => sales.year,
measureFn: (LinearSales sales, _) => sales.sales,
data: data,
)
];
}
}
/// Sample linear data type.
class LinearSales {
final int year;
final int sales;
LinearSales(this.year, this.sales);
}