From 2210d8d654ff5d367dd8ccb9fbbe1596937e069c Mon Sep 17 00:00:00 2001 From: Michael Mikovsky <77305074+Astatin3@users.noreply.github.com> Date: Wed, 9 Apr 2025 18:35:28 -0600 Subject: [PATCH] Add candlestick charts --- .../scoutingData/ScoutingDataWriter.java | 5 +- .../ridgescout/types/input/CheckboxType.java | 4 +- .../ridgescout/types/input/DropdownType.java | 4 +- .../ridgescout/types/input/FieldType.java | 23 +- .../ridgescout/types/input/FieldposType.java | 4 +- .../ridgescout/types/input/NumberType.java | 4 +- .../ridgescout/types/input/SliderType.java | 4 +- .../ridgescout/types/input/TallyType.java | 73 +++++-- .../ridgescout/types/input/TextType.java | 4 +- .../ridgescout/ui/CandlestickHeader.java | 106 +++++++++ .../ridgescout/ui/CandlestickView.java | 205 ++++++++++++++++++ .../ridgescout/ui/data/DataFragment.java | 5 + .../ridgescout/ui/data/DataProcessing.java | 107 +++++++++ .../ridgescout/ui/data/FieldDataFragment.java | 14 +- .../ridgescout/utility/AlertManager.java | 13 +- 15 files changed, 521 insertions(+), 54 deletions(-) create mode 100644 app/src/main/java/com/ridgebotics/ridgescout/ui/CandlestickHeader.java create mode 100644 app/src/main/java/com/ridgebotics/ridgescout/ui/CandlestickView.java create mode 100644 app/src/main/java/com/ridgebotics/ridgescout/ui/data/DataProcessing.java diff --git a/app/src/main/java/com/ridgebotics/ridgescout/scoutingData/ScoutingDataWriter.java b/app/src/main/java/com/ridgebotics/ridgescout/scoutingData/ScoutingDataWriter.java index 7725516..ff569cc 100644 --- a/app/src/main/java/com/ridgebotics/ridgescout/scoutingData/ScoutingDataWriter.java +++ b/app/src/main/java/com/ridgebotics/ridgescout/scoutingData/ScoutingDataWriter.java @@ -67,8 +67,9 @@ public class ScoutingDataWriter { int version = ((int)objects.get(0).get()); if(values.length <= version) { - AlertManager.addSimpleError("Field version (" +version + ") is too recent as compared to latest version (" + (values.length-1) + ")!"); - throw new BuiltByteParser.byteParsingExeption(); +// AlertManager.addSimpleError("Error loading " + filename); + AlertManager.error(new BuiltByteParser.byteParsingExeption("Field version (" +version + ") is too recent as compared to latest version (" + (values.length-1) + ")!")); + return null; } // System.out.println(version); diff --git a/app/src/main/java/com/ridgebotics/ridgescout/types/input/CheckboxType.java b/app/src/main/java/com/ridgebotics/ridgescout/types/input/CheckboxType.java index 251c0d4..1cac210 100644 --- a/app/src/main/java/com/ridgebotics/ridgescout/types/input/CheckboxType.java +++ b/app/src/main/java/com/ridgebotics/ridgescout/types/input/CheckboxType.java @@ -7,6 +7,7 @@ import android.view.ViewGroup; import android.widget.CheckBox; import android.widget.FrameLayout; import android.widget.LinearLayout; +import android.widget.TableLayout; import com.github.mikephil.charting.charts.LineChart; import com.github.mikephil.charting.charts.PieChart; @@ -24,6 +25,7 @@ import com.ridgebotics.ridgescout.utility.ByteBuilder; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.function.Function; public class CheckboxType extends FieldType { @@ -204,7 +206,7 @@ public class CheckboxType extends FieldType { parent.addView(chart); } - public void addDataToTable(LinearLayout parent, List[] data){ + public void addDataToTable(TableLayout parent, Map> data){ } diff --git a/app/src/main/java/com/ridgebotics/ridgescout/types/input/DropdownType.java b/app/src/main/java/com/ridgebotics/ridgescout/types/input/DropdownType.java index bb569ed..20e49b3 100644 --- a/app/src/main/java/com/ridgebotics/ridgescout/types/input/DropdownType.java +++ b/app/src/main/java/com/ridgebotics/ridgescout/types/input/DropdownType.java @@ -7,6 +7,7 @@ import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.LinearLayout; +import android.widget.TableLayout; import android.widget.TextView; import com.ridgebotics.ridgescout.types.data.DataType; @@ -27,6 +28,7 @@ import com.github.mikephil.charting.data.PieEntry; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.function.Function; public class DropdownType extends FieldType { @@ -232,7 +234,7 @@ public class DropdownType extends FieldType { parent.addView(chart); } - public void addDataToTable(LinearLayout parent, List[] data){ + public void addDataToTable(TableLayout parent, Map> data){ } diff --git a/app/src/main/java/com/ridgebotics/ridgescout/types/input/FieldType.java b/app/src/main/java/com/ridgebotics/ridgescout/types/input/FieldType.java index d7ebe97..1fa9122 100644 --- a/app/src/main/java/com/ridgebotics/ridgescout/types/input/FieldType.java +++ b/app/src/main/java/com/ridgebotics/ridgescout/types/input/FieldType.java @@ -3,6 +3,7 @@ package com.ridgebotics.ridgescout.types.input; import android.content.Context; import android.view.View; import android.widget.LinearLayout; +import android.widget.TableLayout; import com.ridgebotics.ridgescout.types.data.DataType; import com.ridgebotics.ridgescout.utility.BuiltByteParser; @@ -10,6 +11,7 @@ import com.ridgebotics.ridgescout.utility.ByteBuilder; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.function.Function; public abstract class FieldType { @@ -92,29 +94,10 @@ public abstract class FieldType { public abstract void add_history_view(LinearLayout parent, DataType[] data); - public abstract void addDataToTable(LinearLayout parent, List[] data); + public abstract void addDataToTable(TableLayout parent, Map> data); public abstract String toString(DataType data); - public int[] getNumberBounds(List[] data){ - int min = Integer.MAX_VALUE; - int max = Integer.MIN_VALUE; - - for(int teamNum = 0; teamNum < data.length; teamNum++){ - if(data[teamNum] == null) continue; - for(int i = 0; i < data[teamNum].size(); i++){ - DataType dataPoint = data[teamNum].get(i); - if(dataPoint == null || dataPoint.getValueType() != getValueType()) continue; - int num = (int) dataPoint.get(); - if(num > max) max = num; - if(num < min) min = num; - } - } - - return new int[]{min, max}; - } - - } \ No newline at end of file diff --git a/app/src/main/java/com/ridgebotics/ridgescout/types/input/FieldposType.java b/app/src/main/java/com/ridgebotics/ridgescout/types/input/FieldposType.java index 1be1fb4..2948afe 100644 --- a/app/src/main/java/com/ridgebotics/ridgescout/types/input/FieldposType.java +++ b/app/src/main/java/com/ridgebotics/ridgescout/types/input/FieldposType.java @@ -6,6 +6,7 @@ import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.LinearLayout; +import android.widget.TableLayout; import com.github.mikephil.charting.charts.LineChart; import com.github.mikephil.charting.components.Legend; @@ -21,6 +22,7 @@ import com.ridgebotics.ridgescout.utility.ByteBuilder; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.function.Function; public class FieldposType extends FieldType { @@ -214,7 +216,7 @@ public class FieldposType extends FieldType { parent.addView(chart); } - public void addDataToTable(LinearLayout parent, List[] data){ + public void addDataToTable(TableLayout parent, Map> data){ } diff --git a/app/src/main/java/com/ridgebotics/ridgescout/types/input/NumberType.java b/app/src/main/java/com/ridgebotics/ridgescout/types/input/NumberType.java index 9ec36f1..9bffaeb 100644 --- a/app/src/main/java/com/ridgebotics/ridgescout/types/input/NumberType.java +++ b/app/src/main/java/com/ridgebotics/ridgescout/types/input/NumberType.java @@ -12,6 +12,7 @@ import android.view.ViewGroup; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.LinearLayout; +import android.widget.TableLayout; import android.widget.TextView; import com.github.mikephil.charting.charts.LineChart; @@ -26,6 +27,7 @@ import com.ridgebotics.ridgescout.utility.ByteBuilder; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.function.Function; public class NumberType extends FieldType { @@ -309,7 +311,7 @@ public class NumberType extends FieldType { parent.addView(chart); } - public void addDataToTable(LinearLayout parent, List[] data){ + public void addDataToTable(TableLayout parent, Map> data){ } diff --git a/app/src/main/java/com/ridgebotics/ridgescout/types/input/SliderType.java b/app/src/main/java/com/ridgebotics/ridgescout/types/input/SliderType.java index 3ec85bd..5e2684e 100644 --- a/app/src/main/java/com/ridgebotics/ridgescout/types/input/SliderType.java +++ b/app/src/main/java/com/ridgebotics/ridgescout/types/input/SliderType.java @@ -6,6 +6,7 @@ import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.LinearLayout; +import android.widget.TableLayout; import androidx.annotation.NonNull; @@ -23,6 +24,7 @@ import com.google.android.material.slider.Slider; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.function.Function; public class SliderType extends FieldType { @@ -296,7 +298,7 @@ public class SliderType extends FieldType { parent.addView(chart); } - public void addDataToTable(LinearLayout parent, List[] data){ + public void addDataToTable(TableLayout parent, Map> data){ } diff --git a/app/src/main/java/com/ridgebotics/ridgescout/types/input/TallyType.java b/app/src/main/java/com/ridgebotics/ridgescout/types/input/TallyType.java index 0cf41ba..10e8374 100644 --- a/app/src/main/java/com/ridgebotics/ridgescout/types/input/TallyType.java +++ b/app/src/main/java/com/ridgebotics/ridgescout/types/input/TallyType.java @@ -7,12 +7,16 @@ import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.LinearLayout; +import android.widget.TableLayout; +import android.widget.TableRow; import android.widget.TextView; import com.ridgebotics.ridgescout.types.data.DataType; import com.ridgebotics.ridgescout.types.data.IntType; +import com.ridgebotics.ridgescout.ui.CandlestickHeader; +import com.ridgebotics.ridgescout.ui.CandlestickView; +import com.ridgebotics.ridgescout.ui.data.DataProcessing; import com.ridgebotics.ridgescout.ui.scouting.TallyCounterView; -import com.ridgebotics.ridgescout.utility.AlertManager; import com.ridgebotics.ridgescout.utility.BuiltByteParser; import com.ridgebotics.ridgescout.utility.ByteBuilder; import com.github.mikephil.charting.charts.LineChart; @@ -22,7 +26,9 @@ import com.github.mikephil.charting.data.LineData; import com.github.mikephil.charting.data.LineDataSet; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.function.Function; public class TallyType extends FieldType { @@ -290,23 +296,60 @@ public class TallyType extends FieldType { parent.addView(chart); } - public void addDataToTable(LinearLayout parent, List[] data){ - int min = Integer.MAX_VALUE; - int max = Integer.MIN_VALUE; + public void addDataToTable(TableLayout parent, Map> data){ + int[] tmp_abs_bounds = DataProcessing.getNumberBounds(data); + int absmin = tmp_abs_bounds[0]; + int absmax = tmp_abs_bounds[1]; - for(int teamNum = 0; teamNum < data.length; teamNum++){ - if(data[teamNum] == null) continue; - for(int i = 0; i < data[teamNum].size(); i++){ - DataType dataPoint = data[teamNum].get(i); - if(dataPoint == null || dataPoint.getValueType() != getValueType()) continue; - int num = (int) dataPoint.get(); - System.out.println(num); - if(num > max) max = num; - if(num < min) min = num; - } + //(int[]) teamData.get(i).get())[0]; +// AlertManager.alert("Results","Min: " + min + " Max: " + max); + + parent.removeAllViews(); + + List views = new ArrayList<>(); + + for(Integer teamNum : data.keySet()){ + CandlestickView candlestickView = new CandlestickView(parent.getContext()); + candlestickView.fromTeamData(data.get(teamNum), teamNum, absmin, absmax); + views.add(candlestickView); } - AlertManager.alert("Results","Min: " + min + " Max: " + max); + + TableRow row = new TableRow(parent.getContext()); + + // Make candlestick chart fill full width + parent.setColumnStretchable(1, true); + + // Fill in top left cell + row.addView(new View(parent.getContext())); + + CandlestickHeader header = new CandlestickHeader(parent.getContext()); + header.setScale(absmin, absmax); + row.addView(header); + + parent.addView(row); + +// parent.addView(new ); + + Collections.sort(views, (a, b) -> (int) ((b.average - a.average)*10.f)); + for(int i = 0; i < views.size(); i++){ + row = new TableRow(parent.getContext()); + CandlestickView view = views.get(i); + + TextView teamNum = new TextView(parent.getContext()); + TableRow.LayoutParams params = new TableRow.LayoutParams(); + params.gravity = Gravity.CENTER; + teamNum.setLayoutParams(params); + teamNum.setPadding(10,10,10,10); + teamNum.setTextAppearance(com.google.android.material.R.style.TextAppearance_MaterialComponents_Headline6); + teamNum.setText(String.valueOf(view.teamNum)); + + + row.addView(teamNum); + row.addView(view); + + parent.addView(row); + } } public String toString(DataType data){ diff --git a/app/src/main/java/com/ridgebotics/ridgescout/types/input/TextType.java b/app/src/main/java/com/ridgebotics/ridgescout/types/input/TextType.java index 2fe5448..acef39f 100644 --- a/app/src/main/java/com/ridgebotics/ridgescout/types/input/TextType.java +++ b/app/src/main/java/com/ridgebotics/ridgescout/types/input/TextType.java @@ -10,6 +10,7 @@ import android.view.ViewGroup; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.LinearLayout; +import android.widget.TableLayout; import android.widget.TextView; import com.ridgebotics.ridgescout.types.data.DataType; @@ -25,6 +26,7 @@ import com.github.mikephil.charting.data.LineDataSet; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.function.Function; public class TextType extends FieldType { @@ -221,7 +223,7 @@ public class TextType extends FieldType { } - public void addDataToTable(LinearLayout parent, List[] data){ + public void addDataToTable(TableLayout parent, Map> data){ } diff --git a/app/src/main/java/com/ridgebotics/ridgescout/ui/CandlestickHeader.java b/app/src/main/java/com/ridgebotics/ridgescout/ui/CandlestickHeader.java new file mode 100644 index 0000000..79bbf06 --- /dev/null +++ b/app/src/main/java/com/ridgebotics/ridgescout/ui/CandlestickHeader.java @@ -0,0 +1,106 @@ +package com.ridgebotics.ridgescout.ui; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.view.View; + +public class CandlestickHeader extends View { + + private float absoluteMin = 0; + private float absoluteMax = 100; + + // Number of scale marks to show + private final int SCALE_MARKS = 5; + + // Padding and dimensions + private final int PADDING_LEFT = 50; + private final int PADDING_RIGHT = 50; + private final int PADDING_TOP = 10; + private final int PADDING_BOTTOM = 10; + private final int TICK_HEIGHT = 10; + + private Paint linePaint; + private Paint textPaint; + + public CandlestickHeader(Context context) { + super(context); + init(); + } + + public CandlestickHeader(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public CandlestickHeader(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + linePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + linePaint.setColor(Color.GREEN); + linePaint.setStrokeWidth(2f); + linePaint.setStyle(Paint.Style.STROKE); + + textPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + textPaint.setColor(Color.GREEN); + textPaint.setTextSize(30f); + textPaint.setTextAlign(Paint.Align.CENTER); + } + + /** + * Set the scale range for the header + * + * @param absoluteMin Absolute minimum of the dataset + * @param absoluteMax Absolute maximum of the dataset + */ + public void setScale(float absoluteMin, float absoluteMax) { + this.absoluteMin = absoluteMin; + this.absoluteMax = absoluteMax; + invalidate(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int desiredWidth = 600; + int desiredHeight = 80; // Height for the scale with labels + + int width = resolveSize(desiredWidth, widthMeasureSpec); + int height = resolveSize(desiredHeight, heightMeasureSpec); + + setMeasuredDimension(width, height); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + int width = getWidth(); + int height = getHeight(); + + // Draw horizontal axis + float y = height - PADDING_BOTTOM; + canvas.drawLine(PADDING_LEFT, y, width - PADDING_RIGHT, y, linePaint); + + // Calculate interval between scale marks + float availableWidth = width - PADDING_LEFT - PADDING_RIGHT; + float intervalValue = (absoluteMax - absoluteMin) / (SCALE_MARKS - 1); + float intervalPixels = availableWidth / (SCALE_MARKS - 1); + + // Draw scale marks and labels + for (int i = 0; i < SCALE_MARKS; i++) { + float x = PADDING_LEFT + (intervalPixels * i); + float value = absoluteMin + (intervalValue * i); + + // Draw tick mark + canvas.drawLine(x, y, x, y - TICK_HEIGHT, linePaint); + + // Draw label + canvas.drawText(String.format("%.1f", value), x, y - TICK_HEIGHT - 10, textPaint); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ridgebotics/ridgescout/ui/CandlestickView.java b/app/src/main/java/com/ridgebotics/ridgescout/ui/CandlestickView.java new file mode 100644 index 0000000..57771de --- /dev/null +++ b/app/src/main/java/com/ridgebotics/ridgescout/ui/CandlestickView.java @@ -0,0 +1,205 @@ +package com.ridgebotics.ridgescout.ui; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.view.View; + +import com.ridgebotics.ridgescout.types.data.DataType; +import com.ridgebotics.ridgescout.ui.data.DataProcessing; + +import java.util.List; + +public class CandlestickView extends View { + + // Data points + private float min = 0; + private float max = 0; + private float lowerQuartile = 0; + private float upperQuartile = 0; + public float average = 0; + + // Dataset absolute bounds for scaling + private float absoluteMin = 0; + private float absoluteMax = 100; + + // Padding and dimensions + private final int PADDING_LEFT = 50; + private final int PADDING_RIGHT = 50; + private final int PADDING_TOP = 20; + private final int PADDING_BOTTOM = 20; + private final int CANDLESTICK_HEIGHT = 60; + private final int WHISKER_HEIGHT = 10; + + // Paint objects + private Paint boxPaint; + private Paint whiskerPaint; + private Paint averagePaint; + private Paint textPaint; + + public CandlestickView(Context context) { + super(context); + init(); + } + + public CandlestickView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public CandlestickView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + // Initialize paint objects + boxPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + boxPaint.setColor(Color.GREEN); + boxPaint.setStyle(Paint.Style.STROKE); + boxPaint.setStrokeWidth(2f); + + whiskerPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + whiskerPaint.setColor(Color.GREEN); + whiskerPaint.setStrokeWidth(2f); + whiskerPaint.setStyle(Paint.Style.STROKE); + + averagePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + averagePaint.setColor(Color.GREEN); + averagePaint.setStrokeWidth(3f); + + textPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + textPaint.setColor(Color.GREEN); + textPaint.setTextSize(30f); + } + + /** + * Set data for the candlestick chart + * + * @param min Minimum value + * @param lowerQuartile Lower quartile value + * @param average Average value + * @param upperQuartile Upper quartile value + * @param max Maximum value + * @param absoluteMin Absolute minimum of the dataset + * @param absoluteMax Absolute maximum of the dataset + */ + public void setData(float min, float lowerQuartile, float average, + float upperQuartile, float max, + float absoluteMin, float absoluteMax) { + this.min = min; + this.lowerQuartile = lowerQuartile; + this.average = average; + this.upperQuartile = upperQuartile; + this.max = max; + this.absoluteMin = absoluteMin; + this.absoluteMax = absoluteMax; + + // Request redraw + invalidate(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int desiredWidth = 600; + int desiredHeight = CANDLESTICK_HEIGHT + PADDING_TOP + PADDING_BOTTOM; + + int width = resolveSize(desiredWidth, widthMeasureSpec); + int height = resolveSize(desiredHeight, heightMeasureSpec); + + setMeasuredDimension(width, height); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + int width = getWidth(); + int height = getHeight(); + + // Calculate scaling factor for data points + float availableWidth = width - PADDING_LEFT - PADDING_RIGHT; + float scale = availableWidth / (absoluteMax - absoluteMin); + + // Calculate vertical center of the view + int centerY = height / 2; + + // Calculate scaled positions + float scaledMin = PADDING_LEFT + (min - absoluteMin) * scale; + float scaledLQ = PADDING_LEFT + (lowerQuartile - absoluteMin) * scale; + float scaledAvg = PADDING_LEFT + (average - absoluteMin) * scale; + float scaledUQ = PADDING_LEFT + (upperQuartile - absoluteMin) * scale; + float scaledMax = PADDING_LEFT + (max - absoluteMin) * scale; + + // Draw the box (interquartile range) + RectF box = new RectF( + scaledLQ, + centerY - CANDLESTICK_HEIGHT / 2, + scaledUQ, + centerY + CANDLESTICK_HEIGHT / 2); + canvas.drawRect(box, boxPaint); + + // Draw whiskers (min to lower quartile and upper quartile to max) + // Left whisker + canvas.drawLine(scaledMin, centerY, scaledLQ, centerY, whiskerPaint); + canvas.drawLine( + scaledMin, + centerY - WHISKER_HEIGHT, + scaledMin, + centerY + WHISKER_HEIGHT, + whiskerPaint); + + // Right whisker + canvas.drawLine(scaledUQ, centerY, scaledMax, centerY, whiskerPaint); + canvas.drawLine( + scaledMax, + centerY - WHISKER_HEIGHT, + scaledMax, + centerY + WHISKER_HEIGHT, + whiskerPaint); + + // Draw average line + canvas.drawLine( + scaledAvg, + centerY - CANDLESTICK_HEIGHT / 2, + scaledAvg, + centerY + CANDLESTICK_HEIGHT / 2, + averagePaint); + + // Draw labels +// canvas.drawText(String.format("%.1f", min), scaledMin - 20, centerY - CANDLESTICK_HEIGHT / 2 - 10, textPaint); +// canvas.drawText(String.format("%.1f", max), scaledMax - 20, centerY - CANDLESTICK_HEIGHT / 2 - 10, textPaint); +// canvas.drawText(String.format("%.1f", average), scaledAvg - 20, centerY + CANDLESTICK_HEIGHT / 2 + 30, textPaint); + } + + public int teamNum; + + public void fromTeamData(List teamData, Integer teamNum, float absmin, float absmax){ + this.teamNum = teamNum; + int[] tmp_loc_bounds = DataProcessing.getNumberBounds(teamData); + int locmin = tmp_loc_bounds[0]; + int locmax = tmp_loc_bounds[1]; + + float avg = 0; + + for(int i = 0; i < teamData.size(); i++){ + avg += (int) teamData.get(i).get(); + } + + avg /= teamData.size(); + + float[] teamDataArray = new float[teamData.size()]; + for(int i = 0; i < teamData.size(); i++){ + teamDataArray[i] = (int) teamData.get(i).get(); + } + + float lowerQuartile = DataProcessing.calculatePercentile(teamDataArray, 25); + float upperQuartile = DataProcessing.calculatePercentile(teamDataArray, 75); + + System.out.println(locmin + ", " + lowerQuartile + ", " + avg + ", " + upperQuartile + ", " + locmax); + setData(locmin, lowerQuartile, avg, upperQuartile, locmax, absmin, absmax); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ridgebotics/ridgescout/ui/data/DataFragment.java b/app/src/main/java/com/ridgebotics/ridgescout/ui/data/DataFragment.java index 13118d1..94cc933 100644 --- a/app/src/main/java/com/ridgebotics/ridgescout/ui/data/DataFragment.java +++ b/app/src/main/java/com/ridgebotics/ridgescout/ui/data/DataFragment.java @@ -83,6 +83,9 @@ public class DataFragment extends Fragment { public void load_teams(){ DataManager.reload_event(); + + if(event == null) return; + int[] teamNums = new int[event.teams.size()]; for(int i = 0 ; i < event.teams.size(); i++){ @@ -115,6 +118,8 @@ public class DataFragment extends Fragment { public void load_fields(){ DataManager.reload_match_fields(); + if(match_latest_values == null) return; + for(int i = 0; i < match_latest_values.length; i++){ FieldBorderedRow tr = new FieldBorderedRow(getContext()); tr.fromField(match_latest_values[i]); diff --git a/app/src/main/java/com/ridgebotics/ridgescout/ui/data/DataProcessing.java b/app/src/main/java/com/ridgebotics/ridgescout/ui/data/DataProcessing.java new file mode 100644 index 0000000..e2e4d6d --- /dev/null +++ b/app/src/main/java/com/ridgebotics/ridgescout/ui/data/DataProcessing.java @@ -0,0 +1,107 @@ +package com.ridgebotics.ridgescout.ui.data; + +import com.ridgebotics.ridgescout.types.data.DataType; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +public class DataProcessing { + public static int[] getNumberBounds(Map> data){ + int min = Integer.MAX_VALUE; + int max = Integer.MIN_VALUE; + + + + for(Integer teamNum : data.keySet()){ + List teamData = data.get(teamNum); + + int[] locBounds = getNumberBounds(teamData); + + if(locBounds[1] > max) max = locBounds[1]; + if(locBounds[0] < min) min = locBounds[0]; + + } + + return new int[]{min, max}; + } + + public static int[] getNumberBounds(List data){ + int min = Integer.MAX_VALUE; + int max = Integer.MIN_VALUE; + + if(data == null) return new int[]{min, max}; + for(int i = 0; i < data.size(); i++){ + DataType dataPoint = data.get(i); +// if(dataPoint == null) continue; + int num = (int) dataPoint.get(); + if(num > max) max = num; + if(num < min) min = num; + } + + return new int[]{min, max}; + } + + + //https://stackoverflow.com/questions/42381759/finding-first-quartile-and-third-quartile-in-integer-array-using-java#63891545 + public static float[] getQuartiles(List data) { + float ans[] = new float[3]; + + float[] val = new float[data.size()]; + for(int i = 0; i < val.length; i++){ + val[i] = (int) data.get(i).get(); + } + + for (int quartileType = 1; quartileType < 4; quartileType++) { + float length = val.length + 1; + float quartile; + float newArraySize = (length * ((float) (quartileType) * 25 / 100)) - 1; + Arrays.sort(val); + if (newArraySize % 1 == 0) { + quartile = val[(int) (newArraySize)]; + } else { + int newArraySize1 = (int) (newArraySize); + quartile = (val[newArraySize1] + val[newArraySize1 + 1]) / 2; + } + ans[quartileType - 1] = quartile; + } + return ans; + } + + + /** + * Calculates the specified percentile of a dataset. + * + * @param data The dataset to analyze + * @param percentile The percentile to find (0-100) + * @return The value at the specified percentile + */ + public static float calculatePercentile(float[] data, float percentile) { + // Make a copy of the data to avoid modifying the original + float[] dataCopy = Arrays.copyOf(data, data.length); + + // Sort the data in ascending order + Arrays.sort(dataCopy); + + // Calculate the position as a fraction + float position = (percentile / 100.0f) * (dataCopy.length - 1); + + // Get the integer part of the position + int lowerIndex = (int) Math.floor(position); + int upperIndex = (int) Math.ceil(position); + + // If position is already an integer, return the value at that position + if (lowerIndex == upperIndex) { + return dataCopy[lowerIndex]; + } + + // Otherwise, interpolate between the two adjacent values + float fraction = position - lowerIndex; + float lowerValue = dataCopy[lowerIndex]; + float upperValue = dataCopy[upperIndex]; + + // Linear interpolation + return lowerValue + fraction * (upperValue - lowerValue); + } + +} diff --git a/app/src/main/java/com/ridgebotics/ridgescout/ui/data/FieldDataFragment.java b/app/src/main/java/com/ridgebotics/ridgescout/ui/data/FieldDataFragment.java index a86fe38..caa6fa7 100644 --- a/app/src/main/java/com/ridgebotics/ridgescout/ui/data/FieldDataFragment.java +++ b/app/src/main/java/com/ridgebotics/ridgescout/ui/data/FieldDataFragment.java @@ -25,7 +25,9 @@ import com.ridgebotics.ridgescout.utility.AlertManager; import com.ridgebotics.ridgescout.utility.FileEditor; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; public class FieldDataFragment extends Fragment { @@ -50,22 +52,25 @@ public class FieldDataFragment extends Fragment { Thread t = new Thread(() -> { - List[] data = new ArrayList[event.teams.size()]; + Map> data = new HashMap<>(); for (int teamIndex = 0; teamIndex < event.teams.size(); teamIndex++) { - + int teamNum = event.teams.get(teamIndex).teamNumber; List filenames = new ArrayList<>(List.of(FileEditor.getMatchesByTeamNum(evcode, event.teams.get(teamIndex).teamNumber))); filenames.removeAll(rescout_list); + ArrayList teamData = new ArrayList<>(); + for (int i = 0; i < filenames.size(); i++) { - data[teamIndex] = new ArrayList<>(); try { ScoutingDataWriter.ParsedScoutingDataResult psda = ScoutingDataWriter.load(filenames.get(i), match_values, match_transferValues); if (psda.data.array[fieldIndex] != null && psda.data.array[fieldIndex].get() != null) - data[teamIndex].add(psda.data.array[fieldIndex]); + teamData.add(psda.data.array[fieldIndex]); } catch (Exception e) { AlertManager.error("Failure to load file " + filenames.get(i), e); } } + + data.put(teamNum, teamData); } System.out.println("Finished!"); @@ -73,6 +78,7 @@ public class FieldDataFragment extends Fragment { getActivity().runOnUiThread(() -> { + binding.table.setStretchAllColumns(false); match_latest_values[fieldIndex].addDataToTable(binding.table, data); stopLoading(); }); diff --git a/app/src/main/java/com/ridgebotics/ridgescout/utility/AlertManager.java b/app/src/main/java/com/ridgebotics/ridgescout/utility/AlertManager.java index 810403b..50fecf0 100644 --- a/app/src/main/java/com/ridgebotics/ridgescout/utility/AlertManager.java +++ b/app/src/main/java/com/ridgebotics/ridgescout/utility/AlertManager.java @@ -62,18 +62,18 @@ public class AlertManager { StringWriter sw = new StringWriter(); e.printStackTrace(new PrintWriter(sw)); - errorList.add((sw.toString())); + errorList.add(sw.getBuffer().toString()); updateErrors(); } public static void error(String title, Exception e) { simpleErrorList.add(title); - e.printStackTrace(); StringWriter sw = new StringWriter(); e.printStackTrace(new PrintWriter(sw)); - errorList.add((sw.toString())); + errorList.add(sw.toString()); + e.printStackTrace(); updateErrors(); } @@ -97,16 +97,15 @@ public class AlertManager { alert.setPositiveButton("OK", (dialogInterface, i) -> {if(currentError != null){errorList.clear(); simpleErrorList.clear();}}); + String detailedErrors = String.join("\n\n\n\n\n", errorList); + if(!errorList.isEmpty()) - alert.setNeutralButton("View Detailed Error" + (errorList.size() != 1 ? "s" : ""), (dialogInterface, i) -> alert(errorList.size() + " Error" + (errorList.size() != 1 ? "s" : "") + ":", String.join("\n\n\n\n\n", errorList))); + alert.setNeutralButton("View Detailed Error" + (errorList.size() != 1 ? "s" : ""), (dialogInterface, i) -> alert("Details", detailedErrors)); alert.setOnDismissListener((x) -> {if(currentError != null){errorList.clear(); simpleErrorList.clear();}}); - alert.setCancelable(true); - currentError = alert.create(); - currentError.show(); }); }