Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions MPChartLib/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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(),
Expand All @@ -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
Expand All @@ -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;
}

// /**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

/**
* Created by Philipp Jahoda on 21/07/15.
* Fully optimized interaction targeting engine fork.
*/
public class ChartHighlighter<T extends BarLineScatterCandleBubbleDataProvider> implements IHighlighter
{
Expand Down Expand Up @@ -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<Highlight> 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.
Expand Down Expand Up @@ -113,12 +199,11 @@ protected float getMinimumDistance(List<Highlight> 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
Expand Down Expand Up @@ -150,12 +235,6 @@ protected List<Highlight> 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<Highlight> buildHighlights(IDataSet set, int dataSetIndex, float xVal, DataSet.Rounding rounding) {

Expand Down Expand Up @@ -189,58 +268,14 @@ protected List<Highlight> 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<Highlight> 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();
}
}
}
Loading