Skip to content

Commit 5445bb5

Browse files
Merge pull request #14 from CtrlAltDevelop/develop
release: 3.1.1
2 parents b0d328d + 44c824a commit 5445bb5

10 files changed

Lines changed: 197 additions & 10 deletions

‎CHANGELOG.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
## 3.1.1 - 2026-10-12
2+
3+
### Fixed
4+
5+
- Histogram bars in an indicator pane now widen with the candles. They stayed at
6+
`ChartStyle.macdWidth` while the candles grew, so MACD bars — and any custom
7+
histogram indicator — were hairline-thin beside candles spread by
8+
`ChartStyle.fitContent`, and beside candles zoomed in. They follow both now,
9+
and a histogram drawn over the candles follows the zoom too. A bar is never
10+
thinner than a pixel, so zooming far out does not make them vanish.
11+
112
## 3.1.0 - 2026-10-06
213

314
### Added

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ indicator and drawing tool is included; there is no paid tier.
387387
```yaml
388388
dependencies:
389389
material_ui: ">=1.0.0 <2.0.0"
390-
ohlcv_chart: ">=3.1.0 <4.0.0"
390+
ohlcv_chart: ">=3.1.1 <4.0.0"
391391
```
392392
393393
Every widget here is built on the `material_ui` package rather than the

‎lib/src/chart_style.dart‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -523,8 +523,9 @@ class ChartStyle {
523523
/// [pointWidth] is a fixed distance, so a short series — a handful of
524524
/// intraday bars, say — bunches up against the left edge and leaves the rest
525525
/// of the chart empty. With this set the spacing is widened to whatever makes
526-
/// the series span the plot, and [candleWidth] and [ChartStyle.volWidth] are
527-
/// widened with it so the bars keep their proportions.
526+
/// the series span the plot, and [candleWidth], [ChartStyle.volWidth] and
527+
/// [ChartStyle.macdWidth] are widened with it so the bars keep their
528+
/// proportions.
528529
///
529530
/// Only ever widens: a series long enough to fill the plot, or one zoomed in
530531
/// past it, is laid out on [pointWidth] as before.
@@ -534,7 +535,9 @@ class ChartStyle {
534535
///vol column width
535536
final double volWidth;
536537

537-
///macd column width
538+
/// Width of the histogram bars in an indicator pane, MACD or custom.
539+
/// The bars follow the candles: they widen when [fitContent] spreads them and
540+
/// when the chart is zoomed in, and are never thinner than a pixel.
538541
final double macdWidth;
539542

540543
/// Stroke width of an indicator's lines, in a pane or over the candles.

‎lib/src/renderer/base_chart_painter.dart‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,7 @@ abstract class BaseChartPainter extends CustomPainter {
477477
pointWidth: fitted,
478478
candleWidth: chartStyle.candleWidth * spread,
479479
volWidth: chartStyle.volWidth * spread,
480+
macdWidth: chartStyle.macdWidth * spread,
480481
);
481482
}
482483
mDataLen = mItemCount * mPointWidth;

‎lib/src/renderer/chart_painter.dart‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ class ChartPainter extends BaseChartPainter {
423423
mSecondaryRectList[i].mMinValue,
424424
mChildPadding,
425425
fixedLength,
426-
chartStyle,
426+
fittedStyle,
427427
chartColors,
428428
panes[i],
429429
percentBase: _paneBase(panes[i]),
@@ -662,6 +662,7 @@ class ChartPainter extends BaseChartPainter {
662662
start: mStartIndex,
663663
stop: mStopIndex,
664664
xOf: xOf,
665+
scaleX: scaleX,
665666
);
666667
}
667668
}

