All Downloads are FREE. Search and download functionalities are using the official Maven repository.

smile.plot.swing.Histogram3D Maven / Gradle / Ivy

/*
 * Copyright (c) 2010-2021 Haifeng Li. All rights reserved.
 *
 * Smile is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Smile is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Smile.  If not, see .
 */

package smile.plot.swing;

import java.awt.Color;
import smile.math.MathEx;
import smile.sort.QuickSort;

/**
 * A histogram is a graphical display of tabulated frequencies, shown as bars.
 * It shows what proportion of cases fall into each of several categories:
 * it is a form of data binning. The categories are usually specified as
 * non-overlapping intervals of some variable. The categories (bars) must
 * be adjacent. The intervals (or bands, or bins) are generally of the same
 * size, and are most easily interpreted if they are.
 *
 * @author Haifeng Li
 */
public class Histogram3D extends Plot {

    /**
     * The original data.
     */
    private double[][] data;
    /**
     * The frequencies/probabilities of bins.
     */
    private double[][] freq;
    /**
     * The location of bars.
     */
    private double[][] topNW;
    private double[][] topNE;
    private double[][] topSW;
    private double[][] topSE;
    private double[][] bottomNW;
    private double[][] bottomNE;
    private double[][] bottomSW;
    private double[][] bottomSE;
    /**
     * Z-values in camera coordinates.
     */
    private double[] zTopNW;
    private double[] zTopNE;
    private double[] zTopSW;
    private double[] zTopSE;
    private double[] zBottomNW;
    private double[] zBottomNE;
    private double[] zBottomSW;
    private double[] zBottomSE;
    /**
     * Average z-values of each surface of bars.
     */
    private double[] z;
    /**
     * Index of surfaces in descending order of z-values.
     */
    private int[] order;
    /**
     * The maximum of the frequency.
     */
    private double max;
    /**
     * The window width of values for each color.
     */
    private double width = 1.0;
    /**
     * The color palette to represent values.
     */
    private Color[] palette;

    /**
     * Constructor.
     * @param data a sample set.
     * @param xbins the number of bins on x-axis.
     * @param ybins the number of bins on y-axis.
     * @param prob if true, the z-axis will be in the probability scale.
     * Otherwise, z-axis will be in the frequency scale.
     * @param palette the color palette.
     */
    public Histogram3D(double[][] data, int xbins, int ybins, boolean prob, Color[] palette) {
        super(Color.LIGHT_GRAY);

        if (data[0].length != 2) {
            throw new IllegalArgumentException("dimension is not 2.");
        }

        this.data = data;
        this.palette = palette;

        double xmin = data[0][0];
        double xmax = data[0][0];
        double ymin = data[0][1];
        double ymax = data[0][1];
        for (int i = 1; i < data.length; i++) {
            if (xmin > data[i][0]) {
                xmin = data[i][0];
            }
            if (xmax < data[i][0]) {
                xmax = data[i][0];
            }

            if (ymin > data[i][1]) {
                ymin = data[i][1];
            }
            if (ymax < data[i][1]) {
                ymax = data[i][1];
            }
        }

        double xspan = xmax - xmin;
        double xwidth = xspan / xbins;
        double yspan = ymax - ymin;
        double ywidth = yspan / ybins;

        freq = new double[xbins * ybins][3];
        freq[0][0] = xmin + xwidth / 2;
        freq[0][1] = ymin + ywidth / 2;
        for (int i = 0; i < xbins; i++) {
            for (int j = 0; j < ybins; j++) {
                freq[j * xbins + i][0] = freq[0][0] + xwidth * i;
                freq[j * xbins + i][1] = freq[0][1] + ywidth * j;
            }
        }

        for (int k = 0; k < data.length; k++) {
            int i = (int) ((data[k][0] - xmin) / xwidth);
            if (i >= xbins) {
                i = xbins - 1;
            }

            int j = (int) ((data[k][1] - ymin) / ywidth);
            if (j >= ybins) {
                j = ybins - 1;
            }

            freq[j * xbins + i][2]++;
        }

        if (prob) {
            for (int i = 0; i < freq.length; i++) {
                freq[i][2] /= data.length;
            }
        }

        max = Double.NEGATIVE_INFINITY;
        for (int i = 0; i < freq.length; i++) {
            if (freq[i][2] > max) {
                max = freq[i][2];
            }
        }
        
        if (palette != null) {
            width = max / palette.length;
        }

        // calculate cube coordinates.
        topNW = new double[freq.length][3];
        topNE = new double[freq.length][3];
        topSW = new double[freq.length][3];
        topSE = new double[freq.length][3];
        bottomNW = new double[freq.length][3];
        bottomNE = new double[freq.length][3];
        bottomSW = new double[freq.length][3];
        bottomSE = new double[freq.length][3];
        for (int i = 0; i < freq.length; i++) {
            topNW[i][0] = freq[i][0] - xwidth / 2;
            topNW[i][1] = freq[i][1] + ywidth / 2;
            topNW[i][2] = freq[i][2];

            topNE[i][0] = freq[i][0] + xwidth / 2;
            topNE[i][1] = freq[i][1] + ywidth / 2;
            topNE[i][2] = freq[i][2];

            topSW[i][0] = freq[i][0] - xwidth / 2;
            topSW[i][1] = freq[i][1] - ywidth / 2;
            topSW[i][2] = freq[i][2];

            topSE[i][0] = freq[i][0] + xwidth / 2;
            topSE[i][1] = freq[i][1] - ywidth / 2;
            topSE[i][2] = freq[i][2];

            bottomNW[i][0] = freq[i][0] - xwidth / 2;
            bottomNW[i][1] = freq[i][1] + ywidth / 2;
            bottomNW[i][2] = 0;

            bottomNE[i][0] = freq[i][0] + xwidth / 2;
            bottomNE[i][1] = freq[i][1] + ywidth / 2;
            bottomNE[i][2] = 0;

            bottomSW[i][0] = freq[i][0] - xwidth / 2;
            bottomSW[i][1] = freq[i][1] - ywidth / 2;
            bottomSW[i][2] = 0;

            bottomSE[i][0] = freq[i][0] + xwidth / 2;
            bottomSE[i][1] = freq[i][1] - ywidth / 2;
            bottomSE[i][2] = 0;
        }

        z = new double[6 * freq.length];
        order = new int[6 * freq.length];
        zTopNW = new double[freq.length];
        zTopNE = new double[freq.length];
        zTopSW = new double[freq.length];
        zTopSE = new double[freq.length];
        zBottomNW = new double[freq.length];
        zBottomNE = new double[freq.length];
        zBottomSW = new double[freq.length];
        zBottomSE = new double[freq.length];
    }

