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
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,15 @@ class CandlestickChartSample1State extends State<CandlestickChartSample1> {
),
),
),
candleMarkerPainter: BuyAndSellSignalPainter(
buySignalCondition: (spot, index) {
return spot.isUp && spot.high > 60000;
},
sellSignalCondition: (spot, index) {
return !spot.isUp && spot.low < 40000;
},
tradeSignalCondition: (spot, index) => false,
),
),
),
),
Expand Down
265 changes: 265 additions & 0 deletions lib/src/chart/candlestick_chart/candlestick_chart_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class CandlestickChartData extends AxisChartData with EquatableMixin {
super.backgroundColor,
super.rotationQuarterTurns,
this.touchedPointIndicator,
this.candleMarkerPainter,
}) : candlestickSpots = candlestickSpots ?? const [],
candlestickPainter = candlestickPainter ?? DefaultCandlestickPainter(),
candlestickTouchData = candlestickTouchData ?? CandlestickTouchData(),
Expand Down Expand Up @@ -108,6 +109,13 @@ class CandlestickChartData extends AxisChartData with EquatableMixin {
/// if you want to have customized [touchedPointIndicator]
final AxisSpotIndicator? touchedPointIndicator;

/// The painter used to draw markers on candlesticks.
///
/// For example, buy/sell signal markers, important point markers, etc.
/// If null, no markers will be drawn.
/// You can use the built-in [BuySellSignalMarkerPainter] or implement your own.
final CandleMarkerPainter? candleMarkerPainter;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think if we allow users to have a list of CandleMarkerPainter?
This way, we can let the user compose different marker painters to render on top of each other.


/// Lerps a [CandlestickChartData] based on [t] value, check [Tween.lerp].
@override
CandlestickChartData lerp(BaseChartData a, BaseChartData b, double t) {
Expand Down Expand Up @@ -144,6 +152,7 @@ class CandlestickChartData extends AxisChartData with EquatableMixin {
backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t),
rotationQuarterTurns: b.rotationQuarterTurns,
touchedPointIndicator: b.touchedPointIndicator,
candleMarkerPainter: b.candleMarkerPainter,
);
} else {
throw Exception('Illegal State');
Expand Down Expand Up @@ -171,6 +180,7 @@ class CandlestickChartData extends AxisChartData with EquatableMixin {
Color? backgroundColor,
int? rotationQuarterTurns,
AxisSpotIndicator? touchedPointIndicator,
CandleMarkerPainter? candleMarkerPainter,
}) =>
CandlestickChartData(
candlestickSpots: candlestickSpots ?? this.candlestickSpots,
Expand All @@ -193,6 +203,7 @@ class CandlestickChartData extends AxisChartData with EquatableMixin {
rotationQuarterTurns: rotationQuarterTurns ?? this.rotationQuarterTurns,
touchedPointIndicator:
touchedPointIndicator ?? this.touchedPointIndicator,
candleMarkerPainter: candleMarkerPainter ?? this.candleMarkerPainter,
);

/// Used for equality check, see [EquatableMixin].
Expand All @@ -216,6 +227,7 @@ class CandlestickChartData extends AxisChartData with EquatableMixin {
borderData,
rotationQuarterTurns,
touchedPointIndicator,
candleMarkerPainter,
];
}

Expand Down Expand Up @@ -997,3 +1009,256 @@ class CandlestickChartDataTween extends Tween<CandlestickChartData> {
@override
CandlestickChartData lerp(double t) => begin!.lerp(begin!, end!, t);
}

/// Base class for custom candle markers.
///
/// Use this class to implement your own marker painter.
abstract class CandleMarkerPainter with EquatableMixin {
const CandleMarkerPainter();

/// Determines whether this marker should be shown on the given candle spot.
///
/// [spot] contains the candlestick data (open, high, low, close, x).
/// [spotIndex] is the index of the spot in the candlestickSpots list.
///
/// Return true if you want to draw this marker on this spot, false otherwise.
bool shouldShow(CandlestickSpot spot, int spotIndex);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we actually need it? I think we can have an empty paint() implementation to not show anything.
This way simplifies the implementation


/// Paints the marker on the canvas.
///
/// [canvas] is the canvas to paint on.
/// [spot] contains the candlestick data.
/// [spotIndex] is the index of the spot.
/// [xInCanvasProvider] is a function that converts data X values to canvas pixel X.
/// [yInCanvasProvider] is a function that converts data Y values to canvas pixel Y.
void paint(
Canvas canvas,
CandlestickSpot spot,
int spotIndex,
ValueInCanvasProvider xInCanvasProvider,
ValueInCanvasProvider yInCanvasProvider,
);

/// Used for animation transitions.
///
/// For static markers, you can simply return b.
CandleMarkerPainter lerp(
CandleMarkerPainter a,
CandleMarkerPainter b,
double t,
);

@override
List<Object?> get props => [];
}