‎lib/src/renderer/indicator_pane_renderer.dart‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ class IndicatorPaneRenderer extends BaseChartRenderer<KLineEntity> {
136136
required int start,
137137
required int stop,
138138
required double Function(int index) xOf,
139+
double scaleX = 1.0,
139140
}) {
140141
canvas.save();
141142
canvas.clipRect(
@@ -156,7 +157,8 @@ class IndicatorPaneRenderer extends BaseChartRenderer<KLineEntity> {
156157
yOf: getY,
157158
colors: chartColors,
158159
strokeWidth: chartStyle.indicatorLineWidth,
159-
barWidth: chartStyle.macdWidth,
160+
// View space, so the bar takes the zoom the candles take from the canvas.
161+
barWidth: chartStyle.macdWidth * scaleX,
160162
zeroY: getY(0).clamp(chartRect.top, chartRect.bottom),
161163
);
162164
canvas.restore();

‎lib/src/renderer/main_renderer.dart‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,9 @@ class MainRenderer extends BaseChartRenderer<CandleEntity> {
449449
yOf: getY,
450450
colors: chartColors,
451451
strokeWidth: chartStyle.indicatorLineWidth,
452-
barWidth: chartStyle.candleWidth,
452+
// View space, so the bar takes the zoom the candles take from the
453+
// canvas.
454+
barWidth: chartStyle.candleWidth * scaleX,
453455
);
454456
}
455457
canvas.restore();

‎lib/src/renderer/series_painter.dart‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'dart:math' as math;
2+
13
import 'package:material_ui/material_ui.dart';
24

35
import '../chart_style.dart';
@@ -227,7 +229,9 @@ void _paintHistogram(
227229
required double zeroY,
228230
}) {
229231
final paint = Paint()..isAntiAlias = true;
230-
final half = barWidth / 2;
232+
// Zoomed far out the candles shrink with the bar; a bar thinner than a hairline
233+
// would vanish.
234+
final half = math.max(barWidth, 1.0) / 2;
231235

232236
for (var i = start; i <= stop; i++) {
233237
final value = resolved.valueAt(line, i);

‎pubspec.yaml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ description: >-
33
Candlestick charts for Flutter with 31 indicators, 29 drawing tools and 39
44
more chart widgets: depth, order flow, series, pie, treemap, sankey. Pure
55
CustomPainter, no WebView.
6-
version: 3.1.0
6+
version: 3.1.1
77
homepage: https://github.com/CtrlAltDevelop
88
repository: https://github.com/CtrlAltDevelop/ohlcv_chart
99
issue_tracker: https://github.com/CtrlAltDevelop/ohlcv_chart/issues

‎test/fit_content_test.dart‎

Lines changed: 164 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ ChartPainter _painterOf(WidgetTester tester) {
1212
}
1313

1414
/// A chart too short to fill its box at the default spacing.
15-
Widget _chart({required bool fitContent, int count = 10}) {
15+
Widget _chart({
16+
required bool fitContent,
17+
int count = 10,
18+
List<Indicator> indicators = const [],
19+
}) {
1620
final data = candles(rampThenFall(count));
1721
DataUtil.calculate(data);
1822

@@ -30,13 +34,143 @@ Widget _chart({required bool fitContent, int count = 10}) {
3034
volHidden: true,
3135
showNowPrice: false,
3236
chartStyle: ChartStyle(fitContent: fitContent),
37+
indicators: indicators,
3338
),
3439
),
3540
),
3641
);
3742
}
3843

