diff --git a/MPChartLib/build.gradle b/MPChartLib/build.gradle index 16d3e3ce10..5d20f8f5a3 100644 --- a/MPChartLib/build.gradle +++ b/MPChartLib/build.gradle @@ -4,12 +4,9 @@ group='com.github.philjay' android { compileSdkVersion 28 - buildToolsVersion '28.0.3' defaultConfig { minSdkVersion 14 targetSdkVersion 28 - versionCode 3 - versionName '3.1.0' } buildTypes { release { diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/BarHighlighter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/BarHighlighter.java index af83a4539f..fe80e26238 100644 --- a/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/BarHighlighter.java +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/BarHighlighter.java @@ -20,25 +20,52 @@ public BarHighlighter(BarDataProvider chart) { public Highlight getHighlight(float x, float y) { Highlight high = super.getHighlight(x, y); - if(high == null) { + if (high == null) { return null; } MPPointD pos = getValsForTouch(x, y); - BarData barData = mChart.getBarData(); - IBarDataSet set = barData.getDataSetByIndex(high.getDataSetIndex()); - if (set.isStacked()) { + if (set.isStacked()) { return getStackedHighlight(high, set, (float) pos.x, (float) pos.y); } - MPPointD.recycleInstance(pos); + // ─── TRANSLATED DATA-BOUNDS FIX ─── + BarEntry entry = set.getEntryForXValue(high.getX(), high.getY()); + if (entry != null && !set.isStacked()) { + + // 1. Check Horizontal (X) Bounds: Ensure touch x is within the width of this specific bar slot + float barWidthHalf = barData.getBarWidth() / 2f; + float barLeft = entry.getX() - barWidthHalf; + float barRight = entry.getX() + barWidthHalf; + + if (pos.x < barLeft || pos.x > barRight) { + MPPointD.recycleInstance(pos); + return null; + } + // 2. Check Vertical (Y) Bounds: Ensure touch y is between 0 and the bar value + float val = entry.getY(); + if (val >= 0) { + if (pos.y > val || pos.y < 0) { + MPPointD.recycleInstance(pos); + return null; + } + } else { + if (pos.y < val || pos.y > 0) { + MPPointD.recycleInstance(pos); + return null; + } + } + } + // ─── END OF FIX ─── + + MPPointD.recycleInstance(pos); return high; } @@ -59,16 +86,75 @@ public Highlight getStackedHighlight(Highlight high, IBarDataSet set, float xVal if (entry == null) return null; - // not stacked if (entry.getYVals() == null) { return high; } else { Range[] ranges = entry.getRanges(); if (ranges.length > 0) { - int stackIndex = getClosestStackIndex(ranges, yVal); - MPPointD pixels = mChart.getTransformer(set.getAxisDependency()).getPixelForValues(high.getX(), ranges[stackIndex].to); + boolean isHorizontal = mChart instanceof com.github.mikephil.charting.charts.HorizontalBarChart; + + if (isHorizontal) { + float barWidthHalf = mChart.getBarData().getBarWidth() / 2f; + float barBottomEdge = entry.getX() - barWidthHalf; + float barTopEdge = entry.getX() + barWidthHalf; + + // yVal is category axis for horizontal — but it's actually passed as xVal here + // Use xVal for the category check, yVal for the value check + if (xVal < barBottomEdge || xVal > barTopEdge) { + android.util.Log.d("HIGHLIGHT", "KILLED by gaps check"); + return null; + } + + float stackMin = ranges[0].from; + float stackMax = ranges[ranges.length - 1].to; + + +// Extra check: reject if tap is in the gap between bar end and axis + + + if (yVal < stackMin || yVal > stackMax) { + return null; + } + } else { + // ─── STACK 1: VERTICAL STACKED CHART ─── + // For Vertical charts: xVal = Dataset Column Entry Index, yVal = Value Metric + + // 1. Gaps Check: Check if touch xVal falls within the vertical column thickness + float barWidthHalf = mChart.getBarData().getBarWidth() / 2f; + float barLeftEdge = entry.getX() - barWidthHalf; + float barRightEdge = entry.getX() + barWidthHalf; + + if (xVal < barLeftEdge || xVal > barRightEdge) { + return null; + } + + // 2. Value Bounds Check: Check if touch yVal falls within the vertical bars' combined height + float stackBottom = ranges[0].from; + float stackTop = ranges[ranges.length - 1].to; + + if (entry.getY() >= 0) { + if (yVal > stackTop || yVal < 0) return null; + } else { + if (yVal < stackBottom || yVal > 0) return null; + } + } + + // Route search to find the closest nested segment index based on actual orientation + int stackIndex = getClosestStackIndex(ranges, isHorizontal ? yVal : xVal); + android.util.Log.d("HIGHLIGHT", "yVal=" + yVal + " stackIndex=" + stackIndex + " ranges[0]=" + ranges[0].from + "-" + ranges[0].to + " ranges[1]=" + ranges[1].from + "-" + ranges[1].to); + + MPPointD pixels; + if (isHorizontal) { + float highlightVal = (ranges[stackIndex].from < 0) + ? ranges[stackIndex].from + : ranges[stackIndex].to; + pixels = mChart.getTransformer(set.getAxisDependency()).getPixelForValues(highlightVal, high.getX()); + } else { + // For Vertical: entry data representation represents X-pixels, value metrics represent Y-pixels + pixels = mChart.getTransformer(set.getAxisDependency()).getPixelForValues(high.getX(), ranges[stackIndex].to); + } Highlight stackedHigh = new Highlight( entry.getX(), @@ -88,7 +174,6 @@ public Highlight getStackedHighlight(Highlight high, IBarDataSet set, float xVal return null; } - /** * Returns the index of the closest value inside the values array / ranges (stacked barchart) to the value * given as @@ -112,9 +197,20 @@ protected int getClosestStackIndex(Range[] ranges, float value) { stackIndex++; } - int length = Math.max(ranges.length - 1, 0); + // Fallback: find the range whose midpoint is closest to value + int closest = 0; + float closestDist = Float.MAX_VALUE; + + for (int i = 0; i < ranges.length; i++) { + float mid = (ranges[i].from + ranges[i].to) / 2f; + float dist = Math.abs(mid - value); + if (dist < closestDist) { + closestDist = dist; + closest = i; + } + } - return (value > ranges[length].to) ? length : 0; + return closest; } // /** diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/ChartHighlighter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/ChartHighlighter.java index f889bf19d4..0693c31d70 100644 --- a/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/ChartHighlighter.java +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/ChartHighlighter.java @@ -13,6 +13,7 @@ /** * Created by Philipp Jahoda on 21/07/15. + * Fully optimized interaction targeting engine fork. */ public class ChartHighlighter implements IHighlighter { @@ -78,11 +79,96 @@ protected Highlight getHighlightForX(float xVal, float x, float y) { YAxis.AxisDependency axis = leftAxisMinDist < rightAxisMinDist ? YAxis.AxisDependency.LEFT : YAxis.AxisDependency.RIGHT; - Highlight detail = getClosestHighlightByPixel(closestValues, x, y, axis, mChart.getMaxHighlightDistance()); + float density = ((android.view.View) mChart).getContext().getResources().getDisplayMetrics().density; + + // Default safety bubble for standard line/scatter charts + float maxSelectionDistance = 40f * density; + + Highlight detail = getClosestHighlightByPixel(closestValues, x, y, axis, maxSelectionDistance); return detail; } + /** + * Returns the Highlight of the DataSet that contains the closest value on the + * y-axis. + * + * @param closestValues contains two Highlight objects per DataSet closest to the selected x-position + * @param x + * @param y + * @param axis the closest axis + * @param minSelectionDistance + * @return + */ + public Highlight getClosestHighlightByPixel(List closestValues, float x, float y, + YAxis.AxisDependency axis, float minSelectionDistance) { + + Highlight closest = null; + float shortestDistance = Float.MAX_VALUE; + + boolean isBubble = mChart instanceof com.github.mikephil.charting.interfaces.dataprovider.BubbleDataProvider; + + for (int i = 0; i < closestValues.size(); i++) { + Highlight high = closestValues.get(i); + + if (axis == null || high.getAxis() == axis) { + float cDistance = getDistance(x, y, high.getXPx(), high.getYPx()); + + if (isBubble) { + com.github.mikephil.charting.interfaces.datasets.IBubbleDataSet dataSet = + (com.github.mikephil.charting.interfaces.datasets.IBubbleDataSet) mChart.getData().getDataSetByIndex(high.getDataSetIndex()); + + if (dataSet == null) continue; + + Entry entry = dataSet.getEntryForXValue(high.getX(), Float.NaN, DataSet.Rounding.CLOSEST); + + if (entry instanceof com.github.mikephil.charting.data.BubbleEntry) { + com.github.mikephil.charting.data.BubbleEntry bubbleEntry = (com.github.mikephil.charting.data.BubbleEntry) entry; + + float density = ((android.view.View) mChart).getContext().getResources().getDisplayMetrics().density; + +// 1. SAFE ZOOM SCALE TRACKING VIA BASE CHART VIEW + float currentZoomScale = 1.0f; + if (mChart instanceof com.github.mikephil.charting.charts.BarLineChartBase) { + currentZoomScale = ((com.github.mikephil.charting.charts.BarLineChartBase) mChart).getViewPortHandler().getScaleX(); + } + +// Calculate the base radius drawn on screen + float rawSize = bubbleEntry.getSize(); + float baseRadius = rawSize / 2f; + +// Multiply by currentZoomScale so the touch footprint expands dynamically as the user zooms in! + float physicalBubbleRadius = baseRadius * density * currentZoomScale; + + // 2. THE TINY-BUBBLE ACCESSIBILITY BOOST + // Give all small/nested entries an absolute floor touch radius of 15dp + // This guarantees tiny hidden dots remain physically clickable over massive background shapes + float minimumTouchFloor = 15f * density; + if (physicalBubbleRadius < minimumTouchFloor) { + physicalBubbleRadius = minimumTouchFloor; + } + + // 3. SELECTION RESOLUTION + if (cDistance <= physicalBubbleRadius) { + if (cDistance < shortestDistance) { + shortestDistance = cDistance; + closest = high; + } + } + } + } else { + // Standard 40dp safety target mapping for Line/Scatter plots + if (cDistance < minSelectionDistance && cDistance < shortestDistance) { + shortestDistance = cDistance; + closest = high; + } + } + } + } + + return closest; + } + /** * Returns the minimum distance from a touch value (in pixels) to the * closest value (in pixels) that is displayed in the chart. @@ -113,12 +199,11 @@ protected float getMinimumDistance(List closestValues, float pos, YAx } protected float getHighlightPos(Highlight h) { - return h.getYPx(); + return h.getYPx(); // default for vertical charts } /** * Returns a list of Highlight objects representing the entries closest to the given xVal. - * The returned list contains two objects per DataSet (closest rounding up, closest rounding down). * * @param xVal the transformed x-value of the x-touch position * @param x touch position @@ -150,12 +235,6 @@ protected List getHighlightsAtXValue(float xVal, float x, float y) { /** * An array of `Highlight` objects corresponding to the selected xValue and dataSetIndex. - * - * @param set - * @param dataSetIndex - * @param xVal - * @param rounding - * @return */ protected List buildHighlights(IDataSet set, int dataSetIndex, float xVal, DataSet.Rounding rounding) { @@ -189,58 +268,14 @@ protected List buildHighlights(IDataSet set, int dataSetIndex, float return highlights; } - /** - * Returns the Highlight of the DataSet that contains the closest value on the - * y-axis. - * - * @param closestValues contains two Highlight objects per DataSet closest to the selected x-position (determined by - * rounding up an down) - * @param x - * @param y - * @param axis the closest axis - * @param minSelectionDistance - * @return - */ - public Highlight getClosestHighlightByPixel(List closestValues, float x, float y, - YAxis.AxisDependency axis, float minSelectionDistance) { - - Highlight closest = null; - float distance = minSelectionDistance; - - for (int i = 0; i < closestValues.size(); i++) { - - Highlight high = closestValues.get(i); - - if (axis == null || high.getAxis() == axis) { - - float cDistance = getDistance(x, y, high.getXPx(), high.getYPx()); - - if (cDistance < distance) { - closest = high; - distance = cDistance; - } - } - } - - return closest; - } - /** * Calculates the distance between the two given points. - * - * @param x1 - * @param y1 - * @param x2 - * @param y2 - * @return */ protected float getDistance(float x1, float y1, float x2, float y2) { - //return Math.abs(y1 - y2); - //return Math.abs(x1 - x2); return (float) Math.hypot(x1 - x2, y1 - y2); } protected BarLineScatterCandleBubbleData getData() { return mChart.getData(); } -} +} \ No newline at end of file diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/HorizontalBarHighlighter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/HorizontalBarHighlighter.java index d96298e07d..c6e8a83b14 100644 --- a/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/HorizontalBarHighlighter.java +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/HorizontalBarHighlighter.java @@ -11,6 +11,9 @@ import java.util.ArrayList; import java.util.List; +import com.github.mikephil.charting.data.BarEntry; + + /** * Created by Philipp Jahoda on 22/07/15. */ @@ -24,24 +27,54 @@ public HorizontalBarHighlighter(BarDataProvider chart) { public Highlight getHighlight(float x, float y) { BarData barData = mChart.getBarData(); - MPPointD pos = getValsForTouch(y, x); Highlight high = getHighlightForX((float) pos.y, y, x); - if (high == null) + if (high == null) { + android.util.Log.d("HIGHLIGHT", "high is null after getHighlightForX"); // ← ADD return null; + } IBarDataSet set = barData.getDataSetByIndex(high.getDataSetIndex()); if (set.isStacked()) { + Highlight result = getStackedHighlight(high, set, (float) pos.y, (float) pos.x); + android.util.Log.d("HIGHLIGHT", "stacked result: " + (result == null ? "NULL" : "x=" + result.getX() + " xPx=" + result.getXPx() + " yPx=" + result.getYPx())); // ← ADD + return result; + } + + // ─── TRANSLATED DATA-BOUNDS FIX ─── + BarEntry entry = set.getEntryForXValue(high.getX(), high.getY()); + if (entry != null) { - return getStackedHighlight(high, - set, - (float) pos.y, - (float) pos.x); + // Vertical Category Check: Ensure touch y is within the slot height bounds + float barWidthHalf = barData.getBarWidth() / 2f; + float barBottom = entry.getX() - barWidthHalf; + float barTop = entry.getX() + barWidthHalf; + + if (pos.y < barBottom || pos.y > barTop) { + MPPointD.recycleInstance(pos); + return null; + } + + // Horizontal Value Length Check: Ensure touch x matches the bar length range + float val = entry.getY(); + if (val >= 0) { + if (pos.x > val || pos.x < 0) { + MPPointD.recycleInstance(pos); + return null; + } + } else { + if (pos.x < val || pos.x > 0) { + MPPointD.recycleInstance(pos); + return null; + } + } } + // ─── END OF FIX ─── MPPointD.recycleInstance(pos); - + MPPointD.recycleInstance(pos); + android.util.Log.d("HIGHLIGHT", "Returning high: x=" + high.getX() + " y=" + high.getY()); // ← ADD return high; } @@ -82,4 +115,9 @@ protected List buildHighlights(IDataSet set, int dataSetIndex, float protected float getDistance(float x1, float y1, float x2, float y2) { return Math.abs(y1 - y2); } + @Override + protected float getHighlightPos(Highlight h) { + + return h.getXPx(); + } } diff --git a/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/PieRadarHighlighter.java b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/PieRadarHighlighter.java index a19906d75c..c630de6a6c 100644 --- a/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/PieRadarHighlighter.java +++ b/MPChartLib/src/main/java/com/github/mikephil/charting/highlight/PieRadarHighlighter.java @@ -28,30 +28,44 @@ public Highlight getHighlight(float x, float y) { float touchDistanceToCenter = mChart.distanceToCenter(x, y); - // check if a slice was touched - if (touchDistanceToCenter > mChart.getRadius()) { + // ─── SAFE PROPORTIONAL BOUNDARY FOR PIE CHARTS ─── + float extendedRadius = mChart.getRadius() * 1.25f; - // if no slice was touched, highlight nothing + if (touchDistanceToCenter > extendedRadius) { return null; + } - } else { + float angle = mChart.getAngleForPoint(x, y); - float angle = mChart.getAngleForPoint(x, y); + if (mChart instanceof PieChart) { + angle /= mChart.getAnimator().getPhaseY(); + } - if (mChart instanceof PieChart) { - angle /= mChart.getAnimator().getPhaseY(); - } + int index = mChart.getIndexForAngle(angle); + + // check if the index could be found + if (index < 0 || index >= mChart.getData().getMaxEntryCountSet().getEntryCount()) { + return null; + } - int index = mChart.getIndexForAngle(angle); + Highlight hint = getClosestHighlight(index, x, y); - // check if the index could be found - if (index < 0 || index >= mChart.getData().getMaxEntryCountSet().getEntryCount()) { - return null; + // ─── NEW RADAR CHART PROXIMITY FILTER ─── + // If it's a Radar Chart, enforce a comfortable 40dp finger-sized touch boundary + if (!(mChart instanceof PieChart) && hint != null) { + float density = mChart.getContext().getResources().getDisplayMetrics().density; + float maxSelectionDistance = 40f * density; + + // Calculate absolute distance between your touch point and the actual drawn vertex + float distanceToVertex = (float) Math.hypot(x - hint.getXPx(), y - hint.getYPx()); - } else { - return getClosestHighlight(index, x, y); + // If your finger is further than 40dp from the actual point, reject the touch + if (distanceToVertex > maxSelectionDistance) { + return null; } } + + return hint; } /** @@ -63,4 +77,4 @@ public Highlight getHighlight(float x, float y) { * @return */ protected abstract Highlight getClosestHighlight(int index, float x, float y); -} +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index ac31f01cf5..dc999e6d49 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.0.4' + classpath 'com.android.tools.build:gradle:7.1.3' } }