From bee6cbf25a338e6fae2079d551cdc3498b3ede86 Mon Sep 17 00:00:00 2001 From: Mohammad Zarif Date: Wed, 7 Oct 2026 14:12:15 +0330 Subject: [PATCH 1/4] fix: widen indicator pane bars when fitContent spreads the candles fitContent widened candleWidth and volWidth but not macdWidth, and the indicator panes were handed the unfitted style, so a MACD or custom histogram stayed hairline-thin beside candles spread to fill the plot. macdWidth is spread with the rest and the panes take the fitted style. --- lib/src/chart_style.dart | 5 ++-- lib/src/renderer/base_chart_painter.dart | 1 + lib/src/renderer/chart_painter.dart | 2 +- test/fit_content_test.dart | 36 +++++++++++++++++++++++- 4 files changed, 40 insertions(+), 4 deletions(-) diff --git a/lib/src/chart_style.dart b/lib/src/chart_style.dart index cb59317..bd68e35 100644 --- a/lib/src/chart_style.dart +++ b/lib/src/chart_style.dart @@ -523,8 +523,9 @@ class ChartStyle { /// [pointWidth] is a fixed distance, so a short series — a handful of /// intraday bars, say — bunches up against the left edge and leaves the rest /// of the chart empty. With this set the spacing is widened to whatever makes - /// the series span the plot, and [candleWidth] and [ChartStyle.volWidth] are - /// widened with it so the bars keep their proportions. + /// the series span the plot, and [candleWidth], [ChartStyle.volWidth] and + /// [ChartStyle.macdWidth] are widened with it so the bars keep their + /// proportions. /// /// Only ever widens: a series long enough to fill the plot, or one zoomed in /// past it, is laid out on [pointWidth] as before. diff --git a/lib/src/renderer/base_chart_painter.dart b/lib/src/renderer/base_chart_painter.dart index 636e0f4..f6548dc 100644 --- a/lib/src/renderer/base_chart_painter.dart +++ b/lib/src/renderer/base_chart_painter.dart @@ -477,6 +477,7 @@ abstract class BaseChartPainter extends CustomPainter { pointWidth: fitted, candleWidth: chartStyle.candleWidth * spread, volWidth: chartStyle.volWidth * spread, + macdWidth: chartStyle.macdWidth * spread, ); } mDataLen = mItemCount * mPointWidth; diff --git a/lib/src/renderer/chart_painter.dart b/lib/src/renderer/chart_painter.dart index cfc12e6..ffd9a46 100644 --- a/lib/src/renderer/chart_painter.dart +++ b/lib/src/renderer/chart_painter.dart @@ -423,7 +423,7 @@ class ChartPainter extends BaseChartPainter { mSecondaryRectList[i].mMinValue, mChildPadding, fixedLength, - chartStyle, + fittedStyle, chartColors, panes[i], percentBase: _paneBase(panes[i]), diff --git a/test/fit_content_test.dart b/test/fit_content_test.dart index a3df2bc..8bc2a50 100644 --- a/test/fit_content_test.dart +++ b/test/fit_content_test.dart @@ -12,7 +12,11 @@ ChartPainter _painterOf(WidgetTester tester) { } /// A chart too short to fill its box at the default spacing. -Widget _chart({required bool fitContent, int count = 10}) { +Widget _chart({ + required bool fitContent, + int count = 10, + List indicators = const [], +}) { final data = candles(rampThenFall(count)); DataUtil.calculate(data); @@ -30,6 +34,7 @@ Widget _chart({required bool fitContent, int count = 10}) { volHidden: true, showNowPrice: false, chartStyle: ChartStyle(fitContent: fitContent), + indicators: indicators, ), ), ), @@ -67,6 +72,35 @@ void main() { ); }); + testWidgets('on, an indicator pane\'s bars widen with the spacing', ( + tester, + ) async { + await tester.pumpWidget( + _chart(fitContent: true, indicators: [MacdIndicator()]), + ); + final painter = _painterOf(tester); + + const style = ChartStyle(); + final spread = painter.mPointWidth / style.pointWidth; + expect( + painter.mIndicatorPaneList.single.chartStyle.macdWidth, + closeTo(style.macdWidth * spread, 0.001), + ); + }); + + testWidgets('off, an indicator pane\'s bars keep their width', ( + tester, + ) async { + await tester.pumpWidget( + _chart(fitContent: false, indicators: [MacdIndicator()]), + ); + + expect( + _painterOf(tester).mIndicatorPaneList.single.chartStyle.macdWidth, + const ChartStyle().macdWidth, + ); + }); + testWidgets('on, a series that already fills the plot is left alone', ( tester, ) async { From f0fd52978e70b0fa868e735e10e6ca792f9457b0 Mon Sep 17 00:00:00 2001 From: Mohammad Zarif Date: Wed, 7 Oct 2026 14:12:20 +0330 Subject: [PATCH 2/4] fix: widen histogram bars with the zoom Candles and volume are drawn under the canvas's horizontal scale, but indicators paint in view space, so zooming in grew the candles while a pane's histogram bars stayed at macdWidth. A pane's bars and an overlay's now take the zoom the candles take from the canvas. A bar is never thinner than a pixel, so zooming far out does not make it vanish. --- lib/src/chart_style.dart | 4 +- lib/src/renderer/chart_painter.dart | 1 + lib/src/renderer/indicator_pane_renderer.dart | 4 +- lib/src/renderer/main_renderer.dart | 4 +- lib/src/renderer/series_painter.dart | 6 +- test/fit_content_test.dart | 129 ++++++++++++++++++ 6 files changed, 144 insertions(+), 4 deletions(-) diff --git a/lib/src/chart_style.dart b/lib/src/chart_style.dart index bd68e35..60cf71c 100644 --- a/lib/src/chart_style.dart +++ b/lib/src/chart_style.dart @@ -535,7 +535,9 @@ class ChartStyle { ///vol column width final double volWidth; - ///macd column width + /// Width of the histogram bars in an indicator pane, MACD or custom. + /// The bars follow the candles: they widen when [fitContent] spreads them and + /// when the chart is zoomed in, and are never thinner than a pixel. final double macdWidth; /// Stroke width of an indicator's lines, in a pane or over the candles. diff --git a/lib/src/renderer/chart_painter.dart b/lib/src/renderer/chart_painter.dart index ffd9a46..e3d19a5 100644 --- a/lib/src/renderer/chart_painter.dart +++ b/lib/src/renderer/chart_painter.dart @@ -662,6 +662,7 @@ class ChartPainter extends BaseChartPainter { start: mStartIndex, stop: mStopIndex, xOf: xOf, + scaleX: scaleX, ); } } diff --git a/lib/src/renderer/indicator_pane_renderer.dart b/lib/src/renderer/indicator_pane_renderer.dart index 714061b..80a61c4 100644 --- a/lib/src/renderer/indicator_pane_renderer.dart +++ b/lib/src/renderer/indicator_pane_renderer.dart @@ -136,6 +136,7 @@ class IndicatorPaneRenderer extends BaseChartRenderer { required int start, required int stop, required double Function(int index) xOf, + double scaleX = 1.0, }) { canvas.save(); canvas.clipRect( @@ -156,7 +157,8 @@ class IndicatorPaneRenderer extends BaseChartRenderer { yOf: getY, colors: chartColors, strokeWidth: chartStyle.indicatorLineWidth, - barWidth: chartStyle.macdWidth, + // View space, so the bar takes the zoom the candles take from the canvas. + barWidth: chartStyle.macdWidth * scaleX, zeroY: getY(0).clamp(chartRect.top, chartRect.bottom), ); canvas.restore(); diff --git a/lib/src/renderer/main_renderer.dart b/lib/src/renderer/main_renderer.dart index b69969b..df94e61 100644 --- a/lib/src/renderer/main_renderer.dart +++ b/lib/src/renderer/main_renderer.dart @@ -449,7 +449,9 @@ class MainRenderer extends BaseChartRenderer { yOf: getY, colors: chartColors, strokeWidth: chartStyle.indicatorLineWidth, - barWidth: chartStyle.candleWidth, + // View space, so the bar takes the zoom the candles take from the + // canvas. + barWidth: chartStyle.candleWidth * scaleX, ); } canvas.restore(); diff --git a/lib/src/renderer/series_painter.dart b/lib/src/renderer/series_painter.dart index c380523..9a11eee 100644 --- a/lib/src/renderer/series_painter.dart +++ b/lib/src/renderer/series_painter.dart @@ -1,3 +1,5 @@ +import 'dart:math' as math; + import 'package:material_ui/material_ui.dart'; import '../chart_style.dart'; @@ -227,7 +229,9 @@ void _paintHistogram( required double zeroY, }) { final paint = Paint()..isAntiAlias = true; - final half = barWidth / 2; + // Zoomed far out the candles shrink with the bar; a bar thinner than a hairline + // would vanish. + final half = math.max(barWidth, 1.0) / 2; for (var i = start; i <= stop; i++) { final value = resolved.valueAt(line, i); diff --git a/test/fit_content_test.dart b/test/fit_content_test.dart index 8bc2a50..8ee4cd0 100644 --- a/test/fit_content_test.dart +++ b/test/fit_content_test.dart @@ -41,7 +41,136 @@ Widget _chart({ ); } +/// The widths of the rects an indicator pane draws across its candles. +List _paneBarWidths(WidgetTester tester, {required double scaleX}) { + final painter = _painterOf(tester); + final canvas = TestRecordingCanvas(); + painter.mIndicatorPaneList.single.drawSeries( + canvas, + painter.candles!, + start: 0, + stop: painter.candles!.length - 1, + xOf: (i) => i * 10.0, + scaleX: scaleX, + ); + final widths = [ + for (final call in canvas.invocations) + if (call.invocation.memberName == #drawRect) + (call.invocation.positionalArguments.first as Rect).width, + ]; + // everyElement passes on an empty list, which would hide a pane drawing none. + expect(widths, isNotEmpty); + return widths; +} + +/// Bars drawn over the candles, one per candle, from zero. +class _OverlayBars extends Indicator { + @override + String get name => 'OVERLAYBARS'; + + @override + String get label => name; + + @override + IndicatorPlacement get placement => IndicatorPlacement.overlay; + + @override + List get lines => const [ + IndicatorLine('V', shape: IndicatorShape.histogram), + ]; + + @override + List get settings => [name]; + + @override + IndicatorSeries compute(List candles) => IndicatorSeries([ + [for (final c in candles) c.close], + ]); + + @override + Color defaultColor(int line, ChartColors theme, int ordinal) => + theme.ma5Color; +} + +Future _pinchOut(WidgetTester tester) async { + final centre = tester.getCenter(find.byType(KChartWidget)); + final left = await tester.startGesture(centre - const Offset(40, 0)); + final right = await tester.startGesture(centre + const Offset(40, 0)); + await left.moveBy(const Offset(-90, 0)); + await right.moveBy(const Offset(90, 0)); + await tester.pump(); + await left.up(); + await right.up(); + await tester.pumpAndSettle(); +} + void main() { + group('indicator pane bars', () { + testWidgets('over the candles, widen with the zoom too', (tester) async { + await tester.pumpWidget( + _chart(fitContent: false, count: 200, indicators: [_OverlayBars()]), + ); + await _pinchOut(tester); + final painter = _painterOf(tester); + expect(painter.scaleX, greaterThan(1.2)); + + final canvas = TestRecordingCanvas(); + painter.paint(canvas, tester.getSize(find.byType(KChartWidget))); + + final widths = [ + for (final call in canvas.invocations) + if (call.invocation.memberName == #drawRect) + (call.invocation.positionalArguments.first as Rect).width, + ]; + expect( + widths, + contains( + closeTo(const ChartStyle().candleWidth * painter.scaleX, 0.001), + ), + ); + }); + + testWidgets('the chart hands its zoom to the panes', (tester) async { + await tester.pumpWidget( + _chart(fitContent: false, count: 200, indicators: [MacdIndicator()]), + ); + await _pinchOut(tester); + final painter = _painterOf(tester); + expect(painter.scaleX, greaterThan(1.2)); + + final canvas = TestRecordingCanvas(); + painter.paint(canvas, tester.getSize(find.byType(KChartWidget))); + + final widths = [ + for (final call in canvas.invocations) + if (call.invocation.memberName == #drawRect) + (call.invocation.positionalArguments.first as Rect).width, + ]; + expect( + widths, + contains(closeTo(const ChartStyle().macdWidth * painter.scaleX, 0.001)), + ); + }); + + testWidgets('widen with the zoom, as the candles do', (tester) async { + await tester.pumpWidget( + _chart(fitContent: false, indicators: [MacdIndicator()]), + ); + final width = const ChartStyle().macdWidth; + + expect(_paneBarWidths(tester, scaleX: 1), everyElement(width)); + expect(_paneBarWidths(tester, scaleX: 3), everyElement(width * 3)); + }); + + testWidgets('never shrink past a hairline when zoomed out', (tester) async { + await tester.pumpWidget( + _chart(fitContent: false, indicators: [MacdIndicator()]), + ); + + expect(_paneBarWidths(tester, scaleX: 0.1), everyElement(1.0)); + }); + }); + group('ChartStyle.fitContent', () { testWidgets('off, a short series keeps the fixed spacing', (tester) async { await tester.pumpWidget(_chart(fitContent: false)); From 404f60afda0929a89e528c5a1f3c49d1270f1b9f Mon Sep 17 00:00:00 2001 From: Mohammad Zarif Date: Wed, 7 Oct 2026 14:12:20 +0330 Subject: [PATCH 3/4] docs: note the histogram bar fix in the changelog --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50858f6..e703074 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## Unreleased + +### Fixed + +- Histogram bars in an indicator pane now widen with the candles. They stayed at + `ChartStyle.macdWidth` while the candles grew, so MACD bars — and any custom + histogram indicator — were hairline-thin beside candles spread by + `ChartStyle.fitContent`, and beside candles zoomed in. They follow both now, + and a histogram drawn over the candles follows the zoom too. A bar is never + thinner than a pixel, so zooming far out does not make them vanish. + ## 3.1.0 - 2026-10-06 ### Added From 44c824a5641f88a9cc8d80fc9bfb4e2a99849b19 Mon Sep 17 00:00:00 2001 From: Mohammad Zarif Date: Mon, 12 Oct 2026 13:39:49 +0330 Subject: [PATCH 4/4] chore: release 3.1.1 Bump the version, date the changelog, and point the install snippet at 3.1.1. --- CHANGELOG.md | 2 +- README.md | 2 +- pubspec.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e703074..de860f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## Unreleased +## 3.1.1 - 2026-10-12 ### Fixed diff --git a/README.md b/README.md index e18c77e..e599b66 100644 --- a/README.md +++ b/README.md @@ -387,7 +387,7 @@ indicator and drawing tool is included; there is no paid tier. ```yaml dependencies: material_ui: ">=1.0.0 <2.0.0" - ohlcv_chart: ">=3.1.0 <4.0.0" + ohlcv_chart: ">=3.1.1 <4.0.0" ``` Every widget here is built on the `material_ui` package rather than the diff --git a/pubspec.yaml b/pubspec.yaml index 3a4b785..47ba5d9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: >- Candlestick charts for Flutter with 31 indicators, 29 drawing tools and 39 more chart widgets: depth, order flow, series, pie, treemap, sankey. Pure CustomPainter, no WebView. -version: 3.1.0 +version: 3.1.1 homepage: https://github.com/CtrlAltDevelop repository: https://github.com/CtrlAltDevelop/ohlcv_chart issue_tracker: https://github.com/CtrlAltDevelop/ohlcv_chart/issues