From c94f064af8e39253247a5a37efe0208bf4c65390 Mon Sep 17 00:00:00 2001 From: Razvan Turturica Date: Thu, 11 Jun 2026 15:56:35 +0300 Subject: [PATCH 1/4] Android scale and oval bs mark fix --- .../klinechart/BaseKLineChartView.java | 17 +++++++++++++---- .../klinechart/ScrollAndScaleView.java | 2 +- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/android/src/main/java/com/github/fujianlian/klinechart/BaseKLineChartView.java b/android/src/main/java/com/github/fujianlian/klinechart/BaseKLineChartView.java index 1cee40c..42340b3 100755 --- a/android/src/main/java/com/github/fujianlian/klinechart/BaseKLineChartView.java +++ b/android/src/main/java/com/github/fujianlian/klinechart/BaseKLineChartView.java @@ -1128,20 +1128,27 @@ private void drawBuySellMark(java.util.Map markData, KLineEntity circleColor = configManager.decreaseColor; // Use same color as decreasing candlesticks } + // The canvas is horizontally scaled by mScaleX (see drawK). Counter that scale + // around the mark's center so it stays a perfect circle at any zoom level + // instead of stretching into an ellipse. + canvas.save(); + canvas.translate(candleX, markCenterY); + canvas.scale(1f / mScaleX, 1f); + // Create paint for circle Paint circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG); circlePaint.setColor(circleColor); circlePaint.setStyle(Paint.Style.FILL); // Draw circle - canvas.drawCircle(candleX, markCenterY, circleRadius, circlePaint); + canvas.drawCircle(0, 0, circleRadius, circlePaint); // Draw border Paint borderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); borderPaint.setColor(circleColor); borderPaint.setStyle(Paint.Style.STROKE); borderPaint.setStrokeWidth(2.0f); - canvas.drawCircle(candleX, markCenterY, circleRadius, borderPaint); + canvas.drawCircle(0, 0, circleRadius, borderPaint); // Draw text inside circle String markText = "buy".equals(type) ? "B" : "S"; @@ -1153,9 +1160,11 @@ private void drawBuySellMark(java.util.Map markData, KLineEntity // Calculate text position (center of circle) Paint.FontMetrics fontMetrics = textPaint.getFontMetrics(); - float textY = markCenterY - (fontMetrics.ascent + fontMetrics.descent) / 2; + float textY = -(fontMetrics.ascent + fontMetrics.descent) / 2; - canvas.drawText(markText, candleX, textY, textPaint); + canvas.drawText(markText, 0, textY, textPaint); + + canvas.restore(); } public int dp2px(float dp) { diff --git a/android/src/main/java/com/github/fujianlian/klinechart/ScrollAndScaleView.java b/android/src/main/java/com/github/fujianlian/klinechart/ScrollAndScaleView.java index 8ca9f34..6b439cb 100755 --- a/android/src/main/java/com/github/fujianlian/klinechart/ScrollAndScaleView.java +++ b/android/src/main/java/com/github/fujianlian/klinechart/ScrollAndScaleView.java @@ -34,7 +34,7 @@ public abstract class ScrollAndScaleView extends RelativeLayout implements protected float mScaleX = 1; - protected float mScaleXMax = 2f; + protected float mScaleXMax = 2.5f; protected float mScaleXMin = 0.5f; From 0c8720e7202c288848ae94936668c09c09ff19a9 Mon Sep 17 00:00:00 2001 From: Razvan Turturica Date: Fri, 12 Jun 2026 14:47:36 +0300 Subject: [PATCH 2/4] Bigger buy sell marks --- .../fujianlian/klinechart/BaseKLineChartView.java | 9 +++++++-- ios/Classes/HTKLineView.swift | 10 ++++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/android/src/main/java/com/github/fujianlian/klinechart/BaseKLineChartView.java b/android/src/main/java/com/github/fujianlian/klinechart/BaseKLineChartView.java index 42340b3..2cf6551 100755 --- a/android/src/main/java/com/github/fujianlian/klinechart/BaseKLineChartView.java +++ b/android/src/main/java/com/github/fujianlian/klinechart/BaseKLineChartView.java @@ -1100,8 +1100,13 @@ private void drawBuySellMark(java.util.Map markData, KLineEntity float candleHigh = candlestick.getHighPrice(); float highY = yFromValue(candleHigh); // Same method used by MainDraw.drawCandle - // Circle properties - diameter should match candlestick width - float circleRadius = mPointWidth * 0.4f; // Use 80% of candlestick width for diameter + // Circle properties - diameter should match candlestick width, but never smaller + // than a dynamic floor of 3 * the candle width when fully zoomed out. The on-screen + // candle width is mPointWidth * mScaleX, so at the most zoomed-out scale (mScaleXMin) + // it is mPointWidth * mScaleXMin; the floor stays constant regardless of zoom. + float candleWidthAtMaxZoomOut = mPointWidth * mScaleXMin; + float minCircleRadius = candleWidthAtMaxZoomOut * 3f / 2f; // diameter = 3 * candle width + float circleRadius = Math.max(mPointWidth * 0.4f, minCircleRadius); // Position both marks above the candlestick, with collision avoidance float markCenterY; diff --git a/ios/Classes/HTKLineView.swift b/ios/Classes/HTKLineView.swift index ab66ac1..fb92c68 100644 --- a/ios/Classes/HTKLineView.swift +++ b/ios/Classes/HTKLineView.swift @@ -976,8 +976,14 @@ class HTKLineView: UIScrollView { let normalizedPrice = (markPrice - mainMinMaxRange.lowerBound) / priceRange let markY = mainBaseY + mainHeight - CGFloat(normalizedPrice) * mainHeight - // Make circle radius match candlestick width (same as Android logic) - let circleRadius: CGFloat = configManager.itemWidth * 0.4 // 80% of candlestick width for diameter + // Make circle radius match candlestick width (same as Android logic), but never + // smaller than a dynamic floor of 3 * the candle width when fully zoomed out. + // `_itemWidth` is the unscaled candle width and `minScale` (0.3, see pinchSelector) + // is the most zoomed-out scale, so the floor stays constant regardless of zoom. + let minScale: CGFloat = 0.3 + let candleWidthAtMaxZoomOut = configManager._itemWidth * minScale + let minCircleRadius = candleWidthAtMaxZoomOut * 3 / 2 // diameter = 3 * candle width + let circleRadius: CGFloat = max(configManager.itemWidth * 0.4, minCircleRadius) // Position both marks above the candlestick, with collision avoidance let markCenterY: CGFloat From e757623c5068ec163a38aeb4f6199a7959312b94 Mon Sep 17 00:00:00 2001 From: Razvan Turturica Date: Fri, 12 Jun 2026 15:30:12 +0300 Subject: [PATCH 3/4] Make config --- .../github/fujianlian/klinechart/BaseKLineChartView.java | 9 +++++---- .../fujianlian/klinechart/HTKLineConfigManager.java | 9 +++++++++ example/utils/businessLogic.js | 2 ++ ios/Classes/HTKLineConfigManager.swift | 5 +++++ ios/Classes/HTKLineView.swift | 9 +++++---- 5 files changed, 26 insertions(+), 8 deletions(-) diff --git a/android/src/main/java/com/github/fujianlian/klinechart/BaseKLineChartView.java b/android/src/main/java/com/github/fujianlian/klinechart/BaseKLineChartView.java index 2cf6551..52c8237 100755 --- a/android/src/main/java/com/github/fujianlian/klinechart/BaseKLineChartView.java +++ b/android/src/main/java/com/github/fujianlian/klinechart/BaseKLineChartView.java @@ -1101,11 +1101,12 @@ private void drawBuySellMark(java.util.Map markData, KLineEntity float highY = yFromValue(candleHigh); // Same method used by MainDraw.drawCandle // Circle properties - diameter should match candlestick width, but never smaller - // than a dynamic floor of 3 * the candle width when fully zoomed out. The on-screen - // candle width is mPointWidth * mScaleX, so at the most zoomed-out scale (mScaleXMin) - // it is mPointWidth * mScaleXMin; the floor stays constant regardless of zoom. + // than a dynamic floor of buySellMarkMinWidthMultiplier * the candle width when fully + // zoomed out. The on-screen candle width is mPointWidth * mScaleX, so at the most + // zoomed-out scale (mScaleXMin) it is mPointWidth * mScaleXMin; the floor stays + // constant regardless of zoom. float candleWidthAtMaxZoomOut = mPointWidth * mScaleXMin; - float minCircleRadius = candleWidthAtMaxZoomOut * 3f / 2f; // diameter = 3 * candle width + float minCircleRadius = candleWidthAtMaxZoomOut * configManager.buySellMarkMinWidthMultiplier / 2f; float circleRadius = Math.max(mPointWidth * 0.4f, minCircleRadius); // Position both marks above the candlestick, with collision avoidance diff --git a/android/src/main/java/com/github/fujianlian/klinechart/HTKLineConfigManager.java b/android/src/main/java/com/github/fujianlian/klinechart/HTKLineConfigManager.java index 4574834..e7dfbc8 100644 --- a/android/src/main/java/com/github/fujianlian/klinechart/HTKLineConfigManager.java +++ b/android/src/main/java/com/github/fujianlian/klinechart/HTKLineConfigManager.java @@ -105,6 +105,10 @@ public class HTKLineConfigManager { public float minVisibleCandles = 5; + // Minimum buy/sell mark diameter expressed as a multiple of the candle width when + // fully zoomed out. The mark never shrinks below this floor regardless of zoom. + public float buySellMarkMinWidthMultiplier = 3; + public int minuteVolumeCandleColor = Color.RED; public float minuteVolumeCandleWidth = 1.5f; @@ -462,6 +466,11 @@ public void reloadOptionList(Map optionList) { this.minVisibleCandles = minVisibleCandlesValue.floatValue(); } + Number buySellMarkMinWidthMultiplierValue = (Number)configList.get("buySellMarkMinWidthMultiplier"); + if (buySellMarkMinWidthMultiplierValue != null) { + this.buySellMarkMinWidthMultiplier = buySellMarkMinWidthMultiplierValue.floatValue(); + } + this.fontFamily = (configList.get("fontFamily")).toString(); this.textColor = ((Number) configList.get("textColor")).intValue(); this.headerTextFontSize = ((Number)configList.get("headerTextFontSize")).floatValue(); diff --git a/example/utils/businessLogic.js b/example/utils/businessLogic.js index acbb500..75aa6a7 100644 --- a/example/utils/businessLogic.js +++ b/example/utils/businessLogic.js @@ -357,6 +357,8 @@ export const packOptionList = (modelArray, appState, shouldScrollToEnd = true, u candleWidth: 6 * pixelRatio, candleCornerRadius: candleCornerRadius * pixelRatio, minVisibleCandles: minVisibleCandles || 5, + buySellMarkMinWidthMultiplier: 3, // Min mark diameter = N * candle width when fully zoomed out + minuteVolumeCandleColor: processColor(showVolumeChart ? COLOR(0.0941176, 0.509804, 0.831373, 0.501961) : 'transparent'), minuteVolumeCandleWidth: showVolumeChart ? 2 * pixelRatio : 0, macdCandleWidth: 1 * pixelRatio, diff --git a/ios/Classes/HTKLineConfigManager.swift b/ios/Classes/HTKLineConfigManager.swift index 26360b9..4fdd200 100644 --- a/ios/Classes/HTKLineConfigManager.swift +++ b/ios/Classes/HTKLineConfigManager.swift @@ -136,6 +136,10 @@ class HTKLineConfigManager: NSObject { var minVisibleCandles: CGFloat = 5 + // Minimum buy/sell mark diameter expressed as a multiple of the candle width when + // fully zoomed out. The mark never shrinks below this floor regardless of zoom. + var buySellMarkMinWidthMultiplier: CGFloat = 3 + var minuteVolumeCandleWidth: CGFloat = 0 var _minuteVolumeCandleWidth: CGFloat = 0 @@ -451,6 +455,7 @@ class HTKLineConfigManager: NSObject { _macdCandleWidth = configList["macdCandleWidth"] as? CGFloat ?? 0 candleCornerRadius = configList["candleCornerRadius"] as? CGFloat ?? 0 minVisibleCandles = configList["minVisibleCandles"] as? CGFloat ?? 5 + buySellMarkMinWidthMultiplier = configList["buySellMarkMinWidthMultiplier"] as? CGFloat ?? 3 reloadScrollViewScale(1) paddingTop = configList["paddingTop"] as? CGFloat ?? 0 paddingRight = configList["paddingRight"] as? CGFloat ?? 0 diff --git a/ios/Classes/HTKLineView.swift b/ios/Classes/HTKLineView.swift index fb92c68..d20f706 100644 --- a/ios/Classes/HTKLineView.swift +++ b/ios/Classes/HTKLineView.swift @@ -977,12 +977,13 @@ class HTKLineView: UIScrollView { let markY = mainBaseY + mainHeight - CGFloat(normalizedPrice) * mainHeight // Make circle radius match candlestick width (same as Android logic), but never - // smaller than a dynamic floor of 3 * the candle width when fully zoomed out. - // `_itemWidth` is the unscaled candle width and `minScale` (0.3, see pinchSelector) - // is the most zoomed-out scale, so the floor stays constant regardless of zoom. + // smaller than a dynamic floor of buySellMarkMinWidthMultiplier * the candle width + // when fully zoomed out. `_itemWidth` is the unscaled candle width and `minScale` + // (0.3, see pinchSelector) is the most zoomed-out scale, so the floor stays constant + // regardless of zoom. let minScale: CGFloat = 0.3 let candleWidthAtMaxZoomOut = configManager._itemWidth * minScale - let minCircleRadius = candleWidthAtMaxZoomOut * 3 / 2 // diameter = 3 * candle width + let minCircleRadius = candleWidthAtMaxZoomOut * configManager.buySellMarkMinWidthMultiplier / 2 let circleRadius: CGFloat = max(configManager.itemWidth * 0.4, minCircleRadius) // Position both marks above the candlestick, with collision avoidance From 8bd36eae8b4698449afb65f8cd9b0a02de888c44 Mon Sep 17 00:00:00 2001 From: Razvan Turturica Date: Fri, 12 Jun 2026 16:20:32 +0300 Subject: [PATCH 4/4] Bigger buy/sell marks on android --- .../com/github/fujianlian/klinechart/BaseKLineChartView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/src/main/java/com/github/fujianlian/klinechart/BaseKLineChartView.java b/android/src/main/java/com/github/fujianlian/klinechart/BaseKLineChartView.java index 52c8237..4f4eeaf 100755 --- a/android/src/main/java/com/github/fujianlian/klinechart/BaseKLineChartView.java +++ b/android/src/main/java/com/github/fujianlian/klinechart/BaseKLineChartView.java @@ -1107,7 +1107,7 @@ private void drawBuySellMark(java.util.Map markData, KLineEntity // constant regardless of zoom. float candleWidthAtMaxZoomOut = mPointWidth * mScaleXMin; float minCircleRadius = candleWidthAtMaxZoomOut * configManager.buySellMarkMinWidthMultiplier / 2f; - float circleRadius = Math.max(mPointWidth * 0.4f, minCircleRadius); + float circleRadius = Math.max(mPointWidth * 0.5f, minCircleRadius); // Position both marks above the candlestick, with collision avoidance float markCenterY;