/// Signal type enum for buy/sell markers.
enum BuyAndSellSignalType {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion, we should prevent defining domain-specific models inside the fl_chart. And it's better to keep things generic.

So I suggest removing it (and adding it inside the candlestick_chart_sample1.dart if you want), and here we can rename the BuyAndSellSignalPainter() to CandleBalloonTextPainter(). This way, it feels like a generic implementation of balloon text, and you can use it to show buy/sell in your implementation.

And the logic of CandleBalloonTextPainter() will be way simpler, and the complexity goes into the candlestick_chart_sample1.dart where you want to show sell/buy points.

buy,
sell,
trade,
}

/// Typedef for custom balloon text builder.
typedef BalloonTextBuilder = TextPainter? Function(
CandlestickSpot spot,
int spotIndex,
BuyAndSellSignalType signalType,
);

/// A marker painter that displays buy, sell, and trade signals on candlesticks.
///
/// This painter allows you to mark signal points on your candlestick chart.
/// You can customize the conditions for displaying different signals and
/// provide custom balloon painters and text builders.
class BuyAndSellSignalPainter extends CandleMarkerPainter {
const BuyAndSellSignalPainter({
required this.buySignalCondition,
required this.sellSignalCondition,
required this.tradeSignalCondition,
this.balloonPainter = _defaultBalloonPainter,
this.balloonTextBuilder = _defaultBalloonText,
});

/// A callback that determines whether to show a buy signal for the given candlestick.
final bool Function(CandlestickSpot spot, int spotIndex) buySignalCondition;

/// A callback that determines whether to show a sell signal for the given candlestick.
final bool Function(CandlestickSpot spot, int spotIndex) sellSignalCondition;

/// A callback that determines whether to show a trade signal for the given candlestick.
final bool Function(CandlestickSpot spot, int spotIndex) tradeSignalCondition;

/// Custom balloon painter for drawing different signal types.
/// Users can override this to customize balloon appearance and colors.
final void Function(Canvas, Offset, BuyAndSellSignalType) balloonPainter;

/// Custom balloon text builder for displaying text inside the balloon.
/// Users can override this to customize the text shown for each signal type.
final BalloonTextBuilder balloonTextBuilder;

static const double _iconOffsetY = -15;

@override
bool shouldShow(CandlestickSpot spot, int spotIndex) {
return buySignalCondition(spot, spotIndex) ||
sellSignalCondition(spot, spotIndex) ||
tradeSignalCondition.call(spot, spotIndex);
}

@override
void paint(
Canvas canvas,
CandlestickSpot spot,
int spotIndex,
ValueInCanvasProvider xInCanvasProvider,
ValueInCanvasProvider yInCanvasProvider,
) {
final pixelX = xInCanvasProvider(spot.x);
final highPixelY = yInCanvasProvider(spot.high) + _iconOffsetY;
final iconCenter = Offset(pixelX, highPixelY);

if (buySignalCondition(spot, spotIndex)) {
balloonPainter(canvas, iconCenter, BuyAndSellSignalType.buy);
_drawBalloonText(
canvas,
iconCenter,
spot,
spotIndex,
BuyAndSellSignalType.buy,
);
}

if (sellSignalCondition(spot, spotIndex)) {
balloonPainter(canvas, iconCenter, BuyAndSellSignalType.sell);
_drawBalloonText(
canvas,
iconCenter,
spot,
spotIndex,
BuyAndSellSignalType.sell,
);
}

if (tradeSignalCondition(spot, spotIndex)) {
balloonPainter(canvas, iconCenter, BuyAndSellSignalType.trade);
_drawBalloonText(
canvas,
iconCenter,
spot,
spotIndex,
BuyAndSellSignalType.trade,
);
}
}

void _drawBalloonText(
Canvas canvas,
Offset center,
CandlestickSpot spot,
int spotIndex,
BuyAndSellSignalType signalType,
) {
final textPainter = balloonTextBuilder(spot, spotIndex, signalType);
if (textPainter != null) {
textPainter.layout();
final textOffset = Offset(
center.dx - textPainter.width / 2,
center.dy - textPainter.height / 2,
);
textPainter.paint(canvas, textOffset);
}
}

@override
CandleMarkerPainter lerp(
CandleMarkerPainter a,
CandleMarkerPainter b,
double t,
) {
// Marker configuration (conditions and painters) are functions that cannot be interpolated.
// During animation transitions, we directly return the target painter instead of blending.
if (b is BuyAndSellSignalPainter) {
return b;
}
return this;
}

@override
List<Object?> get props => [
buySignalCondition,
sellSignalCondition,
tradeSignalCondition,
balloonPainter,
balloonTextBuilder,
];

/// Default balloon painter for different signal types.
static void _defaultBalloonPainter(
Canvas canvas,
Offset center,
BuyAndSellSignalType signalType,
) {
const balloonRadius = 6.0;
const tailHeight = 3.0;

// Determine color based on signal type
final color = switch (signalType) {
BuyAndSellSignalType.buy => const Color(0xFF4CAF50), // Green
BuyAndSellSignalType.sell => const Color(0xFFEF5350), // Red
BuyAndSellSignalType.trade => const Color(0xFF2196F3), // Blue
};

final paint = Paint()
..color = color
..style = PaintingStyle.fill;

// Create circular part of balloon
final balloonCircle = Path()
..addOval(
Rect.fromCircle(
center: center,
radius: balloonRadius,
),
);

// Create tail (triangle pointing down)
final tailTopY = center.dy + balloonRadius - 1;
final tailTipY = center.dy + balloonRadius + tailHeight;
final balloonTail = Path()
..moveTo(center.dx - 2.5, tailTopY)
..lineTo(center.dx, tailTipY)
..lineTo(center.dx + 2.5, tailTopY)
..close();

// Paint both circle and tail
canvas
..drawPath(balloonCircle, paint)
..drawPath(balloonTail, paint);
}

/// Default balloon text builder for different signal types.
static TextPainter? _defaultBalloonText(
CandlestickSpot spot,
int spotIndex,
BuyAndSellSignalType signalType,
) {
final text = switch (signalType) {
BuyAndSellSignalType.buy => 'B',
BuyAndSellSignalType.sell => 'S',
BuyAndSellSignalType.trade => 'T',
};

return TextPainter(
text: TextSpan(
text: text,
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
textDirection: TextDirection.ltr,
);
}
}
34 changes: 34 additions & 0 deletions lib/src/chart/candlestick_chart/candlestick_chart_painter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class CandlestickChartPainter extends AxisChartPainter<CandlestickChartData> {
super.paint(context, canvasWrapper, holder);
drawAxisSpotIndicator(context, canvasWrapper, holder);
drawCandlesticks(context, canvasWrapper, holder);
drawCandleMarkers(context, canvasWrapper, holder);

if (holder.chartVirtualRect != null) {
canvasWrapper.restore();
Expand Down Expand Up @@ -118,6 +119,39 @@ class CandlestickChartPainter extends AxisChartPainter<CandlestickChartData> {
}
}

@visibleForTesting
void drawCandleMarkers(
BuildContext context,
CanvasWrapper canvasWrapper,
PaintHolder<CandlestickChartData> holder,
) {
final painter = holder.data.candleMarkerPainter;
if (painter == null) {
return;
}

final data = holder.data;
final viewSize = canvasWrapper.size;

for (var i = 0; i < data.candlestickSpots.length; i++) {
final spot = data.candlestickSpots[i];

if (!spot.show) {
continue;
}

if (painter.shouldShow(spot, i)) {
painter.paint(
canvasWrapper.canvas,
spot,
i,
(x) => getPixelX(x, viewSize, holder),
(y) => getPixelY(y, viewSize, holder),
);
}
}
}

@visibleForTesting
void drawTouchTooltips(
BuildContext context,
Expand Down
Loading