package com.example.violin;
import com.example.awt.StippledStroke;
import com.example.math.Range;
import com.example.util.ArrayUtil;
import com.example.violin.density.GraphFunction;
import com.example.violin.density.KernelDensity;
import com.example.violin.density.kernels.Kernels;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.util.Collections;
import java.util.List;
/**
* @author Neil Traft
*/
public final class ViolinPlot extends JPanel {
////////////////////////////////////////////////////////////////////
// Variables
private static final int PADDING = 20;
private final GraphFunction fn = new KernelDensity(Kernels.NORMAL, 0.8f);
private final Rectangle area = new Rectangle();
private float[] values = new float[0];
private int[] yPoints = new int[0];
private int[] leftPoints = new int[0];
private int[] rightPoints = new int[0];
private Range range;
private float min;
private float max;
private float q1;
private float q3;
private float median;
private int minPx;
private int maxPx;
private int q1Px;
private int q3Px;
private int medianPx;
private int boxLeft;
private int boxRight;
private int midWidth;
////////////////////////////////////////////////////////////////////
// Public
public final void setValues(float[] values) {
if (!SwingUtilities.isEventDispatchThread()) throw new RuntimeException();
if (values == null) throw new NullPointerException();
updateValues(values);
computeStats();
}
@Override
public final void setBounds(int x, int y, int width, int height) {
super.setBounds(x, y, width, height);
SwingUtilities.calculateInnerArea(this, area);
computeStats();
}
////////////////////////////////////////////////////////////////////
// Drawing
@Override
protected final void paintComponent(Graphics graphics) {
// long start = System.currentTimeMillis();
Graphics2D g = (Graphics2D) graphics;
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setColor(Color.WHITE);
g.fillRect(area.x, area.y, area.width, area.height);
g.setColor(Color.BLACK);
// Stroke thin = g.getStroke();
// g.setStroke(new BasicStroke(1.5f));
g.drawPolyline(leftPoints, yPoints, yPoints.length);
g.drawPolyline(rightPoints, yPoints, yPoints.length);
// g.setStroke(thin);
g.drawLine(boxLeft, minPx, boxRight, minPx);
g.drawLine(boxLeft, maxPx, boxRight, maxPx);
Stroke orig = g.getStroke();
g.setStroke(new StippledStroke());
g.drawLine(midWidth, minPx, midWidth, q1Px);
g.drawLine(midWidth, q3Px, midWidth, maxPx);
g.setStroke(orig);
g.setColor(Color.BLUE);
g.drawRect(boxLeft, q1Px, boxRight - boxLeft, q3Px - q1Px);
g.drawLine(boxLeft, medianPx, boxRight, medianPx);
// System.err.printf("Drawing took %dms%n", System.currentTimeMillis() - start);
}
////////////////////////////////////////////////////////////////////
// Statistics
private void updateValues(float[] newVals) {
// long start = System.currentTimeMillis();
List<Float> sorted = ArrayUtil.listOf(newVals);
Collections.sort(sorted);
values = ArrayUtil.toArray(sorted);
min = values[0];
max = values[values.length - 1];
range = new Range(min, max).expandBy(1);
q1 = values[(int) (values.length * .25)];
q3 = values[(int) (values.length * .75)];
float mid = values.length / 2f;
int mid1 = (int) mid;
int mid2 = mid1 + 1;
if (mid == mid1) {
median = (values[mid1] + values[mid2]) / 2;
} else {
median = values[mid2];
}
// System.err.printf("Sorting took %dms%n", System.currentTimeMillis() - start);
}
private void computeStats() {
// long start = System.currentTimeMillis();
int totaly = area.height;
yPoints = new int[totaly];
leftPoints = new int[totaly];
rightPoints = new int[totaly];
if (totaly == 0) {
boxLeft = boxRight = 0;
minPx = maxPx = q1Px = q3Px = medianPx = 0;
return;
}
int totalx = area.width / 2 - PADDING;
int startx = area.x;
int starty = area.y;
midWidth = area.width / 2 + startx;
int halfLineWidth = (int) (totalx * 0.30);
boxLeft = midWidth - halfLineWidth;
boxRight = midWidth + halfLineWidth;
Range viewRange = new Range(starty, starty+totaly);
minPx = (int) range.scaleTo(min, viewRange);
maxPx = (int) range.scaleTo(max, viewRange);
q1Px = (int) range.scaleTo(q1, viewRange);
q3Px = (int) range.scaleTo(q3, viewRange);
medianPx = (int) range.scaleTo(median, viewRange);
fn.compute(totaly, totalx, range, values);
float[] yValues = fn.yValues();
float yRange = fn.yRange();
for (int i = 0; i < totaly; i++) {
yPoints[i] = i + starty;
float xpos = yValues[i] / yRange * totalx;
leftPoints[i] = (int) (midWidth - xpos);
rightPoints[i] = (int) (midWidth + xpos);
}
// System.err.printf("Computation took %dms%n", System.currentTimeMillis() - start);
}
}