From 83d3c38894fb6f65ff7e344e08fee92ce1c7ea5a Mon Sep 17 00:00:00 2001 From: Razvan Turturica Date: Mon, 26 Jan 2026 11:48:03 +0200 Subject: [PATCH 1/4] Partial commit --- .../klinechart/BaseKLineChartView.java | 85 ++++++ .../fujianlian/klinechart/RNKLineView.java | 67 ++++- .../container/HTKLineContainerView.java | 176 ++++++++++++ example/App.js | 69 +++++ example/components/BuySellMarkInput.js | 259 ++++++++++++++++++ index.js | 81 ++++++ ios/Classes/HTKLineContainerView.swift | 121 ++++++++ ios/Classes/HTKLineView.swift | 83 ++++++ ios/Classes/RNKLineView.m | 11 + ios/Classes/RNKLineView.swift | 46 ++++ 10 files changed, 997 insertions(+), 1 deletion(-) create mode 100644 example/components/BuySellMarkInput.js 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 5344f7e..1de99df 100755 --- a/android/src/main/java/com/github/fujianlian/klinechart/BaseKLineChartView.java +++ b/android/src/main/java/com/github/fujianlian/klinechart/BaseKLineChartView.java @@ -557,6 +557,9 @@ private void drawK(Canvas canvas) { if (mChildDraw != null) { mChildDraw.drawTranslated(lastPoint, currentPoint, lastX, currentPointX, canvas, this, i); } + + // Draw buy/sell marks for this candlestick if they exist (O(1) lookup) + drawBuySellMarksForCandlestick((KLineEntity) currentPoint, i, canvas); } //还原 平移缩放 @@ -1062,6 +1065,88 @@ private void drawOrderLines(Canvas canvas) { } } + private void drawBuySellMarksForCandlestick(KLineEntity candlestick, int index, Canvas canvas) { + // Get parent container view to access buy/sell marks + if (getParent() instanceof HTKLineContainerView) { + HTKLineContainerView containerView = (HTKLineContainerView) getParent(); + + // Get candlestick time as timestamp + long candleTime; + try { + candleTime = Long.parseLong(candlestick.Date); + } catch (NumberFormatException e) { + return; // Skip if date is not a valid timestamp + } + + // Check for buy mark (O(1) lookup) + java.util.Map buyMark = containerView.getBuyMarkForTime(candleTime); + if (buyMark != null) { + drawBuySellMark(buyMark, candlestick, index, "buy", canvas); + } + + // Check for sell mark (O(1) lookup) + java.util.Map sellMark = containerView.getSellMarkForTime(candleTime); + if (sellMark != null) { + drawBuySellMark(sellMark, candlestick, index, "sell", canvas); + } + } + } + + private void drawBuySellMark(java.util.Map markData, KLineEntity candlestick, int index, String type, Canvas canvas) { + // Calculate X position for the candlestick + float candleX = getItemMiddleScrollX(index); + float candleViewX = scrollXtoViewX(candleX); + + // Position mark above the candlestick high + float markY = yFromValue(candlestick.getHighPrice()); + + // Circle properties + float circleRadius = dp2px(10); + float markCenterY = markY - circleRadius - dp2px(5); // 5dp above the high + + // Only draw if visible + if (candleViewX >= 0 && candleViewX <= getWidth() && + markCenterY >= 0 && markCenterY <= getHeight()) { + + // Determine colors based on type, using same colors as candlesticks + int circleColor; + if ("buy".equals(type)) { + circleColor = configManager.increaseColor; // Use same color as increasing candlesticks + } else { // sell + circleColor = configManager.decreaseColor; // Use same color as decreasing candlesticks + } + + // Create paint for circle + Paint circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + circlePaint.setColor(circleColor); + circlePaint.setStyle(Paint.Style.FILL); + + // Draw circle + canvas.drawCircle(candleViewX, markCenterY, 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(candleViewX, markCenterY, circleRadius, borderPaint); + + // Draw text inside circle + String markText = "buy".equals(type) ? "B" : "S"; + Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + textPaint.setColor(android.graphics.Color.WHITE); + textPaint.setTextSize(sp2px(12)); + textPaint.setTypeface(configManager.font); + textPaint.setTextAlign(Paint.Align.CENTER); + + // Calculate text position (center of circle) + Paint.FontMetrics fontMetrics = textPaint.getFontMetrics(); + float textY = markCenterY - (fontMetrics.ascent + fontMetrics.descent) / 2; + + canvas.drawText(markText, candleViewX, textY, textPaint); + } + } + public int dp2px(float dp) { final float scale = getContext().getResources().getDisplayMetrics().density; return (int) (dp * scale + 0.5f); diff --git a/android/src/main/java/com/github/fujianlian/klinechart/RNKLineView.java b/android/src/main/java/com/github/fujianlian/klinechart/RNKLineView.java index 4b03e37..9c84dd0 100644 --- a/android/src/main/java/com/github/fujianlian/klinechart/RNKLineView.java +++ b/android/src/main/java/com/github/fujianlian/klinechart/RNKLineView.java @@ -86,7 +86,7 @@ public void run() { @Override public Map getCommandsMap() { - return MapBuilder.of( + Map commands = MapBuilder.of( "updateLastCandlestick", 1, "addCandlesticksAtTheEnd", 2, "addCandlesticksAtTheStart", 3, @@ -95,6 +95,14 @@ public Map getCommandsMap() { "updateOrderLine", 6, "getOrderLines", 7 ); + + // Add the new buy/sell mark commands + commands.put("addBuySellMark", 8); + commands.put("removeBuySellMark", 9); + commands.put("updateBuySellMark", 10); + commands.put("getBuySellMarks", 11); + + return commands; } @Override @@ -204,6 +212,63 @@ public void receiveCommand(@Nonnull HTKLineContainerView containerView, String c e.printStackTrace(); } break; + case "addBuySellMark": + android.util.Log.d("RNKLineView", "Processing addBuySellMark command"); + if (args != null && args.size() > 0) { + try { + ReadableMap buySellMarkData = args.getMap(0); + Map dataMap = buySellMarkData.toHashMap(); + android.util.Log.d("RNKLineView", "Calling containerView.addBuySellMark with data: " + dataMap); + containerView.addBuySellMark(dataMap); + } catch (Exception e) { + android.util.Log.e("RNKLineView", "Error in addBuySellMark command", e); + e.printStackTrace(); + } + } else { + android.util.Log.w("RNKLineView", "addBuySellMark: args is null or empty"); + } + break; + case "removeBuySellMark": + android.util.Log.d("RNKLineView", "Processing removeBuySellMark command"); + if (args != null && args.size() > 0) { + try { + String buySellMarkId = args.getString(0); + android.util.Log.d("RNKLineView", "Calling containerView.removeBuySellMark with id: " + buySellMarkId); + containerView.removeBuySellMark(buySellMarkId); + } catch (Exception e) { + android.util.Log.e("RNKLineView", "Error in removeBuySellMark command", e); + e.printStackTrace(); + } + } else { + android.util.Log.w("RNKLineView", "removeBuySellMark: args is null or empty"); + } + break; + case "updateBuySellMark": + android.util.Log.d("RNKLineView", "Processing updateBuySellMark command"); + if (args != null && args.size() > 0) { + try { + ReadableMap buySellMarkData = args.getMap(0); + Map dataMap = buySellMarkData.toHashMap(); + android.util.Log.d("RNKLineView", "Calling containerView.updateBuySellMark with data: " + dataMap); + containerView.updateBuySellMark(dataMap); + } catch (Exception e) { + android.util.Log.e("RNKLineView", "Error in updateBuySellMark command", e); + e.printStackTrace(); + } + } else { + android.util.Log.w("RNKLineView", "updateBuySellMark: args is null or empty"); + } + break; + case "getBuySellMarks": + android.util.Log.d("RNKLineView", "Processing getBuySellMarks command"); + try { + android.util.Log.d("RNKLineView", "Calling containerView.getBuySellMarks"); + containerView.getBuySellMarks(); + } catch (Exception e) { + android.util.Log.e("RNKLineView", "Error in getBuySellMarks command", e); + e.printStackTrace(); + } + break; default: android.util.Log.w("RNKLineView", "Unknown command: " + commandId); break; diff --git a/android/src/main/java/com/github/fujianlian/klinechart/container/HTKLineContainerView.java b/android/src/main/java/com/github/fujianlian/klinechart/container/HTKLineContainerView.java index 2482a50..a5b50a7 100644 --- a/android/src/main/java/com/github/fujianlian/klinechart/container/HTKLineContainerView.java +++ b/android/src/main/java/com/github/fujianlian/klinechart/container/HTKLineContainerView.java @@ -33,6 +33,11 @@ public class HTKLineContainerView extends RelativeLayout { // Order line management private Map> orderLines = new HashMap<>(); + // Buy/sell mark management - indexed by timestamp for O(1) lookup + private Map> buyMarks = new HashMap<>(); + private Map> sellMarks = new HashMap<>(); + private Map> buySellMarks = new HashMap<>(); // Keep for compatibility + public HTKLineContainerView(ThemedReactContext context) { super(context); this.reactContext = context; @@ -649,4 +654,175 @@ public Map> getAllOrderLines() { } } + public void addBuySellMark(Map buySellMarkData) { + android.util.Log.d("HTKLineContainerView", "addBuySellMark called with data: " + buySellMarkData); + + if (buySellMarkData == null || !buySellMarkData.containsKey("id") || + !buySellMarkData.containsKey("time") || !buySellMarkData.containsKey("type")) { + android.util.Log.w("HTKLineContainerView", "addBuySellMark - Invalid buy/sell mark data"); + return; + } + + String id = (String) buySellMarkData.get("id"); + long time = ((Number) buySellMarkData.get("time")).longValue(); + String type = (String) buySellMarkData.get("type"); + + // Store in compatibility map + synchronized (buySellMarks) { + buySellMarks.put(id, buySellMarkData); + } + + // Store in efficient lookup maps by timestamp + if ("buy".equals(type)) { + synchronized (buyMarks) { + buyMarks.put(time, buySellMarkData); + } + } else if ("sell".equals(type)) { + synchronized (sellMarks) { + sellMarks.put(time, buySellMarkData); + } + } + + android.util.Log.d("HTKLineContainerView", "Added " + type + " mark with id: " + id + " at time: " + time); + + // Trigger redraw to show the buy/sell mark + post(new Runnable() { + @Override + public void run() { + klineView.invalidate(); + } + }); + } + + public void removeBuySellMark(String buySellMarkId) { + android.util.Log.d("HTKLineContainerView", "removeBuySellMark called with id: " + buySellMarkId); + + if (buySellMarkId == null || buySellMarkId.trim().isEmpty()) { + android.util.Log.w("HTKLineContainerView", "removeBuySellMark - Invalid buy/sell mark id"); + return; + } + + // Find and remove from efficient lookup maps + Map markData; + synchronized (buySellMarks) { + markData = buySellMarks.get(buySellMarkId); + if (markData != null) { + buySellMarks.remove(buySellMarkId); + } + } + + if (markData != null && markData.containsKey("time") && markData.containsKey("type")) { + long time = ((Number) markData.get("time")).longValue(); + String type = (String) markData.get("type"); + + if ("buy".equals(type)) { + synchronized (buyMarks) { + buyMarks.remove(time); + } + } else if ("sell".equals(type)) { + synchronized (sellMarks) { + sellMarks.remove(time); + } + } + } + + android.util.Log.d("HTKLineContainerView", "Removed buy/sell mark with id: " + buySellMarkId); + + // Trigger redraw to remove the buy/sell mark + post(new Runnable() { + @Override + public void run() { + klineView.invalidate(); + } + }); + } + + public void updateBuySellMark(Map buySellMarkData) { + android.util.Log.d("HTKLineContainerView", "updateBuySellMark called with data: " + buySellMarkData); + + if (buySellMarkData == null || !buySellMarkData.containsKey("id") || + !buySellMarkData.containsKey("time") || !buySellMarkData.containsKey("type")) { + android.util.Log.w("HTKLineContainerView", "updateBuySellMark - Invalid buy/sell mark data"); + return; + } + + String id = (String) buySellMarkData.get("id"); + long time = ((Number) buySellMarkData.get("time")).longValue(); + String type = (String) buySellMarkData.get("type"); + + // Remove old entry from efficient lookup maps if it exists + Map oldMarkData; + synchronized (buySellMarks) { + oldMarkData = buySellMarks.get(id); + buySellMarks.put(id, buySellMarkData); + } + + if (oldMarkData != null && oldMarkData.containsKey("time") && oldMarkData.containsKey("type")) { + long oldTime = ((Number) oldMarkData.get("time")).longValue(); + String oldType = (String) oldMarkData.get("type"); + + if ("buy".equals(oldType)) { + synchronized (buyMarks) { + buyMarks.remove(oldTime); + } + } else if ("sell".equals(oldType)) { + synchronized (sellMarks) { + sellMarks.remove(oldTime); + } + } + } + + // Add to efficient lookup maps + if ("buy".equals(type)) { + synchronized (buyMarks) { + buyMarks.put(time, buySellMarkData); + } + } else if ("sell".equals(type)) { + synchronized (sellMarks) { + sellMarks.put(time, buySellMarkData); + } + } + + android.util.Log.d("HTKLineContainerView", "Updated " + type + " mark with id: " + id + " at time: " + time); + + // Trigger redraw to update the buy/sell mark + post(new Runnable() { + @Override + public void run() { + klineView.invalidate(); + } + }); + } + + public List> getBuySellMarks() { + android.util.Log.d("HTKLineContainerView", "getBuySellMarks called"); + + // Return all buy/sell marks as a list + synchronized (buySellMarks) { + List> buySellMarksList = new ArrayList<>(buySellMarks.values()); + android.util.Log.d("HTKLineContainerView", "Returning " + buySellMarksList.size() + " buy/sell marks"); + return buySellMarksList; + } + } + + // Method to allow KLineChartView to access buy/sell marks for drawing + public Map> getAllBuySellMarks() { + synchronized (buySellMarks) { + return new HashMap<>(buySellMarks); + } + } + + // Efficient O(1) lookup methods for buy/sell marks by timestamp + public Map getBuyMarkForTime(long time) { + synchronized (buyMarks) { + return buyMarks.get(time); + } + } + + public Map getSellMarkForTime(long time) { + synchronized (sellMarks) { + return sellMarks.get(time); + } + } + } diff --git a/example/App.js b/example/App.js index 0dbc7c6..c515000 100644 --- a/example/App.js +++ b/example/App.js @@ -26,6 +26,7 @@ import { import Toolbar from './components/Toolbar' import ControlBar from './components/ControlBar' import OrderInput from './components/OrderInput' +import BuySellMarkInput from './components/BuySellMarkInput' import Selectors from './components/Selectors' import { processKLineData, @@ -319,6 +320,10 @@ const App = () => { const [orderIdCounter, setOrderIdCounter] = useState(1) const [orderLines, setOrderLines] = useState({}) + // Buy/sell mark management + const [buySellMarkIdCounter, setBuySellMarkIdCounter] = useState(1) + const [buySellMarks, setBuySellMarks] = useState({}) + const handleAddLimitOrder = useCallback((price, label) => { if (!kLineViewRef.current) return @@ -373,6 +378,59 @@ const App = () => { return null }, [klineData]) + // Buy/sell mark handlers + const handleAddBuySellMark = useCallback((type, time, price, amount, orderCount) => { + if (!kLineViewRef.current) return + + const buySellMark = { + id: `buysell-mark-${buySellMarkIdCounter}`, + time: time, + type: type, // 'buy' or 'sell' + amount: amount || '1.0', + price: price || getCurrentPrice()?.toString() || '0', + orderCount: orderCount || 1 + } + + console.log('Adding buy/sell mark:', buySellMark) + kLineViewRef.current.addBuySellMark(buySellMark) + setBuySellMarks(prev => ({ ...prev, [buySellMark.id]: buySellMark })) + setBuySellMarkIdCounter(prev => prev + 1) + }, [kLineViewRef.current, buySellMarkIdCounter, getCurrentPrice]) + + const handleRemoveBuySellMark = useCallback((markId) => { + if (!kLineViewRef.current) return + + console.log('Removing buy/sell mark:', markId) + kLineViewRef.current.removeBuySellMark(markId) + setBuySellMarks(prev => { + const newMarks = { ...prev } + delete newMarks[markId] + return newMarks + }) + }, [kLineViewRef.current]) + + const handleUpdateBuySellMark = useCallback((markId, newType, newPrice, newAmount, newOrderCount) => { + if (!kLineViewRef.current) return + + const existingMark = buySellMarks[markId] + if (!existingMark) { + console.warn(`Buy/sell mark with ID ${markId} not found`) + return + } + + const updatedMark = { + ...existingMark, + type: newType || existingMark.type, + price: newPrice?.toString() || existingMark.price, + amount: newAmount || existingMark.amount, + orderCount: newOrderCount || existingMark.orderCount + } + + console.log('Updating buy/sell mark:', updatedMark) + kLineViewRef.current.updateBuySellMark(updatedMark) + setBuySellMarks(prev => ({ ...prev, [markId]: updatedMark })) + }, [kLineViewRef.current, buySellMarks]) + const renderKLineChart = useCallback((styles) => { @@ -453,6 +511,17 @@ const App = () => { orderLines={orderLines} /> + {/* Buy/Sell mark input */} + + {/* Bottom control bar */} { + const [markType, setMarkType] = useState('buy') + const [priceInput, setPriceInput] = useState('') + const [amountInput, setAmountInput] = useState('1.0') + const [updateMarkId, setUpdateMarkId] = useState('') + const [updateType, setUpdateType] = useState('buy') + const [updatePriceInput, setUpdatePriceInput] = useState('') + + const handleAddMark = () => { + if (!klineData || klineData.length === 0) return + + // Use the last candlestick's time for demonstration + const lastCandlestick = klineData[klineData.length - 10] + const time = lastCandlestick.time + + const price = priceInput ? parseFloat(priceInput) : currentPrice + const amount = amountInput || '1.0' + + if (price > 0) { + onAddBuySellMark(markType, time, price, amount, 1) + setPriceInput('') + setAmountInput('1.0') + } + } + + const handleRemoveMark = () => { + if (updateMarkId.trim()) { + onRemoveBuySellMark(updateMarkId.trim()) + setUpdateMarkId('') + } + } + + const handleUpdateMark = () => { + const price = parseFloat(updatePriceInput) + if (!isNaN(price) && price > 0 && updateMarkId.trim()) { + onUpdateBuySellMark(updateMarkId.trim(), updateType, price) + setUpdateMarkId('') + setUpdatePriceInput('') + } + } + + const styles = StyleSheet.create({ + container: { + padding: 10, + backgroundColor: theme.backgroundColor, + borderTopWidth: 1, + borderTopColor: theme.gridColor, + }, + row: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 8, + }, + label: { + color: theme.textColor, + fontSize: 14, + marginRight: 8, + fontWeight: '600', + }, + input: { + flex: 1, + height: 36, + borderWidth: 1, + borderColor: theme.gridColor, + borderRadius: 4, + paddingHorizontal: 8, + backgroundColor: theme.panelBackgroundColor, + color: theme.textColor, + fontSize: 14, + marginRight: 8, + }, + shortInput: { + width: 80, + height: 36, + borderWidth: 1, + borderColor: theme.gridColor, + borderRadius: 4, + paddingHorizontal: 8, + backgroundColor: theme.panelBackgroundColor, + color: theme.textColor, + fontSize: 14, + marginRight: 8, + }, + typeButton: { + backgroundColor: theme.gridColor, + paddingHorizontal: 12, + paddingVertical: 8, + borderRadius: 4, + marginRight: 8, + }, + typeButtonActive: { + backgroundColor: theme.buttonColor, + }, + typeButtonText: { + color: theme.textColor, + fontSize: 14, + fontWeight: '600', + }, + typeButtonTextActive: { + color: theme.backgroundColor, + }, + addButton: { + backgroundColor: theme.buttonColor, + paddingHorizontal: 12, + paddingVertical: 8, + borderRadius: 4, + }, + addButtonText: { + color: theme.backgroundColor, + fontSize: 14, + fontWeight: '600', + }, + removeButton: { + backgroundColor: theme.decreaseColor, + paddingHorizontal: 12, + paddingVertical: 8, + borderRadius: 4, + }, + removeButtonText: { + color: 'white', + fontSize: 14, + fontWeight: '600', + }, + updateButton: { + backgroundColor: theme.increaseColor, + paddingHorizontal: 12, + paddingVertical: 8, + borderRadius: 4, + }, + updateButtonText: { + color: 'white', + fontSize: 14, + fontWeight: '600', + }, + markInfo: { + color: theme.gridColor, + fontSize: 10, + marginTop: 4, + }, + currentPriceButton: { + backgroundColor: theme.gridColor, + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 4, + marginRight: 8, + }, + currentPriceButtonText: { + color: theme.textColor, + fontSize: 12, + }, + }) + + return ( + + {/* Add Buy/Sell Mark Row */} + + Add Mark: + setMarkType('buy')} + > + + Buy + + + setMarkType('sell')} + > + + Sell + + + + + {currentPrice && ( + setPriceInput(currentPrice.toString())} + > + + Current: {currentPrice.toFixed(2)} + + + )} + + Add + + + + {/* Update/Remove Mark Row */} + + Manage Mark: + setUpdateType('buy')} + > + + Buy + + + setUpdateType('sell')} + > + + Sell + + + + + + Update + + + Remove + + + + {/* Show available mark IDs */} + {buySellMarks && Object.keys(buySellMarks).length > 0 && ( + + Available Mark IDs: {Object.keys(buySellMarks).join(', ')} + + )} + + ) +} + +export default BuySellMarkInput \ No newline at end of file diff --git a/index.js b/index.js index acef87d..42a5976 100644 --- a/index.js +++ b/index.js @@ -147,6 +147,87 @@ const RNKLineView = forwardRef((props, ref) => { console.warn('No nodeHandle found for RNKLineView'); return []; } + }, + addBuySellMark: (buySellMark) => { + const nodeHandle = findNodeHandle(nativeRef.current); + if (nodeHandle) { + if (Platform.OS === 'ios') { + UIManager.dispatchViewManagerCommand( + nodeHandle, + UIManager.getViewManagerConfig('RNKLineView').Commands.addBuySellMark, + [buySellMark] + ); + } else { + UIManager.dispatchViewManagerCommand( + nodeHandle, + 'addBuySellMark', + [buySellMark] + ); + } + } else { + console.warn('No nodeHandle found for RNKLineView'); + } + }, + removeBuySellMark: (buySellMarkId) => { + const nodeHandle = findNodeHandle(nativeRef.current); + if (nodeHandle) { + if (Platform.OS === 'ios') { + UIManager.dispatchViewManagerCommand( + nodeHandle, + UIManager.getViewManagerConfig('RNKLineView').Commands.removeBuySellMark, + [buySellMarkId] + ); + } else { + UIManager.dispatchViewManagerCommand( + nodeHandle, + 'removeBuySellMark', + [buySellMarkId] + ); + } + } else { + console.warn('No nodeHandle found for RNKLineView'); + } + }, + updateBuySellMark: (buySellMark) => { + const nodeHandle = findNodeHandle(nativeRef.current); + if (nodeHandle) { + if (Platform.OS === 'ios') { + UIManager.dispatchViewManagerCommand( + nodeHandle, + UIManager.getViewManagerConfig('RNKLineView').Commands.updateBuySellMark, + [buySellMark] + ); + } else { + UIManager.dispatchViewManagerCommand( + nodeHandle, + 'updateBuySellMark', + [buySellMark] + ); + } + } else { + console.warn('No nodeHandle found for RNKLineView'); + } + }, + getBuySellMarks: () => { + const nodeHandle = findNodeHandle(nativeRef.current); + if (nodeHandle) { + if (Platform.OS === 'ios') { + return UIManager.dispatchViewManagerCommand( + nodeHandle, + UIManager.getViewManagerConfig('RNKLineView').Commands.getBuySellMarks, + [] + ); + } else { + return UIManager.dispatchViewManagerCommand( + nodeHandle, + 'getBuySellMarks', + [] + ); + } + } else { + console.warn('No nodeHandle found for RNKLineView'); + return []; + } } })); diff --git a/ios/Classes/HTKLineContainerView.swift b/ios/Classes/HTKLineContainerView.swift index 930907d..1516ef8 100644 --- a/ios/Classes/HTKLineContainerView.swift +++ b/ios/Classes/HTKLineContainerView.swift @@ -18,6 +18,23 @@ class HTKLineContainerView: UIView { return orderLines } + // Buy/sell mark management - indexed by timestamp for O(1) lookup + private var buyMarks: [Int64: [String: Any]] = [:] + private var sellMarks: [Int64: [String: Any]] = [:] + private var buySellMarks: [String: [String: Any]] = [:] // Keep for compatibility + + func getAllBuySellMarks() -> [String: [String: Any]] { + return buySellMarks + } + + func getBuyMarkForTime(_ time: Int64) -> [String: Any]? { + return buyMarks[time] + } + + func getSellMarkForTime(_ time: Int64) -> [String: Any]? { + return sellMarks[time] + } + @objc var onDrawItemDidTouch: RCTBubblingEventBlock? @objc var onScrollLeft: RCTBubblingEventBlock? @@ -488,5 +505,109 @@ class HTKLineContainerView: UIView { return NSArray(array: orderLinesArray) } + @objc func addBuySellMark(_ buySellMark: NSDictionary) { + print("HTKLineContainerView: addBuySellMark called with data: \(buySellMark)") + + guard let buySellMarkDict = buySellMark as? [String: Any], + let id = buySellMarkDict["id"] as? String, + let time = buySellMarkDict["time"] as? Int64, + let type = buySellMarkDict["type"] as? String else { + print("HTKLineContainerView: addBuySellMark - Invalid buy/sell mark data") + return + } + + // Store in compatibility map + buySellMarks[id] = buySellMarkDict + + // Store in efficient lookup maps by timestamp + if type == "buy" { + buyMarks[time] = buySellMarkDict + } else if type == "sell" { + sellMarks[time] = buySellMarkDict + } + + print("HTKLineContainerView: Added \(type) mark with id: \(id) at time: \(time)") + + // Trigger redraw to show the buy/sell mark + DispatchQueue.main.async { [weak self] in + self?.klineView.setNeedsDisplay() + } + } + + @objc func removeBuySellMark(_ buySellMarkId: String) { + print("HTKLineContainerView: removeBuySellMark called with id: \(buySellMarkId)") + + // Find and remove from efficient lookup maps + if let markData = buySellMarks[buySellMarkId], + let time = markData["time"] as? Int64, + let type = markData["type"] as? String { + + if type == "buy" { + buyMarks.removeValue(forKey: time) + } else if type == "sell" { + sellMarks.removeValue(forKey: time) + } + } + + // Remove from compatibility map + buySellMarks.removeValue(forKey: buySellMarkId) + print("HTKLineContainerView: Removed buy/sell mark with id: \(buySellMarkId)") + + // Trigger redraw to remove the buy/sell mark + DispatchQueue.main.async { [weak self] in + self?.klineView.setNeedsDisplay() + } + } + + @objc func updateBuySellMark(_ buySellMark: NSDictionary) { + print("HTKLineContainerView: updateBuySellMark called with data: \(buySellMark)") + + guard let buySellMarkDict = buySellMark as? [String: Any], + let id = buySellMarkDict["id"] as? String, + let time = buySellMarkDict["time"] as? Int64, + let type = buySellMarkDict["type"] as? String else { + print("HTKLineContainerView: updateBuySellMark - Invalid buy/sell mark data") + return + } + + // Remove old entry from efficient lookup maps if it exists + if let oldMarkData = buySellMarks[id], + let oldTime = oldMarkData["time"] as? Int64, + let oldType = oldMarkData["type"] as? String { + + if oldType == "buy" { + buyMarks.removeValue(forKey: oldTime) + } else if oldType == "sell" { + sellMarks.removeValue(forKey: oldTime) + } + } + + // Update compatibility map + buySellMarks[id] = buySellMarkDict + + // Add to efficient lookup maps + if type == "buy" { + buyMarks[time] = buySellMarkDict + } else if type == "sell" { + sellMarks[time] = buySellMarkDict + } + + print("HTKLineContainerView: Updated \(type) mark with id: \(id) at time: \(time)") + + // Trigger redraw to update the buy/sell mark + DispatchQueue.main.async { [weak self] in + self?.klineView.setNeedsDisplay() + } + } + + @objc func getBuySellMarks() -> NSArray { + print("HTKLineContainerView: getBuySellMarks called") + + // Return all buy/sell marks as an array + let buySellMarksArray = Array(buySellMarks.values) + print("HTKLineContainerView: Returning \(buySellMarksArray.count) buy/sell marks") + return NSArray(array: buySellMarksArray) + } + } diff --git a/ios/Classes/HTKLineView.swift b/ios/Classes/HTKLineView.swift index 5672f65..32ccf36 100644 --- a/ios/Classes/HTKLineView.swift +++ b/ios/Classes/HTKLineView.swift @@ -330,6 +330,9 @@ class HTKLineView: UIScrollView { } childDraw?.drawCandle(model, i, childMinMaxRange.upperBound, childMinMaxRange.lowerBound, childBaseY, childHeight, context, configManager) + // Draw buy/sell marks for this candlestick if they exist (O(1) lookup) + drawBuySellMarksForCandlestick(model, i, context) + let lastIndex = i == 0 ? i : i - 1 let lastModel = visibleModelArray[lastIndex] mainDraw.drawLine(model, lastModel, mainMinMaxRange.upperBound, mainMinMaxRange.lowerBound, mainBaseY, mainHeight, i, lastIndex, context, configManager) @@ -885,6 +888,86 @@ class HTKLineView: UIScrollView { } } + func drawBuySellMarksForCandlestick(_ model: HTKLineModel, _ index: Int, _ context: CGContext) { + // Access buy/sell marks from parent container view + guard let containerView = superview as? HTKLineContainerView else { + return + } + + let candleTime = model.time + + // Check for buy mark (O(1) lookup) + if let buyMark = containerView.getBuyMarkForTime(candleTime) { + drawBuySellMark(buyMark, model, index, "buy", context) + } + + // Check for sell mark (O(1) lookup) + if let sellMark = containerView.getSellMarkForTime(candleTime) { + drawBuySellMark(sellMark, model, index, "sell", context) + } + } + + private func drawBuySellMark(_ markData: [String: Any], _ model: HTKLineModel, _ index: Int, _ type: String, _ context: CGContext) { + // Calculate X position for the candlestick + let candleX = CGFloat(index) * configManager.itemWidth + configManager.itemWidth / 2 + + // Get the candlestick high price for positioning above it + let markPrice = model.high + + // Convert price to Y coordinate + let priceRange = mainMinMaxRange.upperBound - mainMinMaxRange.lowerBound + let normalizedPrice = (markPrice - mainMinMaxRange.lowerBound) / priceRange + let markY = mainBaseY + mainHeight - CGFloat(normalizedPrice) * mainHeight + + // Position mark above the candlestick + let circleRadius: CGFloat = 10 + let markCenterY = markY - circleRadius - 5 // 5 pixels above the high + + // Determine colors and text based on type + let circleColor: UIColor + let markText: String + + if type == "buy" { + circleColor = configManager.increaseColor + markText = "B" + } else { // sell + circleColor = configManager.decreaseColor + markText = "S" + } + + // Draw circle + let circleRect = CGRect(x: candleX - circleRadius, y: markCenterY - circleRadius, + width: circleRadius * 2, height: circleRadius * 2) + let circlePath = UIBezierPath(ovalIn: circleRect) + + context.setFillColor(circleColor.cgColor) + context.addPath(circlePath.cgPath) + context.fillPath() + + // Draw border + context.setStrokeColor(circleColor.cgColor) + context.setLineWidth(1.0) + context.setLineDash(phase: 0, lengths: []) // Solid line + context.addPath(circlePath.cgPath) + context.strokePath() + + // Draw text inside circle + let fontSize: CGFloat = 12 + let font = configManager.createFont(fontSize) + let textAttributes: [NSAttributedString.Key: Any] = [ + .font: font, + .foregroundColor: UIColor.white + ] + + let textSize = markText.size(withAttributes: textAttributes) + let textRect = CGRect(x: candleX - textSize.width / 2, + y: markCenterY - textSize.height / 2, + width: textSize.width, + height: textSize.height) + + markText.draw(in: textRect, withAttributes: textAttributes) + } + func valuePointFromViewPoint(_ point: CGPoint) -> CGPoint { return CGPoint.init(x: valueFromX(point.x), y: valueFromY(point.y)) } diff --git a/ios/Classes/RNKLineView.m b/ios/Classes/RNKLineView.m index 0fe0562..5081619 100644 --- a/ios/Classes/RNKLineView.m +++ b/ios/Classes/RNKLineView.m @@ -36,5 +36,16 @@ @interface RCT_EXTERN_MODULE(RNKLineView, RCTViewManager) RCT_EXTERN_METHOD(getOrderLines:(nonnull NSNumber *)node) +RCT_EXTERN_METHOD(addBuySellMark:(nonnull NSNumber *)node + buySellMark:(nonnull NSDictionary *)buySellMark) + +RCT_EXTERN_METHOD(removeBuySellMark:(nonnull NSNumber *)node + buySellMarkId:(nonnull NSString *)buySellMarkId) + +RCT_EXTERN_METHOD(updateBuySellMark:(nonnull NSNumber *)node + buySellMark:(nonnull NSDictionary *)buySellMark) + +RCT_EXTERN_METHOD(getBuySellMarks:(nonnull NSNumber *)node) + @end diff --git a/ios/Classes/RNKLineView.swift b/ios/Classes/RNKLineView.swift index 862c1c9..4e08b91 100644 --- a/ios/Classes/RNKLineView.swift +++ b/ios/Classes/RNKLineView.swift @@ -103,4 +103,50 @@ class RNKLineView: RCTViewManager { return view.getOrderLines() } + @objc func addBuySellMark(_ node: NSNumber, buySellMark: NSDictionary) { + print("RNKLineView: addBuySellMark called for node \(node) with data: \(buySellMark)") + DispatchQueue.main.async { + guard let view = self.bridge?.uiManager.view(forReactTag: node) as? HTKLineContainerView else { + print("RNKLineView: Could not find HTKLineContainerView for node \(node)") + return + } + print("RNKLineView: Calling view.addBuySellMark") + view.addBuySellMark(buySellMark) + } + } + + @objc func removeBuySellMark(_ node: NSNumber, buySellMarkId: NSString) { + print("RNKLineView: removeBuySellMark called for node \(node) with id: \(buySellMarkId)") + DispatchQueue.main.async { + guard let view = self.bridge?.uiManager.view(forReactTag: node) as? HTKLineContainerView else { + print("RNKLineView: Could not find HTKLineContainerView for node \(node)") + return + } + print("RNKLineView: Calling view.removeBuySellMark") + view.removeBuySellMark(buySellMarkId as String) + } + } + + @objc func updateBuySellMark(_ node: NSNumber, buySellMark: NSDictionary) { + print("RNKLineView: updateBuySellMark called for node \(node) with data: \(buySellMark)") + DispatchQueue.main.async { + guard let view = self.bridge?.uiManager.view(forReactTag: node) as? HTKLineContainerView else { + print("RNKLineView: Could not find HTKLineContainerView for node \(node)") + return + } + print("RNKLineView: Calling view.updateBuySellMark") + view.updateBuySellMark(buySellMark) + } + } + + @objc func getBuySellMarks(_ node: NSNumber) -> NSArray { + print("RNKLineView: getBuySellMarks called for node \(node)") + guard let view = self.bridge?.uiManager.view(forReactTag: node) as? HTKLineContainerView else { + print("RNKLineView: Could not find HTKLineContainerView for node \(node)") + return NSArray() + } + print("RNKLineView: Calling view.getBuySellMarks") + return view.getBuySellMarks() + } + } From f2408586f1c19327e3cbb94b4c450f74ed2f2ede Mon Sep 17 00:00:00 2001 From: Razvan Turturica Date: Tue, 27 Jan 2026 00:04:11 +0200 Subject: [PATCH 2/4] Fix drawings --- .../klinechart/BaseKLineChartView.java | 133 +++++++++-------- .../klinechart/HTKLineConfigManager.java | 2 + .../fujianlian/klinechart/KLineEntity.java | 1 + .../fujianlian/klinechart/RNKLineView.java | 11 -- .../container/HTKLineContainerView.java | 7 - example/App.js | 134 +++++++++--------- ios/Classes/HTKLineContainerView.swift | 6 - ios/Classes/HTKLineView.swift | 61 ++++++-- ios/Classes/RNKLineView.swift | 8 -- 9 files changed, 190 insertions(+), 173 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 1de99df..7ebe8fe 100755 --- a/android/src/main/java/com/github/fujianlian/klinechart/BaseKLineChartView.java +++ b/android/src/main/java/com/github/fujianlian/klinechart/BaseKLineChartView.java @@ -559,7 +559,7 @@ private void drawK(Canvas canvas) { } // Draw buy/sell marks for this candlestick if they exist (O(1) lookup) - drawBuySellMarksForCandlestick((KLineEntity) currentPoint, i, canvas); + drawBuySellMarksForCandlestick((KLineEntity) currentPoint, i, currentPointX, canvas); } //还原 平移缩放 @@ -1065,86 +1065,97 @@ private void drawOrderLines(Canvas canvas) { } } - private void drawBuySellMarksForCandlestick(KLineEntity candlestick, int index, Canvas canvas) { + private void drawBuySellMarksForCandlestick(KLineEntity candlestick, int index, float currentPointX, Canvas canvas) { // Get parent container view to access buy/sell marks if (getParent() instanceof HTKLineContainerView) { HTKLineContainerView containerView = (HTKLineContainerView) getParent(); - // Get candlestick time as timestamp - long candleTime; - try { - candleTime = Long.parseLong(candlestick.Date); - } catch (NumberFormatException e) { - return; // Skip if date is not a valid timestamp - } + // Use the preserved timestamp to avoid precision loss + long candleTime = candlestick.timestamp; - // Check for buy mark (O(1) lookup) + // Check for both marks to handle collision detection java.util.Map buyMark = containerView.getBuyMarkForTime(candleTime); + java.util.Map sellMark = containerView.getSellMarkForTime(candleTime); + + // Check if both marks exist to handle collision avoidance + boolean hasBothMarks = buyMark != null && sellMark != null; + + // Draw buy mark if (buyMark != null) { - drawBuySellMark(buyMark, candlestick, index, "buy", canvas); + drawBuySellMark(buyMark, candlestick, index, currentPointX, "buy", canvas, hasBothMarks); } - // Check for sell mark (O(1) lookup) - java.util.Map sellMark = containerView.getSellMarkForTime(candleTime); + // Draw sell mark if (sellMark != null) { - drawBuySellMark(sellMark, candlestick, index, "sell", canvas); + drawBuySellMark(sellMark, candlestick, index, currentPointX, "sell", canvas, hasBothMarks); } } } - private void drawBuySellMark(java.util.Map markData, KLineEntity candlestick, int index, String type, Canvas canvas) { - // Calculate X position for the candlestick - float candleX = getItemMiddleScrollX(index); - float candleViewX = scrollXtoViewX(candleX); + private void drawBuySellMark(java.util.Map markData, KLineEntity candlestick, int index, float currentPointX, String type, Canvas canvas, boolean hasBothMarks) { + // Use the same X coordinate as candlesticks (same as mMainDraw.drawTranslated) + float candleX = currentPointX; - // Position mark above the candlestick high - float markY = yFromValue(candlestick.getHighPrice()); + // Use the same Y coordinate calculation as MainDraw.drawCandle + float candleHigh = candlestick.getHighPrice(); + float highY = yFromValue(candleHigh); // Same method used by MainDraw.drawCandle - // Circle properties - float circleRadius = dp2px(10); - float markCenterY = markY - circleRadius - dp2px(5); // 5dp above the high + // Circle properties - diameter should match candlestick width + float circleRadius = mPointWidth * 0.4f; // Use 80% of candlestick width for diameter - // Only draw if visible - if (candleViewX >= 0 && candleViewX <= getWidth() && - markCenterY >= 0 && markCenterY <= getHeight()) { - - // Determine colors based on type, using same colors as candlesticks - int circleColor; - if ("buy".equals(type)) { - circleColor = configManager.increaseColor; // Use same color as increasing candlesticks - } else { // sell - circleColor = configManager.decreaseColor; // Use same color as decreasing candlesticks + // Position both marks above the candlestick, with collision avoidance + float markCenterY; + if ("buy".equals(type)) { + // Buy mark directly above the candlestick + markCenterY = highY - circleRadius - dp2px(2); + } else { // sell + if (hasBothMarks) { + // If both marks exist, position sell mark one diameter higher + markCenterY = highY - circleRadius - dp2px(2) - (circleRadius * 2) - dp2px(2); + } else { + // If only sell mark exists, position it directly above + markCenterY = highY - circleRadius - dp2px(2); } + } - // Create paint for circle - Paint circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG); - circlePaint.setColor(circleColor); - circlePaint.setStyle(Paint.Style.FILL); - - // Draw circle - canvas.drawCircle(candleViewX, markCenterY, 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(candleViewX, markCenterY, circleRadius, borderPaint); - - // Draw text inside circle - String markText = "buy".equals(type) ? "B" : "S"; - Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - textPaint.setColor(android.graphics.Color.WHITE); - textPaint.setTextSize(sp2px(12)); - textPaint.setTypeface(configManager.font); - textPaint.setTextAlign(Paint.Align.CENTER); - - // Calculate text position (center of circle) - Paint.FontMetrics fontMetrics = textPaint.getFontMetrics(); - float textY = markCenterY - (fontMetrics.ascent + fontMetrics.descent) / 2; - - canvas.drawText(markText, candleViewX, textY, textPaint); + android.util.Log.d("BuySellDebug", "Drawing " + type + " mark - visible at (" + candleX + ", " + markCenterY + ") radius: " + circleRadius); + + // Determine colors based on type, using same colors as candlesticks + int circleColor; + if ("buy".equals(type)) { + circleColor = configManager.increaseColor; // Use same color as increasing candlesticks + } else { // sell + circleColor = configManager.decreaseColor; // Use same color as decreasing candlesticks } + + // 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); + + // 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); + + // Draw text inside circle + String markText = "buy".equals(type) ? "B" : "S"; + Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + textPaint.setColor(android.graphics.Color.WHITE); + textPaint.setTextSize(circleRadius * 1.2f); // Text size proportional to circle + textPaint.setTypeface(configManager.font); + textPaint.setTextAlign(Paint.Align.CENTER); + + // Calculate text position (center of circle) + Paint.FontMetrics fontMetrics = textPaint.getFontMetrics(); + float textY = markCenterY - (fontMetrics.ascent + fontMetrics.descent) / 2; + + canvas.drawText(markText, candleX, textY, textPaint); } 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 4148528..29895f6 100644 --- a/android/src/main/java/com/github/fujianlian/klinechart/HTKLineConfigManager.java +++ b/android/src/main/java/com/github/fujianlian/klinechart/HTKLineConfigManager.java @@ -223,6 +223,8 @@ public KLineEntity packModel(Map keyValue) { idValue = keyValue.get("time"); } entity.id = idValue != null ? ((Number)idValue).intValue() : 0; + // Store original timestamp as long to preserve precision for buy/sell marks + entity.timestamp = idValue != null ? ((Number)idValue).longValue() : 0; // Handle dateString with fallback Object dateValue = keyValue.get("dateString"); diff --git a/android/src/main/java/com/github/fujianlian/klinechart/KLineEntity.java b/android/src/main/java/com/github/fujianlian/klinechart/KLineEntity.java index ab4e6b9..b7d8348 100755 --- a/android/src/main/java/com/github/fujianlian/klinechart/KLineEntity.java +++ b/android/src/main/java/com/github/fujianlian/klinechart/KLineEntity.java @@ -135,6 +135,7 @@ public float getMA10Volume() { public List> selectedItemList = new ArrayList<>(); public float id; + public long timestamp; // Store original timestamp as long to avoid precision loss public String Date; public float Open; public float High; diff --git a/android/src/main/java/com/github/fujianlian/klinechart/RNKLineView.java b/android/src/main/java/com/github/fujianlian/klinechart/RNKLineView.java index 9c84dd0..8df761c 100644 --- a/android/src/main/java/com/github/fujianlian/klinechart/RNKLineView.java +++ b/android/src/main/java/com/github/fujianlian/klinechart/RNKLineView.java @@ -213,56 +213,45 @@ public void receiveCommand(@Nonnull HTKLineContainerView containerView, String c } break; case "addBuySellMark": - android.util.Log.d("RNKLineView", "Processing addBuySellMark command"); if (args != null && args.size() > 0) { try { ReadableMap buySellMarkData = args.getMap(0); Map dataMap = buySellMarkData.toHashMap(); - android.util.Log.d("RNKLineView", "Calling containerView.addBuySellMark with data: " + dataMap); containerView.addBuySellMark(dataMap); } catch (Exception e) { android.util.Log.e("RNKLineView", "Error in addBuySellMark command", e); e.printStackTrace(); } } else { - android.util.Log.w("RNKLineView", "addBuySellMark: args is null or empty"); } break; case "removeBuySellMark": - android.util.Log.d("RNKLineView", "Processing removeBuySellMark command"); if (args != null && args.size() > 0) { try { String buySellMarkId = args.getString(0); - android.util.Log.d("RNKLineView", "Calling containerView.removeBuySellMark with id: " + buySellMarkId); containerView.removeBuySellMark(buySellMarkId); } catch (Exception e) { android.util.Log.e("RNKLineView", "Error in removeBuySellMark command", e); e.printStackTrace(); } } else { - android.util.Log.w("RNKLineView", "removeBuySellMark: args is null or empty"); } break; case "updateBuySellMark": - android.util.Log.d("RNKLineView", "Processing updateBuySellMark command"); if (args != null && args.size() > 0) { try { ReadableMap buySellMarkData = args.getMap(0); Map dataMap = buySellMarkData.toHashMap(); - android.util.Log.d("RNKLineView", "Calling containerView.updateBuySellMark with data: " + dataMap); containerView.updateBuySellMark(dataMap); } catch (Exception e) { android.util.Log.e("RNKLineView", "Error in updateBuySellMark command", e); e.printStackTrace(); } } else { - android.util.Log.w("RNKLineView", "updateBuySellMark: args is null or empty"); } break; case "getBuySellMarks": - android.util.Log.d("RNKLineView", "Processing getBuySellMarks command"); try { - android.util.Log.d("RNKLineView", "Calling containerView.getBuySellMarks"); containerView.getBuySellMarks(); } catch (Exception e) { android.util.Log.e("RNKLineView", "Error in getBuySellMarks command", e); diff --git a/android/src/main/java/com/github/fujianlian/klinechart/container/HTKLineContainerView.java b/android/src/main/java/com/github/fujianlian/klinechart/container/HTKLineContainerView.java index a5b50a7..3ce63bb 100644 --- a/android/src/main/java/com/github/fujianlian/klinechart/container/HTKLineContainerView.java +++ b/android/src/main/java/com/github/fujianlian/klinechart/container/HTKLineContainerView.java @@ -655,11 +655,9 @@ public Map> getAllOrderLines() { } public void addBuySellMark(Map buySellMarkData) { - android.util.Log.d("HTKLineContainerView", "addBuySellMark called with data: " + buySellMarkData); if (buySellMarkData == null || !buySellMarkData.containsKey("id") || !buySellMarkData.containsKey("time") || !buySellMarkData.containsKey("type")) { - android.util.Log.w("HTKLineContainerView", "addBuySellMark - Invalid buy/sell mark data"); return; } @@ -695,10 +693,8 @@ public void run() { } public void removeBuySellMark(String buySellMarkId) { - android.util.Log.d("HTKLineContainerView", "removeBuySellMark called with id: " + buySellMarkId); if (buySellMarkId == null || buySellMarkId.trim().isEmpty()) { - android.util.Log.w("HTKLineContainerView", "removeBuySellMark - Invalid buy/sell mark id"); return; } @@ -738,11 +734,9 @@ public void run() { } public void updateBuySellMark(Map buySellMarkData) { - android.util.Log.d("HTKLineContainerView", "updateBuySellMark called with data: " + buySellMarkData); if (buySellMarkData == null || !buySellMarkData.containsKey("id") || !buySellMarkData.containsKey("time") || !buySellMarkData.containsKey("type")) { - android.util.Log.w("HTKLineContainerView", "updateBuySellMark - Invalid buy/sell mark data"); return; } @@ -795,7 +789,6 @@ public void run() { } public List> getBuySellMarks() { - android.util.Log.d("HTKLineContainerView", "getBuySellMarks called"); // Return all buy/sell marks as a list synchronized (buySellMarks) { diff --git a/example/App.js b/example/App.js index c515000..679de59 100644 --- a/example/App.js +++ b/example/App.js @@ -9,7 +9,8 @@ import { StyleSheet, StatusBar, Platform, - PixelRatio + PixelRatio, + ScrollView } from 'react-native' import RNKLineView from 'react-native-kline-view' import { ThemeManager } from './utils/themes' @@ -502,71 +503,74 @@ const App = () => { {/* K-line chart */} {renderKLineChart(styles)} - {/* Order input */} - - - {/* Buy/Sell mark input */} - - {/* Bottom control bar */} - setShowTimeSelector(true)} - onShowIndicatorSelector={() => setShowIndicatorSelector(true)} - onToggleDrawToolSelector={() => { - setShowDrawToolSelector(!showDrawToolSelector) - setShowIndicatorSelector(false) - setShowTimeSelector(false) - }} - onClearDrawings={clearDrawings} - onToggleVolume={() => { - setShowVolumeChart(!showVolumeChart) - setTimeout(() => reloadKLineData(), 0) - }} - onToggleRounded={() => { - setCandleCornerRadius(candleCornerRadius > 0 ? 0 : 1) - setTimeout(() => reloadKLineData(), 0) - }} - /> - - {/* Selector popup */} - setShowTimeSelector(false)} - onCloseIndicatorSelector={() => setShowIndicatorSelector(false)} - onToggleDrawShouldContinue={(value) => setDrawShouldContinue(value)} - /> + + {/* Order input */} + + + {/* Buy/Sell mark input */} + + + {/* Bottom control bar */} + setShowTimeSelector(true)} + onShowIndicatorSelector={() => setShowIndicatorSelector(true)} + onToggleDrawToolSelector={() => { + setShowDrawToolSelector(!showDrawToolSelector) + setShowIndicatorSelector(false) + setShowTimeSelector(false) + }} + onClearDrawings={clearDrawings} + onToggleVolume={() => { + setShowVolumeChart(!showVolumeChart) + setTimeout(() => reloadKLineData(), 0) + }} + onToggleRounded={() => { + setCandleCornerRadius(candleCornerRadius > 0 ? 0 : 1) + setTimeout(() => reloadKLineData(), 0) + }} + /> + + {/* Selector popup */} + setShowTimeSelector(false)} + onCloseIndicatorSelector={() => setShowIndicatorSelector(false)} + onToggleDrawShouldContinue={(value) => setDrawShouldContinue(value)} + /> + ) } diff --git a/ios/Classes/HTKLineContainerView.swift b/ios/Classes/HTKLineContainerView.swift index 1516ef8..885ed43 100644 --- a/ios/Classes/HTKLineContainerView.swift +++ b/ios/Classes/HTKLineContainerView.swift @@ -506,13 +506,11 @@ class HTKLineContainerView: UIView { } @objc func addBuySellMark(_ buySellMark: NSDictionary) { - print("HTKLineContainerView: addBuySellMark called with data: \(buySellMark)") guard let buySellMarkDict = buySellMark as? [String: Any], let id = buySellMarkDict["id"] as? String, let time = buySellMarkDict["time"] as? Int64, let type = buySellMarkDict["type"] as? String else { - print("HTKLineContainerView: addBuySellMark - Invalid buy/sell mark data") return } @@ -535,7 +533,6 @@ class HTKLineContainerView: UIView { } @objc func removeBuySellMark(_ buySellMarkId: String) { - print("HTKLineContainerView: removeBuySellMark called with id: \(buySellMarkId)") // Find and remove from efficient lookup maps if let markData = buySellMarks[buySellMarkId], @@ -560,13 +557,11 @@ class HTKLineContainerView: UIView { } @objc func updateBuySellMark(_ buySellMark: NSDictionary) { - print("HTKLineContainerView: updateBuySellMark called with data: \(buySellMark)") guard let buySellMarkDict = buySellMark as? [String: Any], let id = buySellMarkDict["id"] as? String, let time = buySellMarkDict["time"] as? Int64, let type = buySellMarkDict["type"] as? String else { - print("HTKLineContainerView: updateBuySellMark - Invalid buy/sell mark data") return } @@ -601,7 +596,6 @@ class HTKLineContainerView: UIView { } @objc func getBuySellMarks() -> NSArray { - print("HTKLineContainerView: getBuySellMarks called") // Return all buy/sell marks as an array let buySellMarksArray = Array(buySellMarks.values) diff --git a/ios/Classes/HTKLineView.swift b/ios/Classes/HTKLineView.swift index 32ccf36..cbbc65b 100644 --- a/ios/Classes/HTKLineView.swift +++ b/ios/Classes/HTKLineView.swift @@ -238,9 +238,19 @@ class HTKLineView: UIScrollView { } func calculateBaseHeight() { - self.visibleModelArray = - configManager.modelArray.count > 0 - ? Array(configManager.modelArray[visibleRange]) : configManager.modelArray + // Safely handle visible model array calculation with bounds checking + if configManager.modelArray.count > 0 { + let startIndex = max(0, min(visibleRange.lowerBound, configManager.modelArray.count - 1)) + let endIndex = max(0, min(visibleRange.upperBound, configManager.modelArray.count - 1)) + + if startIndex >= 0 && endIndex >= startIndex && endIndex < configManager.modelArray.count { + self.visibleModelArray = Array(configManager.modelArray[startIndex...endIndex]) + } else { + self.visibleModelArray = configManager.modelArray + } + } else { + self.visibleModelArray = configManager.modelArray + } self.volumeRange = configManager.mainFlex...configManager.mainFlex + configManager.volumeFlex @@ -894,20 +904,27 @@ class HTKLineView: UIScrollView { return } - let candleTime = model.time + let candleTime = Int64(model.id) // Use model.id which contains the timestamp + + // Check for both marks to handle collision detection + let buyMark = containerView.getBuyMarkForTime(candleTime) + let sellMark = containerView.getSellMarkForTime(candleTime) + + // Check if both marks exist to handle collision avoidance + let hasBothMarks = buyMark != nil && sellMark != nil - // Check for buy mark (O(1) lookup) - if let buyMark = containerView.getBuyMarkForTime(candleTime) { - drawBuySellMark(buyMark, model, index, "buy", context) + // Draw buy mark + if let buyMark = buyMark { + drawBuySellMark(buyMark, model, index, "buy", context, hasBothMarks: hasBothMarks) } - // Check for sell mark (O(1) lookup) - if let sellMark = containerView.getSellMarkForTime(candleTime) { - drawBuySellMark(sellMark, model, index, "sell", context) + // Draw sell mark + if let sellMark = sellMark { + drawBuySellMark(sellMark, model, index, "sell", context, hasBothMarks: hasBothMarks) } } - private func drawBuySellMark(_ markData: [String: Any], _ model: HTKLineModel, _ index: Int, _ type: String, _ context: CGContext) { + private func drawBuySellMark(_ markData: [String: Any], _ model: HTKLineModel, _ index: Int, _ type: String, _ context: CGContext, hasBothMarks: Bool = false) { // Calculate X position for the candlestick let candleX = CGFloat(index) * configManager.itemWidth + configManager.itemWidth / 2 @@ -919,9 +936,23 @@ class HTKLineView: UIScrollView { let normalizedPrice = (markPrice - mainMinMaxRange.lowerBound) / priceRange let markY = mainBaseY + mainHeight - CGFloat(normalizedPrice) * mainHeight - // Position mark above the candlestick - let circleRadius: CGFloat = 10 - let markCenterY = markY - circleRadius - 5 // 5 pixels above the high + // Make circle radius match candlestick width (same as Android logic) + let circleRadius: CGFloat = configManager.itemWidth * 0.4 // 80% of candlestick width for diameter + + // Position both marks above the candlestick, with collision avoidance + let markCenterY: CGFloat + if type == "buy" { + // Buy mark directly above the candlestick + markCenterY = markY - circleRadius - 2 + } else { // sell + if hasBothMarks { + // If both marks exist, position sell mark one diameter higher + markCenterY = markY - circleRadius - 2 - (circleRadius * 2) - 2 + } else { + // If only sell mark exists, position it directly above + markCenterY = markY - circleRadius - 2 + } + } // Determine colors and text based on type let circleColor: UIColor @@ -952,7 +983,7 @@ class HTKLineView: UIScrollView { context.strokePath() // Draw text inside circle - let fontSize: CGFloat = 12 + let fontSize: CGFloat = circleRadius * 1.2 // Text size proportional to circle (same as Android) let font = configManager.createFont(fontSize) let textAttributes: [NSAttributedString.Key: Any] = [ .font: font, diff --git a/ios/Classes/RNKLineView.swift b/ios/Classes/RNKLineView.swift index 4e08b91..ab329b6 100644 --- a/ios/Classes/RNKLineView.swift +++ b/ios/Classes/RNKLineView.swift @@ -104,48 +104,40 @@ class RNKLineView: RCTViewManager { } @objc func addBuySellMark(_ node: NSNumber, buySellMark: NSDictionary) { - print("RNKLineView: addBuySellMark called for node \(node) with data: \(buySellMark)") DispatchQueue.main.async { guard let view = self.bridge?.uiManager.view(forReactTag: node) as? HTKLineContainerView else { print("RNKLineView: Could not find HTKLineContainerView for node \(node)") return } - print("RNKLineView: Calling view.addBuySellMark") view.addBuySellMark(buySellMark) } } @objc func removeBuySellMark(_ node: NSNumber, buySellMarkId: NSString) { - print("RNKLineView: removeBuySellMark called for node \(node) with id: \(buySellMarkId)") DispatchQueue.main.async { guard let view = self.bridge?.uiManager.view(forReactTag: node) as? HTKLineContainerView else { print("RNKLineView: Could not find HTKLineContainerView for node \(node)") return } - print("RNKLineView: Calling view.removeBuySellMark") view.removeBuySellMark(buySellMarkId as String) } } @objc func updateBuySellMark(_ node: NSNumber, buySellMark: NSDictionary) { - print("RNKLineView: updateBuySellMark called for node \(node) with data: \(buySellMark)") DispatchQueue.main.async { guard let view = self.bridge?.uiManager.view(forReactTag: node) as? HTKLineContainerView else { print("RNKLineView: Could not find HTKLineContainerView for node \(node)") return } - print("RNKLineView: Calling view.updateBuySellMark") view.updateBuySellMark(buySellMark) } } @objc func getBuySellMarks(_ node: NSNumber) -> NSArray { - print("RNKLineView: getBuySellMarks called for node \(node)") guard let view = self.bridge?.uiManager.view(forReactTag: node) as? HTKLineContainerView else { print("RNKLineView: Could not find HTKLineContainerView for node \(node)") return NSArray() } - print("RNKLineView: Calling view.getBuySellMarks") return view.getBuySellMarks() } From 813be5c48236feeeaf926f31b12b410376b0715b Mon Sep 17 00:00:00 2001 From: Razvan Turturica Date: Tue, 27 Jan 2026 00:26:15 +0200 Subject: [PATCH 3/4] Add tooltip --- .../fujianlian/klinechart/draw/MainDraw.java | 29 ++++++++++++++++++- example/App.js | 27 ++++++++++++----- ios/Classes/HTKLineView.swift | 24 ++++++++++++++- 3 files changed, 70 insertions(+), 10 deletions(-) diff --git a/android/src/main/java/com/github/fujianlian/klinechart/draw/MainDraw.java b/android/src/main/java/com/github/fujianlian/klinechart/draw/MainDraw.java index 88a645a..1e07ad0 100755 --- a/android/src/main/java/com/github/fujianlian/klinechart/draw/MainDraw.java +++ b/android/src/main/java/com/github/fujianlian/klinechart/draw/MainDraw.java @@ -14,6 +14,7 @@ import com.github.fujianlian.klinechart.utils.ViewUtil; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -338,8 +339,34 @@ public void drawSelector(final BaseKLineChartView view, Canvas canvas) { float top = margin + view.getTopPadding(); final KLineEntity point = (KLineEntity) view.getItem(index); + // Get the basic selectedItemList + List> itemList = new ArrayList<>(point.selectedItemList); - List> itemList = point.selectedItemList; + // Check if there are buy/sell marks for this candlestick and add them to tooltip + if (view.getParent() instanceof com.github.fujianlian.klinechart.container.HTKLineContainerView) { + com.github.fujianlian.klinechart.container.HTKLineContainerView containerView = + (com.github.fujianlian.klinechart.container.HTKLineContainerView) view.getParent(); + + long candleTime = point.timestamp; + + // Add buy mark tooltip if exists + java.util.Map buyMark = containerView.getBuyMarkForTime(candleTime); + if (buyMark != null && buyMark.containsKey("tooltipText")) { + Map buyTooltipItem = new HashMap<>(); + buyTooltipItem.put("title", ""); + buyTooltipItem.put("detail", buyMark.get("tooltipText")); + itemList.add(buyTooltipItem); + } + + // Add sell mark tooltip if exists + java.util.Map sellMark = containerView.getSellMarkForTime(candleTime); + if (sellMark != null && sellMark.containsKey("tooltipText")) { + Map sellTooltipItem = new HashMap<>(); + sellTooltipItem.put("title", ""); + sellTooltipItem.put("detail", sellMark.get("tooltipText")); + itemList.add(sellTooltipItem); + } + } float height = padding * 2 + (textHeight + lineHeight) * itemList.size() - lineHeight; diff --git a/example/App.js b/example/App.js index 679de59..c5b8c2f 100644 --- a/example/App.js +++ b/example/App.js @@ -383,13 +383,17 @@ const App = () => { const handleAddBuySellMark = useCallback((type, time, price, amount, orderCount) => { if (!kLineViewRef.current) return + const finalPrice = price || getCurrentPrice() || 0 + const finalAmount = amount || '1.0' + const buySellMark = { id: `buysell-mark-${buySellMarkIdCounter}`, time: time, type: type, // 'buy' or 'sell' - amount: amount || '1.0', - price: price || getCurrentPrice()?.toString() || '0', - orderCount: orderCount || 1 + amount: finalAmount, + price: finalPrice.toString(), + orderCount: orderCount || 1, + tooltipText: `${type.toUpperCase()} ${finalAmount} at ${finalPrice.toFixed(2)}` } console.log('Adding buy/sell mark:', buySellMark) @@ -419,12 +423,17 @@ const App = () => { return } + const finalType = newType || existingMark.type + const finalPrice = newPrice || parseFloat(existingMark.price) + const finalAmount = newAmount || existingMark.amount + const updatedMark = { ...existingMark, - type: newType || existingMark.type, - price: newPrice?.toString() || existingMark.price, - amount: newAmount || existingMark.amount, - orderCount: newOrderCount || existingMark.orderCount + type: finalType, + price: finalPrice.toString(), + amount: finalAmount, + orderCount: newOrderCount || existingMark.orderCount, + tooltipText: `${finalType.toUpperCase()} ${finalAmount} at ${finalPrice.toFixed(2)}` } console.log('Updating buy/sell mark:', updatedMark) @@ -501,7 +510,9 @@ const App = () => { /> {/* K-line chart */} - {renderKLineChart(styles)} + + {renderKLineChart(styles)} + diff --git a/ios/Classes/HTKLineView.swift b/ios/Classes/HTKLineView.swift index cbbc65b..8ee26d6 100644 --- a/ios/Classes/HTKLineView.swift +++ b/ios/Classes/HTKLineView.swift @@ -659,7 +659,29 @@ class HTKLineView: UIScrollView { guard !configManager.isMinute else { return } - let itemList = visibleModelArray[selectedIndex - visibleRange.lowerBound].selectedItemList + + // Get the selected model and its basic selectedItemList + let selectedModel = visibleModelArray[selectedIndex - visibleRange.lowerBound] + var itemList = selectedModel.selectedItemList + + // Check if there are buy/sell marks for this candlestick and add them to tooltip + if let containerView = superview as? HTKLineContainerView { + let candleTime = Int64(selectedModel.id) + + // Add buy mark tooltip if exists + if let buyMark = containerView.getBuyMarkForTime(candleTime), + let tooltipText = buyMark["tooltipText"] as? String { + let buyTooltipItem = ["title": "", "detail": tooltipText] + itemList.append(buyTooltipItem) + } + + // Add sell mark tooltip if exists + if let sellMark = containerView.getSellMarkForTime(candleTime), + let tooltipText = sellMark["tooltipText"] as? String { + let sellTooltipItem = ["title": "", "detail": tooltipText] + itemList.append(sellTooltipItem) + } + } let font = configManager.createFont(configManager.panelTextFontSize) let color = configManager.candleTextColor From a612908718e2d10fb1ddc3bc4d7b220b915725a7 Mon Sep 17 00:00:00 2001 From: Razvan Turturica Date: Tue, 27 Jan 2026 13:57:45 +0200 Subject: [PATCH 4/4] Fix types --- index.d.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/index.d.ts b/index.d.ts index 99759d8..8d56e4b 100644 --- a/index.d.ts +++ b/index.d.ts @@ -16,6 +16,16 @@ declare module 'react-native-kline-view' { labelDescriptionColor?: string; // Optional color for the description text (defaults to labelColor if not specified) } + export interface BuySellMark { + id: string; + time: number; + type: 'buy' | 'sell'; + amount: string; + price: string; + orderCount?: number; + tooltipText?: string; + } + export interface RNKLineViewRef { updateLastCandlestick: (candlestick: any) => void; addCandlesticksAtTheEnd: (candlesticks: any[]) => void; @@ -24,6 +34,10 @@ declare module 'react-native-kline-view' { removeOrderLine: (orderLineId: string) => void; updateOrderLine: (orderLine: OrderLine) => void; getOrderLines: () => OrderLine[]; + addBuySellMark: (buySellMark: BuySellMark) => void; + removeBuySellMark: (buySellMarkId: string) => void; + updateBuySellMark: (buySellMark: BuySellMark) => void; + getBuySellMarks: () => BuySellMark[]; } export interface RNKLineViewProps extends ViewProps {