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..4f4eeaf 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,14 @@ 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 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 * configManager.buySellMarkMinWidthMultiplier / 2f; + float circleRadius = Math.max(mPointWidth * 0.5f, minCircleRadius); // Position both marks above the candlestick, with collision avoidance float markCenterY; @@ -1128,20 +1134,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 +1166,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/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/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; 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 ab66ac1..d20f706 100644 --- a/ios/Classes/HTKLineView.swift +++ b/ios/Classes/HTKLineView.swift @@ -976,8 +976,15 @@ 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 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 * configManager.buySellMarkMinWidthMultiplier / 2 + let circleRadius: CGFloat = max(configManager.itemWidth * 0.4, minCircleRadius) // Position both marks above the candlestick, with collision avoidance let markCenterY: CGFloat