44+
/// The widths of the rects an indicator pane draws across its candles.
45+
List<double> _paneBarWidths(WidgetTester tester, {required double scaleX}) {
46+
final painter = _painterOf(tester);
47+
final canvas = TestRecordingCanvas();
48+
painter.mIndicatorPaneList.single.drawSeries(
49+
canvas,
50+
painter.candles!,
51+
start: 0,
52+
stop: painter.candles!.length - 1,
53+
xOf: (i) => i * 10.0,
54+
scaleX: scaleX,
55+
);
56+
final widths = [
57+
for (final call in canvas.invocations)
58+
if (call.invocation.memberName == #drawRect)
59+
(call.invocation.positionalArguments.first as Rect).width,
60+
];
61+
// everyElement passes on an empty list, which would hide a pane drawing none.
62+
expect(widths, isNotEmpty);
63+
return widths;
64+
}
65+
66+
/// Bars drawn over the candles, one per candle, from zero.
67+
class _OverlayBars extends Indicator {
68+
@override
69+
String get name => 'OVERLAYBARS';
70+
71+
@override
72+
String get label => name;
73+
74+
@override
75+
IndicatorPlacement get placement => IndicatorPlacement.overlay;
76+
77+
@override
78+
List<IndicatorLine> get lines => const [
79+
IndicatorLine('V', shape: IndicatorShape.histogram),
80+
];
81+
82+
@override
83+
List<Object?> get settings => [name];
84+
85+
@override
86+
IndicatorSeries compute(List<KLineEntity> candles) => IndicatorSeries([
87+
[for (final c in candles) c.close],
88+
]);
89+
90+
@override
91+
Color defaultColor(int line, ChartColors theme, int ordinal) =>
92+
theme.ma5Color;
93+
}
94+
95+
Future<void> _pinchOut(WidgetTester tester) async {
96+
final centre = tester.getCenter(find.byType(KChartWidget));
97+
final left = await tester.startGesture(centre - const Offset(40, 0));
98+
final right = await tester.startGesture(centre + const Offset(40, 0));
99+
await left.moveBy(const Offset(-90, 0));
100+
await right.moveBy(const Offset(90, 0));
101+
await tester.pump();
102+
await left.up();
103+
await right.up();
104+
await tester.pumpAndSettle();
105+
}
106+
39107
void main() {
108+
group('indicator pane bars', () {
109+
testWidgets('over the candles, widen with the zoom too', (tester) async {
110+
await tester.pumpWidget(
111+
_chart(fitContent: false, count: 200, indicators: [_OverlayBars()]),
112+
);
113+
await _pinchOut(tester);
114+
final painter = _painterOf(tester);
115+
expect(painter.scaleX, greaterThan(1.2));
116+
117+
final canvas = TestRecordingCanvas();
118+
painter.paint(canvas, tester.getSize(find.byType(KChartWidget)));
119+
120+
final widths = [
121+
for (final call in canvas.invocations)
122+
if (call.invocation.memberName == #drawRect)
123+
(call.invocation.positionalArguments.first as Rect).width,
124+
];
125+
expect(
126+
widths,
127+
contains(
128+
closeTo(const ChartStyle().candleWidth * painter.scaleX, 0.001),
129+
),
130+
);
131+
});
132+
133+
testWidgets('the chart hands its zoom to the panes', (tester) async {
134+
await tester.pumpWidget(
135+
_chart(fitContent: false, count: 200, indicators: [MacdIndicator()]),
136+
);
137+
await _pinchOut(tester);
138+
final painter = _painterOf(tester);
139+
expect(painter.scaleX, greaterThan(1.2));
140+
141+
final canvas = TestRecordingCanvas();
142+
painter.paint(canvas, tester.getSize(find.byType(KChartWidget)));
143+
144+
final widths = [
145+
for (final call in canvas.invocations)
146+
if (call.invocation.memberName == #drawRect)
147+
(call.invocation.positionalArguments.first as Rect).width,
148+
];
149+
expect(
150+
widths,
151+
contains(closeTo(const ChartStyle().macdWidth * painter.scaleX, 0.001)),
152+
);
153+
});
154+
155+
testWidgets('widen with the zoom, as the candles do', (tester) async {
156+
await tester.pumpWidget(
157+
_chart(fitContent: false, indicators: [MacdIndicator()]),
158+
);
159+
final width = const ChartStyle().macdWidth;
160+
161+
expect(_paneBarWidths(tester, scaleX: 1), everyElement(width));
162+
expect(_paneBarWidths(tester, scaleX: 3), everyElement(width * 3));
163+
});
164+
165+
testWidgets('never shrink past a hairline when zoomed out', (tester) async {
166+
await tester.pumpWidget(
167+
_chart(fitContent: false, indicators: [MacdIndicator()]),
168+
);
169+
170+
expect(_paneBarWidths(tester, scaleX: 0.1), everyElement(1.0));
171+
});
172+
});
173+
40174
group('ChartStyle.fitContent', () {
41175
testWidgets('off, a short series keeps the fixed spacing', (tester) async {
42176
await tester.pumpWidget(_chart(fitContent: false));
@@ -67,6 +201,35 @@ void main() {
67201
);
68202
});
69203

204+
testWidgets('on, an indicator pane\'s bars widen with the spacing', (
205+
tester,
206+
) async {
207+
await tester.pumpWidget(
208+
_chart(fitContent: true, indicators: [MacdIndicator()]),
209+
);
210+
final painter = _painterOf(tester);
211+
212+
const style = ChartStyle();
213+
final spread = painter.mPointWidth / style.pointWidth;
214+
expect(
215+
painter.mIndicatorPaneList.single.chartStyle.macdWidth,
216+
closeTo(style.macdWidth * spread, 0.001),
217+
);
218+
});
219+
220+
testWidgets('off, an indicator pane\'s bars keep their width', (
221+
tester,
222+
) async {
223+
await tester.pumpWidget(
224+
_chart(fitContent: false, indicators: [MacdIndicator()]),
225+
);
226+
227+
expect(
228+
_painterOf(tester).mIndicatorPaneList.single.chartStyle.macdWidth,
229+
const ChartStyle().macdWidth,
230+
);
231+
});
232+
70233
testWidgets('on, a series that already fills the plot is left alone', (
71234
tester,
72235
) async {

0 commit comments

Comments
 (0)