From ce76632b761de28ee16563187208da242d7eca6b Mon Sep 17 00:00:00 2001 From: Bernardo Belchior Date: Mon, 18 Apr 2022 15:33:32 +0100 Subject: [PATCH] improv: add support for custom modifiers --- example/lib/pages/polygon_custom.dart | 137 ++++++++++++++++++++++++++ lib/graphic.dart | 17 ++-- lib/src/dataflow/tuple.dart | 2 +- lib/src/geom/modifier/dodge.dart | 13 ++- lib/src/geom/modifier/jitter.dart | 13 ++- lib/src/geom/modifier/modifier.dart | 27 +++++ lib/src/geom/modifier/stack.dart | 7 ++ lib/src/geom/modifier/symmetric.dart | 9 +- lib/src/parse/parse.dart | 43 +++----- 9 files changed, 223 insertions(+), 45 deletions(-) diff --git a/example/lib/pages/polygon_custom.dart b/example/lib/pages/polygon_custom.dart index 6032fdb8..359c0b27 100644 --- a/example/lib/pages/polygon_custom.dart +++ b/example/lib/pages/polygon_custom.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:graphic/graphic.dart'; @@ -637,6 +639,54 @@ class PolygonCustomPage extends StatelessWidget { ], ), ), + Container( + child: const Text( + 'Custom Modifier', + style: TextStyle(fontSize: 20), + ), + padding: const EdgeInsets.fromLTRB(20, 40, 20, 5), + ), + Container( + child: const Text( + '- With dodge and size modifier that scales the interval element width to fit within its band', + ), + padding: const EdgeInsets.fromLTRB(10, 5, 10, 0), + alignment: Alignment.centerLeft, + ), + Container( + margin: const EdgeInsets.only(top: 10), + width: 350, + height: 300, + child: Chart( + padding: (_) => const EdgeInsets.fromLTRB(40, 5, 10, 40), + data: adjustData, + variables: { + 'index': Variable( + accessor: (Map map) => map['index'].toString(), + ), + 'type': Variable( + accessor: (Map map) => map['type'] as String, + ), + 'value': Variable( + accessor: (Map map) => map['value'] as num, + ), + }, + elements: [ + IntervalElement( + position: + Varset('index') * Varset('value') / Varset('type'), + color: ColorAttr( + variable: 'type', values: Defaults.colors10), + size: SizeAttr(value: 2), + modifiers: [DodgeSizeModifier()], + ) + ], + axes: [ + Defaults.horizontalAxis..tickLine = TickLine(), + Defaults.verticalAxis, + ], + ), + ), Container( child: const Text( 'Candlestick Chart', @@ -733,3 +783,90 @@ class PolygonCustomPage extends StatelessWidget { ); } } + +/// Changes the position of elements while also updating their width to match +/// the number of elements in a single band. Useful for bar charts when the +/// width of the bars can be dynamic. +@immutable +class DodgeSizeModifier extends Modifier { + @override + bool operator ==(Object other) => other is DodgeSizeModifier; + + @override + DodgeSizeGeomModifierOp toGeomModifierOp(ToGeomModifierOpParams params) { + return DodgeSizeGeomModifierOp({ + 'form': params.form, + 'scales': params.scales, + 'groups': params.groups, + 'coord': params.coord, + }); + } +} + +const _kBaseGroupPaddingHorizontal = 32.0; +const _kXAxis = 1; +const _kMinBarSize = 4.0; + +/// The dodge geometry modifier. +class DodgeSizeGeomModifier extends GeomModifier { + DodgeSizeGeomModifier(this.band, this.coord); + + final CoordConv coord; + + /// The band ratio of each value. It represents the width of the interval the + /// [Aes]es have to position themselves within. + /// Its range is ]0, 1]. + final double band; + + @override + void modify(AesGroups value) { + final ratio = 1 / value.length; + final numGroups = value.length; + final groupHorizontalPadding = _kBaseGroupPaddingHorizontal / numGroups; + final invertedGroupPaddingHorizontal = + coord.invertDistance(groupHorizontalPadding, _kXAxis); + + final effectiveBand = band - 2 * invertedGroupPaddingHorizontal; + + final maxWidth = coord.convert(const Offset(1, 0)).dx; + final maxWidthInBand = effectiveBand * maxWidth; + final maxWidthPerAes = maxWidthInBand / numGroups; + final barHorizontalPadding = groupHorizontalPadding / 2; + final size = max(maxWidthPerAes - barHorizontalPadding, _kMinBarSize); + + final bias = ratio * effectiveBand; + + // Negatively shift half of the total bias. + var accumulated = -bias * (numGroups + 1) / 2; + + for (final group in value) { + for (final aes in group) { + final oldPosition = aes.position; + aes.position = oldPosition + .map( + (point) => Offset(point.dx + accumulated + bias, point.dy), + ) + .toList(); + + aes.size = size; + } + accumulated += bias; + } + } +} + +class DodgeSizeGeomModifierOp extends GeomModifierOp { + DodgeSizeGeomModifierOp(Map params) : super(params); + + @override + DodgeSizeGeomModifier evaluate() { + final form = params['form'] as AlgForm; + final scales = params['scales'] as Map; + final coord = params['coord'] as CoordConv; + + final xField = form.first[0]; + final band = (scales[xField]! as DiscreteScaleConv).band; + + return DodgeSizeGeomModifier(band, coord); + } +} diff --git a/lib/graphic.dart b/lib/graphic.dart index 8128c67f..cbb97a06 100644 --- a/lib/graphic.dart +++ b/lib/graphic.dart @@ -125,12 +125,12 @@ export 'src/variable/transform/map.dart' show MapTrans; export 'src/variable/transform/proportion.dart' show Proportion; export 'src/variable/transform/sort.dart' show Sort; -export 'src/scale/scale.dart' show Scale; -export 'src/scale/discrete.dart' show DiscreteScale; -export 'src/scale/continuous.dart' show ContinuousScale; -export 'src/scale/linear.dart' show LinearScale; -export 'src/scale/ordinal.dart' show OrdinalScale; -export 'src/scale/time.dart' show TimeScale; +export 'src/scale/scale.dart' show Scale, ScaleConv; +export 'src/scale/discrete.dart' show DiscreteScale, DiscreteScaleConv; +export 'src/scale/continuous.dart' show ContinuousScale, ContinuousScaleConv; +export 'src/scale/linear.dart' show LinearScale, LinearScaleConv; +export 'src/scale/ordinal.dart' show OrdinalScale, OrdinalScaleConv; +export 'src/scale/time.dart' show TimeScale, TimeScaleConv; export 'src/geom/element.dart' show GeomElement; export 'src/geom/function.dart' show FunctionElement; @@ -141,7 +141,8 @@ export 'src/geom/interval.dart' show IntervalElement; export 'src/geom/line.dart' show LineElement; export 'src/geom/point.dart' show PointElement; export 'src/geom/polygon.dart' show PolygonElement; -export 'src/geom/modifier/modifier.dart' show Modifier; +export 'src/geom/modifier/modifier.dart' + show Modifier, GeomModifier, GeomModifierOp, ToGeomModifierOpParams; export 'src/geom/modifier/dodge.dart' show DodgeModifier; export 'src/geom/modifier/stack.dart' show StackModifier; export 'src/geom/modifier/jitter.dart' show JitterModifier; @@ -201,7 +202,7 @@ export 'src/common/label.dart' export 'src/common/defaults.dart' show Defaults; export 'src/common/dim.dart' show Dim; -export 'src/dataflow/tuple.dart' show Tuple, Aes; +export 'src/dataflow/tuple.dart' show Tuple, Aes, AesGroups; export 'src/util/path.dart' show Paths; export 'package:path_drawing/path_drawing.dart' show DashOffset; diff --git a/lib/src/dataflow/tuple.dart b/lib/src/dataflow/tuple.dart index 18b71129..14280d3c 100644 --- a/lib/src/dataflow/tuple.dart +++ b/lib/src/dataflow/tuple.dart @@ -66,7 +66,7 @@ class Aes { final Label? label; /// The size of the tuple. - final double? size; + double? size; /// The represent point of [position] points. Offset get representPoint => shape.representPoint(position); diff --git a/lib/src/geom/modifier/dodge.dart b/lib/src/geom/modifier/dodge.dart index 7b3ab0b8..f7f46a61 100644 --- a/lib/src/geom/modifier/dodge.dart +++ b/lib/src/geom/modifier/dodge.dart @@ -1,9 +1,9 @@ import 'dart:ui'; +import 'package:graphic/src/algebra/varset.dart'; import 'package:graphic/src/dataflow/tuple.dart'; import 'package:graphic/src/scale/discrete.dart'; import 'package:graphic/src/scale/scale.dart'; -import 'package:graphic/src/algebra/varset.dart'; import 'modifier.dart'; @@ -34,6 +34,17 @@ class DodgeModifier extends Modifier { super == other && ratio == other.ratio && symmetric == other.symmetric; + + @override + DodgeGeomModifierOp toGeomModifierOp(ToGeomModifierOpParams params) { + return DodgeGeomModifierOp({ + 'ratio': ratio, + 'symmetric': symmetric ?? true, + 'form': params.form, + 'scales': params.scales, + 'groups': params.groups, + }); + } } /// The dodge geometry modifier. diff --git a/lib/src/geom/modifier/jitter.dart b/lib/src/geom/modifier/jitter.dart index dc096168..c7b97ecf 100644 --- a/lib/src/geom/modifier/jitter.dart +++ b/lib/src/geom/modifier/jitter.dart @@ -1,10 +1,10 @@ -import 'dart:ui'; import 'dart:math'; +import 'dart:ui'; +import 'package:graphic/src/algebra/varset.dart'; import 'package:graphic/src/dataflow/tuple.dart'; import 'package:graphic/src/scale/discrete.dart'; import 'package:graphic/src/scale/scale.dart'; -import 'package:graphic/src/algebra/varset.dart'; import 'modifier.dart'; @@ -26,6 +26,15 @@ class JitterModifier extends Modifier { @override bool operator ==(Object other) => other is JitterModifier && super == other && ratio == other.ratio; + + @override + JitterGeomModifierOp toGeomModifierOp(ToGeomModifierOpParams params) { + return JitterGeomModifierOp({ + 'ratio': ratio ?? 0.5, + 'form': params.form, + 'scales': params.scales, + }); + } } /// The jitter geometry modifier. diff --git a/lib/src/geom/modifier/modifier.dart b/lib/src/geom/modifier/modifier.dart index 5b460ddf..79950271 100644 --- a/lib/src/geom/modifier/modifier.dart +++ b/lib/src/geom/modifier/modifier.dart @@ -1,6 +1,12 @@ +import 'package:graphic/graphic.dart'; import 'package:graphic/src/common/modifier.dart' as common; import 'package:graphic/src/dataflow/operator.dart'; import 'package:graphic/src/dataflow/tuple.dart'; +import 'package:graphic/src/scale/scale.dart'; + +import '../../aes/position.dart'; +import '../../algebra/varset.dart'; +import '../../coord/coord.dart'; /// The specification of a collision modifier. /// @@ -9,6 +15,27 @@ import 'package:graphic/src/dataflow/tuple.dart'; abstract class Modifier { @override bool operator ==(Object other) => other is Modifier; + + GeomModifierOp toGeomModifierOp(ToGeomModifierOpParams params); +} + +/// The only use of this class to pass parameters to [Modifier.toGeomModifierOp]. +/// A class is used, instead of named arguments, to avoid breaking changes when +/// adding new fields to these parameters. +class ToGeomModifierOpParams { + final AlgForm? form; + final ScaleConvOp scales; + final Operator groups; + final OriginOp origin; + final CoordConvOp coord; + + ToGeomModifierOpParams({ + required this.form, + required this.scales, + required this.groups, + required this.origin, + required this.coord, + }); } /// The base class of geometry modifiers. diff --git a/lib/src/geom/modifier/stack.dart b/lib/src/geom/modifier/stack.dart index 3abfb5c8..626f7b1c 100644 --- a/lib/src/geom/modifier/stack.dart +++ b/lib/src/geom/modifier/stack.dart @@ -16,6 +16,13 @@ import 'modifier.dart'; class StackModifier extends Modifier { @override bool operator ==(Object other) => other is StackModifier && super == other; + + @override + StackGeomModifierOp toGeomModifierOp(ToGeomModifierOpParams params) { + return StackGeomModifierOp({ + 'origin': params.origin, + }); + } } /// The stack geometry modifier. diff --git a/lib/src/geom/modifier/symmetric.dart b/lib/src/geom/modifier/symmetric.dart index 755d6ced..7a27da00 100644 --- a/lib/src/geom/modifier/symmetric.dart +++ b/lib/src/geom/modifier/symmetric.dart @@ -1,5 +1,5 @@ -import 'dart:ui'; import 'dart:math'; +import 'dart:ui'; import 'package:graphic/src/dataflow/tuple.dart'; @@ -15,6 +15,13 @@ class SymmetricModifier extends Modifier { @override bool operator ==(Object other) => other is SymmetricModifier && super == other; + + @override + SymmetricGeomModifierOp toGeomModifierOp(ToGeomModifierOpParams params) { + return SymmetricGeomModifierOp({ + 'origin': params.origin, + }); + } } /// The symmetric geometry modifier. diff --git a/lib/src/parse/parse.dart b/lib/src/parse/parse.dart index 1516c934..c3b1b181 100644 --- a/lib/src/parse/parse.dart +++ b/lib/src/parse/parse.dart @@ -5,10 +5,10 @@ import 'package:flutter/painting.dart'; import 'package:graphic/src/aes/aes.dart'; import 'package:graphic/src/aes/channel.dart'; import 'package:graphic/src/aes/color.dart'; +import 'package:graphic/src/aes/elevation.dart'; import 'package:graphic/src/aes/gradient.dart'; import 'package:graphic/src/aes/position.dart'; import 'package:graphic/src/aes/shape.dart'; -import 'package:graphic/src/aes/elevation.dart'; import 'package:graphic/src/aes/size.dart'; import 'package:graphic/src/algebra/varset.dart'; import 'package:graphic/src/chart/chart.dart'; @@ -25,12 +25,9 @@ import 'package:graphic/src/coord/polar.dart'; import 'package:graphic/src/coord/rect.dart'; import 'package:graphic/src/data/data_set.dart'; import 'package:graphic/src/dataflow/operator.dart'; +import 'package:graphic/src/dataflow/tuple.dart'; import 'package:graphic/src/geom/element.dart'; -import 'package:graphic/src/geom/modifier/dodge.dart'; -import 'package:graphic/src/geom/modifier/jitter.dart'; import 'package:graphic/src/geom/modifier/modifier.dart'; -import 'package:graphic/src/geom/modifier/stack.dart'; -import 'package:graphic/src/geom/modifier/symmetric.dart'; import 'package:graphic/src/guide/annotation/custom.dart'; import 'package:graphic/src/guide/annotation/figure.dart'; import 'package:graphic/src/guide/annotation/line.dart'; @@ -48,7 +45,6 @@ import 'package:graphic/src/interaction/signal.dart'; import 'package:graphic/src/scale/linear.dart'; import 'package:graphic/src/scale/ordinal.dart'; import 'package:graphic/src/scale/scale.dart'; -import 'package:graphic/src/dataflow/tuple.dart'; import 'package:graphic/src/scale/time.dart'; import 'package:graphic/src/shape/shape.dart'; import 'package:graphic/src/variable/transform/filter.dart'; @@ -431,32 +427,15 @@ void parse( if (elementSpec.modifiers != null) { for (var modifier in elementSpec.modifiers!) { - GeomModifierOp geomModifier; - if (modifier is DodgeModifier) { - geomModifier = view.add(DodgeGeomModifierOp({ - 'ratio': modifier.ratio, - 'symmetric': modifier.symmetric ?? true, - 'form': form, - 'scales': scales, - 'groups': groups, - })); - } else if (modifier is JitterModifier) { - geomModifier = view.add(JitterGeomModifierOp({ - 'ratio': modifier.ratio ?? 0.5, - 'form': form, - 'scales': scales, - })); - } else if (modifier is StackModifier) { - geomModifier = view.add(StackGeomModifierOp({ - 'origin': origin, - })); - } else if (modifier is SymmetricModifier) { - geomModifier = view.add(SymmetricGeomModifierOp({ - 'origin': origin, - })); - } else { - throw UnimplementedError('No such modifier type: $modifier.'); - } + final params = ToGeomModifierOpParams( + form: form, + scales: scales, + groups: groups, + origin: origin, + coord: coord, + ); + final GeomModifierOp geomModifier = + view.add(modifier.toGeomModifierOp(params)); groups = view.add(ModifyOp({ 'groups': groups, 'modifier': geomModifier,