    @Override
    public double[] getLowerBound() {
        double[] min = MathEx.colMin(data);
        double[] bound = {min[0], min[1], 0};
        return bound;
    }

    @Override
    public double[] getUpperBound() {
        double[] max = MathEx.colMax(data);
        double[] bound = {max[0], max[1], 0};
        for (int i = 0; i < freq.length; i++) {
            if (freq[i][2] > bound[2]) {
                bound[2] = freq[i][2];
            }
        }

        return bound;
    }

    @Override
    public void paint(Graphics g) {
        Projection3D p3d = (Projection3D) g.projection;

        /**
         * Calculates z-axis values in camera coordinates.
         */
        for (int i = 0; i < freq.length; i++) {
            zTopNW[i] = p3d.z(topNW[i]);
            zTopNE[i] = p3d.z(topNE[i]);
            zTopSW[i] = p3d.z(topSW[i]);
            zTopSE[i] = p3d.z(topSE[i]);
            zBottomNW[i] = p3d.z(bottomNW[i]);
            zBottomNE[i] = p3d.z(bottomNE[i]);
            zBottomSW[i] = p3d.z(bottomSW[i]);
            zBottomSE[i] = p3d.z(bottomSE[i]);
        }

        /**
         * Calculate (average) z-value for each surface.
         * Note that this is actually just sum, which is sufficient
         * for us to sort them.
         */
        for (int i = 0, k = 0; i < freq.length; i++, k += 6) {
            z[k] = (zTopNW[i] + zTopNE[i] + zTopSE[i] + zTopSW[i]);
            z[k + 1] = (zTopNW[i] + zTopNE[i] + zBottomNE[i] + zBottomNW[i]);
            z[k + 2] = (zTopSW[i] + zTopSE[i] + zBottomSE[i] + zBottomSW[i]);
            z[k + 3] = (zTopNE[i] + zTopSE[i] + zBottomSE[i] + zBottomNE[i]);
            z[k + 4] = (zTopNW[i] + zTopSW[i] + zBottomSW[i] + zBottomNW[i]);
            z[k + 5] = (zBottomNW[i] + zBottomNE[i] + zBottomSE[i] + zBottomSW[i]);
        }

        /**
         * Sorts surfaces by z-values and paint them from furthest to
         * nearest, i.e. painter's algorithm. Although the painter's
         * algorithm is computationally and conceptually much easier than
         * most alternatives, it does suffer from several flaws. The most
         * obvious example of where the painter's algorithm falls short
         * is with intersecting surfaces.
         */
        for (int i = 0; i < order.length; i++) {
            order[i] = i;
        }
        QuickSort.sort(z, order);

        for (int k : order) {
            int i = k / 6;
            if (topNW[i][2] != bottomNW[i][2]) {
                if (palette == null) {
                    g.setColor(color);
                } else {
                    int p = (int) (freq[i][2] / width);
                    if (p == palette.length) {
                        p = palette.length - 1;
                    }
                    
                    g.setColor(palette[p]);
                }

                int j = k % 6;
                switch (j) {
                    case 0:
                        g.fillPolygon(topNW[i], topNE[i], topSE[i], topSW[i]);
                        g.setColor(Color.BLACK);
                        g.drawLine(topNW[i], topNE[i]);
                        g.drawLine(topNE[i], topSE[i]);
                        g.drawLine(topSE[i], topSW[i]);
                        g.drawLine(topSW[i], topNW[i]);
                        break;
                    case 1:
                        g.fillPolygon(topNW[i], topNE[i], bottomNE[i], bottomNW[i]);
                        g.setColor(Color.BLACK);
                        g.drawLine(topNW[i], topNE[i]);
                        g.drawLine(bottomNW[i], topNW[i]);
                        g.drawLine(bottomNE[i], topNE[i]);
                        g.drawLine(bottomNE[i], bottomNW[i]);
                        break;
                    case 2:
                        g.fillPolygon(topSW[i], topSE[i], bottomSE[i], bottomSW[i]);
                        g.setColor(Color.BLACK);
                        g.drawLine(topSW[i], topSE[i]);
                        g.drawLine(bottomSW[i], topSW[i]);
                        g.drawLine(bottomSE[i], topSE[i]);
                        g.drawLine(bottomSE[i], bottomSW[i]);
                        break;
                    case 3:
                        g.fillPolygon(topNE[i], topSE[i], bottomSE[i], bottomNE[i]);
                        g.setColor(Color.BLACK);
                        g.drawLine(topNE[i], topSE[i]);
                        g.drawLine(bottomSE[i], topSE[i]);
                        g.drawLine(bottomNE[i], topNE[i]);
                        g.drawLine(bottomSE[i], bottomNE[i]);
                        break;
                    case 4:
                        g.fillPolygon(topNW[i], topSW[i], bottomSW[i], bottomNW[i]);
                        g.setColor(Color.BLACK);
                        g.drawLine(topNW[i], topSW[i]);
                        g.drawLine(bottomNW[i], topNW[i]);
                        g.drawLine(bottomSW[i], topSW[i]);
                        g.drawLine(bottomNW[i], bottomSW[i]);
                        break;
                    case 5:
                        g.fillPolygon(bottomNW[i], bottomNE[i], bottomSE[i], bottomSW[i]);
                        g.setColor(Color.BLACK);
                        g.drawLine(bottomNW[i], bottomNE[i]);
                        g.drawLine(bottomNE[i], bottomSE[i]);
                        g.drawLine(bottomSE[i], bottomSW[i]);
                        g.drawLine(bottomSW[i], bottomNW[i]);
                        break;
                }
            }
        }
    }

