/* * Apache License * Version 2.0, January 2004 * http://www.apache.org/licenses/ * * Copyright 2013 Aurelian Tutuianu * Copyright 2014 Aurelian Tutuianu * Copyright 2015 Aurelian Tutuianu * Copyright 2016 Aurelian Tutuianu * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package rapaio.graphics.plot; import rapaio.core.stat.Quantiles; import rapaio.data.Frame; import rapaio.data.Index; import rapaio.data.Numeric; import rapaio.data.Var; import rapaio.data.stream.VSpot; import rapaio.graphics.base.HostFigure; import rapaio.graphics.base.Range; import rapaio.graphics.opt.ColorPalette; import rapaio.graphics.opt.GOpt; import rapaio.graphics.opt.GOpts; import rapaio.graphics.opt.PchPalette; import java.awt.*; import java.awt.geom.Line2D; import java.awt.geom.Rectangle2D; import java.util.Arrays; import java.util.List; import java.util.Map; import static java.util.stream.Collectors.*; /** * @author <a href="mailto:padreati@yahoo.com">Aurelian Tutuianu</a> */ public class BoxPlot extends HostFigure { private static final long serialVersionUID = 8868603141563818477L; private final Var[] vars; private final String[] names; private final GOpts options = new GOpts(); public BoxPlot(Var x, Var factor, GOpt... opts) { Map<String, List<Double>> map = x.stream().collect(groupingBy(s -> factor.label(s.row()), mapping(VSpot::value, toList()))); names = factor.streamLevels().filter(map::containsKey).toArray(String[]::new); vars = Arrays.stream(names).map(map::get).map(Numeric::copy).toArray(Var[]::new); this.options.apply(opts); initialize(); } public BoxPlot(Var x, GOpt... opts) { this(new Var[]{x}, opts); } public BoxPlot(Var[] vars, GOpt... opts) { this.vars = vars; this.names = Arrays.stream(vars).map(Var::name).toArray(String[]::new); this.options.apply(opts); initialize(); } public BoxPlot(Frame df, GOpt... opts) { this.vars = df.varStream().filter(var -> var.stream().complete().count() > 0).toArray(Var[]::new); this.names = Arrays.stream(vars).map(Var::name).toArray(String[]::new); this.options.apply(opts); initialize(); } private void initialize() { leftMarkers(true); leftThick(true); bottomMarkers(true); bottomThick(true); options.setPchDefault(gOpts -> Index.wrap(0, 3)); options.setColorDefault(gOpts -> new Color[]{new Color(240, 240, 240)}); } @Override public Range buildRange() { Range range = new Range(); range.union(0, Double.NaN); range.union(vars.length, Double.NaN); for (Var v : vars) { for (int i = 0; i < v.rowCount(); i++) { if (v.missing(i)) continue; range.union(Double.NaN, v.value(i)); } } return range; } @Override public void buildLeftMarkers() { buildNumericLeftMarkers(); } @Override public void buildBottomMarkers() { bottomMarkersPos.clear(); bottomMarkersMsg.clear(); double xSpotWidth = getViewport().width / vars.length; for (int i = 0; i < vars.length; i++) { bottomMarkersPos.add(i * xSpotWidth + xSpotWidth / 2); bottomMarkersMsg.add(names[i]); } } @Override public void paint(Graphics2D g2d, Rectangle rect) { super.paint(g2d, rect); g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, options.getAlpha())); for (int i = 0; i < vars.length; i++) { Var v = vars[i]; if (v.rowCount() == 0) { continue; } double[] p = new double[]{0.25, 0.5, 0.75}; double[] q = Quantiles.from(v, p).values(); double iqr = q[2] - q[0]; double innerFence = 1.5 * iqr; double outerFence = 3 * iqr; double x1 = i + 0.5 - 0.3; double x2 = i + 0.5; double x3 = i + 0.5 + 0.3; // first we fill the space g2d.setColor(options.getColor(i)); g2d.fill(new Rectangle2D.Double(xScale(x1), yScale(q[2]), xScale(x3) - xScale(x1), yScale(q[0]) - yScale(q[2]))); g2d.setColor(ColorPalette.STANDARD.getColor(0)); // median g2d.setStroke(new BasicStroke(options.getLwd() * 2)); g2d.draw(new Line2D.Double( xScale(x1), yScale(q[1]), xScale(x3), yScale(q[1]))); // box g2d.setStroke(new BasicStroke(options.getLwd())); g2d.draw(new Line2D.Double(xScale(x1), yScale(q[0]), xScale(x3), yScale(q[0]))); g2d.draw(new Line2D.Double(xScale(x1), yScale(q[2]), xScale(x3), yScale(q[2]))); g2d.draw(new Line2D.Double(xScale(x1), yScale(q[0]), xScale(x1), yScale(q[2]))); g2d.draw(new Line2D.Double(xScale(x3), yScale(q[0]), xScale(x3), yScale(q[2]))); // outliers double upperwhisker = q[2]; double lowerqhisker = q[0]; for (int j = 0; j < v.rowCount(); j++) { double point = v.value(j); if ((point > q[2] + outerFence) || (point < q[0] - outerFence)) { // big outlier g2d.setStroke(new BasicStroke(options.getLwd())); PchPalette.STANDARD.draw(g2d, xScale(x2), yScale(point), options.getSz(i), options.getPch(1)); continue; } if ((point > q[2] + innerFence) || (point < q[0] - innerFence)) { // outlier g2d.setStroke(new BasicStroke(options.getLwd())); PchPalette.STANDARD.draw(g2d, xScale(x2), yScale(point), options.getSz(i), options.getPch(0)); continue; } if ((point > upperwhisker) && (point < q[2] + innerFence)) { upperwhisker = Math.max(upperwhisker, point); } if ((point < lowerqhisker) && (point >= q[0] - innerFence)) { lowerqhisker = Math.min(lowerqhisker, point); } } // whiskers g2d.draw(new Line2D.Double(xScale(x1), yScale(upperwhisker), xScale(x3), yScale(upperwhisker))); g2d.draw(new Line2D.Double(xScale(x1), yScale(lowerqhisker), xScale(x3), yScale(lowerqhisker))); g2d.setStroke(new BasicStroke(options.getLwd(), BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1f, new float[]{8}, 0)); g2d.draw(new Line2D.Double(xScale(x2), yScale(q[2]), xScale(x2), yScale(upperwhisker))); g2d.draw(new Line2D.Double(xScale(x2), yScale(q[0]), xScale(x2), yScale(lowerqhisker))); } } }