    /**
     * Creates a 3D histogram plot.
     * @param data a sample set.
     */
    public static Histogram3D of(double[][] data) {
        return of(data, 10, true);
    }

    /**
     * Creates a 3D histogram plot.
     * @param data a sample set.
     * @param nbins the number of bins.
     * @param palette the color palette.
     */
    public static Histogram3D of(double[][] data, int nbins, Color[] palette) {
        return new Histogram3D(data, nbins, nbins, true, palette);
    }

    /**
     * Creates a 3D histogram plot.
     * @param data a sample set.
     * @param nbins the number of bins.
     * @param prob if true, the z-axis will be in the probability scale.
     * Otherwise, z-axis will be in the frequency scale.
     */
    public static Histogram3D of(double[][] data, int nbins, boolean prob) {
        return new Histogram3D(data, nbins, nbins, prob, null);
    }

    /**
     * Creates a 3D histogram plot.
     * @param data a sample set.
     * @param nbins the number of bins.
     * @param prob if true, the z-axis will be in the probability scale.
     * Otherwise, z-axis will be in the frequency scale.
     * @param palette the color palette.
     */
    public static Histogram3D of(double[][] data, int nbins, boolean prob, Color[] palette) {
        return new Histogram3D(data, nbins, nbins, prob, palette);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy