Commit 3b5cc835 authored by Radek Ošlejšek's avatar Radek Ošlejšek
Browse files

Merge branch '299-histogram-widget' into 'master'

Interactive histogram for 3D heatmaps

See merge request grp-fidentis/analyst2!367
parents 0aa4f36f 65a6cf05
Loading
Loading
Loading
Loading
+86 −0
Original line number Diff line number Diff line
package cz.fidentis.analyst.gui.elements.histogram;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

/**
 * Class that implements bucket histogram.
 *
 * @author Jakub Nezval
 */
public class BucketHistogram {
    private final int bucketCount;
    
    private List<Integer> buckets;
    
    private double minValue;
    private double maxValue;

    /**
     * Create a new BucketHistogram that will use {@code bucketCount} buckets.
     * Number of buckets cannot be changed later.
     *
     * @param bucketCount number of buckets to be used
     */
    public BucketHistogram(int bucketCount) {
        this.bucketCount = bucketCount;
    }

    /**
     * Set values from which the bucket histogram will be calculated.
     *
     * @param values values
     * @see BucketHistogram#getBuckets()
     */
    public void setValues(Collection<Double> values) {
        this.buckets = new ArrayList<>(Collections.nCopies(this.bucketCount, 0));

        this.minValue = Double.POSITIVE_INFINITY;
        this.maxValue = Double.NEGATIVE_INFINITY;
        for (var value: values) {
            if (value < this.minValue) {
                this.minValue = value;
            }
            if (value > this.maxValue) {
                this.maxValue = value;
            }
        }

        double bucketSpan = (this.maxValue - this.minValue) / this.bucketCount;
        int bucketIndex, safeBucketIndex;
        for (var value: values) {
            bucketIndex = (int) ((value - this.minValue) / bucketSpan);
            safeBucketIndex = bucketIndex < this.bucketCount ? bucketIndex : this.bucketCount - 1;
            buckets.set(safeBucketIndex, buckets.get(safeBucketIndex) + 1);
        }
    }

    /**
     * Return buckets.
     * Integer at index {@code i} is equal to number of values that belong to that bucket range.
     *
     * @return list of buckets
     * @see BucketHistogram#setValues(Collection)
     */
    public List<Integer> getBuckets() {
        return Collections.unmodifiableList(this.buckets);
    }

    /**
     * @return minimum value from {@code values}
     * used in the last {@link BucketHistogram#setValues(Collection)} call
     */
    public double getMinimum() {
        return this.minValue;
    }

    /**
     * @return maximum value from {@code values}
     * used in the last {@link BucketHistogram#setValues(Collection)} call
     */
    public double getMaximum() {
        return this.maxValue;
    }
}
+72 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="UTF-8" ?>

<Form version="1.5" maxVersion="1.9" type="org.netbeans.modules.form.forminfo.JPanelFormInfo">
  <Properties>
    <Property name="opaque" type="boolean" value="false"/>
    <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
      <Dimension value="[15, 420]"/>
    </Property>
  </Properties>
  <AuxValues>
    <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="1"/>
    <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/>
    <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/>
    <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="true"/>
    <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="true"/>
    <AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/>
    <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/>
    <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/>
    <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/>
    <AuxValue name="designerSize" type="java.awt.Dimension" value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,1,-92,0,0,0,15"/>
  </AuxValues>

  <Layout class="org.netbeans.modules.form.compat2.layouts.DesignAbsoluteLayout">
    <Property name="useNullLayout" type="boolean" value="true"/>
  </Layout>
  <SubComponents>
    <Component class="javax.swing.JButton" name="handle">
      <Properties>
        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
          <ResourceString bundle="cz/fidentis/analyst/gui/elements/histogram/components/Bundle.properties" key="Bound.handle.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
        </Property>
        <Property name="cursor" type="java.awt.Cursor" editor="org.netbeans.modules.form.editors2.CursorEditor">
          <Color id="West Resize Cursor"/>
        </Property>
      </Properties>
      <Constraints>
        <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignAbsoluteLayout" value="org.netbeans.modules.form.compat2.layouts.DesignAbsoluteLayout$AbsoluteConstraintsDescription">
          <AbsoluteConstraints x="0" y="188" width="15" height="44"/>
        </Constraint>
      </Constraints>
    </Component>
    <Container class="javax.swing.JPanel" name="indicator">
      <Properties>
        <Property name="background" type="java.awt.Color" editor="org.netbeans.beaninfo.editors.ColorEditor">
          <Color blue="0" green="0" red="0" type="rgb"/>
        </Property>
      </Properties>
      <AuxValues>
        <AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/>
        <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/>
      </AuxValues>
      <Constraints>
        <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignAbsoluteLayout" value="org.netbeans.modules.form.compat2.layouts.DesignAbsoluteLayout$AbsoluteConstraintsDescription">
          <AbsoluteConstraints x="7" y="0" width="1" height="420"/>
        </Constraint>
      </Constraints>

      <Layout>
        <DimensionLayout dim="0">
          <Group type="103" groupAlignment="0" attributes="0">
              <EmptySpace min="0" pref="1" max="32767" attributes="0"/>
          </Group>
        </DimensionLayout>
        <DimensionLayout dim="1">
          <Group type="103" groupAlignment="0" attributes="0">
              <EmptySpace min="0" pref="420" max="32767" attributes="0"/>
          </Group>
        </DimensionLayout>
      </Layout>
    </Container>
  </SubComponents>
</Form>
+181 −0
Original line number Diff line number Diff line
package cz.fidentis.analyst.gui.elements.histogram.components;

import cz.fidentis.analyst.gui.elements.histogram.utils.ActionEmitter;

import javax.swing.JPanel;
import javax.swing.event.MouseInputAdapter;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;

/**
 * Bound/slider bean-component that is draggable along X axis.
 *
 * @author Jakub Nezval
 */
public class Bound extends JPanel {

    /*
     * Actions triggered by this panel and served by the associated action listener
     */
    public static final String ACTION_COMMAND_BOUND_MOVED = "bound moved";

    private final ActionEmitter actionEmitter = new ActionEmitter();

    private int minXCoordinate = Integer.MIN_VALUE;
    private int maxXCoordinate = Integer.MAX_VALUE;
    
    /**
     * Creates new form Bound. Bound has ⟨{@link Integer#MIN_VALUE}, {@link Integer#MAX_VALUE}⟩ range by default.
     *
     * @see Bound#setBoundRange(int, int)
     */
    public Bound() {
        initComponents();

        var dragging = new MouseInputAdapter() {
            private int xOffset;

            @Override
            public void mousePressed(MouseEvent e) {
                xOffset = e.getX();
            }

            @Override
            public void mouseDragged(MouseEvent e) {
                var newX = getX() + e.getX() - xOffset;
                if (newX < minXCoordinate) {
                    newX = minXCoordinate;
                }
                if (newX > maxXCoordinate) {
                    newX = maxXCoordinate;
                }
                setLocation(newX, 0);
                // actionTriggered(e);  // may induce some lagging
            }

            @Override
            public void mouseReleased(MouseEvent e) {
                actionTriggered(e);
            }
        };

        this.handle.addMouseListener(dragging);
        this.handle.addMouseMotionListener(dragging);
    }

    private void actionTriggered(MouseEvent e) {
        actionEmitter.triggerActionEvent(
                new ActionEvent(
                        e.getSource(),
                        ActionEvent.ACTION_PERFORMED,
                        ACTION_COMMAND_BOUND_MOVED
                )
        );
    }

    /**
     * Set constraints on movement along X axis.
     *
     * @param min minimal value bound can have
     * @param max maximal value bound can have
     * @throws IllegalArgumentException if current value is outside of ⟨{@code min}, {@code max}⟩ range
     * @see Bound#getValue()
     */
    public void setBoundRange(int min, int max) throws IllegalArgumentException {
        if (this.getValue() < min || this.getValue() > max) {
            throw new IllegalArgumentException("Bound range doesn't include current value");
        }

        this.minXCoordinate = min;
        this.maxXCoordinate = max;
    }

    /**
     * Obtain value of Bound. Equal to {@code Bound.getX()}.
     *
     * @return value from bound range
     * @see Bound#setBoundRange(int, int)
     */
    public int getValue() {
        return this.getX();
    }

    /**
     * Set Bound's value & position accordingly.
     *
     * @param value value
     * @throws IllegalArgumentException if {@code value} is outside the Bound range
     */
    public void setValue(int value) throws IllegalArgumentException {
        if (value < minXCoordinate || value > maxXCoordinate) {
            throw new IllegalArgumentException("value is outside the Bound range");
        }
        this.setLocation(value, this.getY());
    }

    /**
     * Delegate of {@link ActionEmitter}.
     *
     * @param listener listener
     * @return {@code true} if {@code listener} was successfully added
     * @see ActionEmitter#addActionListener(ActionListener)
     */
    public boolean addActionListener(ActionListener listener) {
        return this.actionEmitter.addActionListener(listener);
    }

    /**
     * Delegate of {@link ActionEmitter}.
     *
     * @param listener listener
     * @return {@code true} if {@code listener} was successfully removed
     * @see ActionEmitter#removeActionListener(ActionListener) 
     */
    public boolean removeActionListener(ActionListener listener) {
        return this.actionEmitter.removeActionListener(listener);
    }

    /**
     * This method is called from within the constructor to initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is always
     * regenerated by the Form Editor.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        handle = new javax.swing.JButton();
        javax.swing.JPanel indicator = new javax.swing.JPanel();

        setOpaque(false);
        setPreferredSize(new java.awt.Dimension(15, 420));
        setLayout(null);

        org.openide.awt.Mnemonics.setLocalizedText(handle, org.openide.util.NbBundle.getMessage(Bound.class, "Bound.handle.text")); // NOI18N
        handle.setCursor(new java.awt.Cursor(java.awt.Cursor.W_RESIZE_CURSOR));
        add(handle);
        handle.setBounds(0, 188, 15, 44);

        indicator.setBackground(new java.awt.Color(0, 0, 0));

        javax.swing.GroupLayout indicatorLayout = new javax.swing.GroupLayout(indicator);
        indicator.setLayout(indicatorLayout);
        indicatorLayout.setHorizontalGroup(
            indicatorLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGap(0, 1, Short.MAX_VALUE)
        );
        indicatorLayout.setVerticalGroup(
            indicatorLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGap(0, 420, Short.MAX_VALUE)
        );

        add(indicator);
        indicator.setBounds(7, 0, 1, 420);
    }// </editor-fold>//GEN-END:initComponents


    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JButton handle;
    // End of variables declaration//GEN-END:variables
}
+84 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="UTF-8" ?>

<Form version="1.9" maxVersion="1.9" type="org.netbeans.modules.form.forminfo.JPanelFormInfo">
  <Properties>
    <Property name="opaque" type="boolean" value="false"/>
  </Properties>
  <AuxValues>
    <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="1"/>
    <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/>
    <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/>
    <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="true"/>
    <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="true"/>
    <AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/>
    <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/>
    <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/>
    <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/>
  </AuxValues>

  <Layout>
    <DimensionLayout dim="0">
      <Group type="103" groupAlignment="0" attributes="0">
          <Group type="102" alignment="0" attributes="0">
              <Component id="layers" min="-2" max="-2" attributes="0"/>
              <EmptySpace min="0" pref="0" max="32767" attributes="0"/>
          </Group>
      </Group>
    </DimensionLayout>
    <DimensionLayout dim="1">
      <Group type="103" groupAlignment="0" attributes="0">
          <Component id="layers" alignment="0" pref="420" max="32767" attributes="0"/>
      </Group>
    </DimensionLayout>
  </Layout>
  <SubComponents>
    <Container class="javax.swing.JLayeredPane" name="layers">
      <Properties>
        <Property name="focusable" type="boolean" value="false"/>
        <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
          <Dimension value="[600, 300]"/>
        </Property>
      </Properties>

      <Layout class="org.netbeans.modules.form.compat2.layouts.DesignAbsoluteLayout">
        <Property name="useNullLayout" type="boolean" value="true"/>
      </Layout>
      <SubComponents>
        <Component class="cz.fidentis.analyst.gui.elements.histogram.components.Bound" name="lowerBound">
          <Constraints>
            <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignAbsoluteLayout" value="org.netbeans.modules.form.compat2.layouts.DesignAbsoluteLayout$AbsoluteConstraintsDescription">
              <AbsoluteConstraints x="0" y="0" width="15" height="420"/>
            </Constraint>
          </Constraints>
        </Component>
        <Component class="cz.fidentis.analyst.gui.elements.histogram.components.Bound" name="higherBound">
          <Constraints>
            <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignAbsoluteLayout" value="org.netbeans.modules.form.compat2.layouts.DesignAbsoluteLayout$AbsoluteConstraintsDescription">
              <AbsoluteConstraints x="585" y="0" width="15" height="420"/>
            </Constraint>
          </Constraints>
        </Component>
        <Container class="cz.fidentis.analyst.gui.elements.histogram.components.LineChartAreaPanel" name="chart">
          <Constraints>
            <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignAbsoluteLayout" value="org.netbeans.modules.form.compat2.layouts.DesignAbsoluteLayout$AbsoluteConstraintsDescription">
              <AbsoluteConstraints x="8" y="5" width="584" height="410"/>
            </Constraint>
          </Constraints>

          <Layout>
            <DimensionLayout dim="0">
              <Group type="103" groupAlignment="0" attributes="0">
                  <EmptySpace min="0" pref="584" max="32767" attributes="0"/>
              </Group>
            </DimensionLayout>
            <DimensionLayout dim="1">
              <Group type="103" groupAlignment="0" attributes="0">
                  <EmptySpace min="0" pref="410" max="32767" attributes="0"/>
              </Group>
            </DimensionLayout>
          </Layout>
        </Container>
      </SubComponents>
    </Container>
  </SubComponents>
</Form>
+189 −0
Original line number Diff line number Diff line
/*
 * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
 * Click nbfs://nbhost/SystemFileSystem/Templates/GUIForms/JPanel.java to edit this template
 */
package cz.fidentis.analyst.gui.elements.histogram.components;

import cz.fidentis.analyst.gui.elements.histogram.utils.ActionEmitter;
import cz.fidentis.analyst.gui.elements.histogram.BucketHistogram;
import cz.fidentis.analyst.gui.elements.histogram.utils.Mapping;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Collection;
import javax.swing.JPanel;

/**
 * High level interactive histogram bean-component.
 *
 * @author Jakub Nezval
 */
public class HistogramComponent extends JPanel {

    /*
     * Actions triggered by this panel and served by the associated action listener
     */
    public static final String ACTION_COMMAND_BOUNDS_MOVED = "histogram bounds moved";
    
    private final ActionEmitter actionEmitter = new ActionEmitter();
    private final BucketHistogram bucketHistogram;
    
    private final int minBoundValue, maxBoundValue;
    
    /**
     * Creates new form HistogramComponent.
     */
    public HistogramComponent() {
        initComponents();
        this.bucketHistogram = new BucketHistogram(this.chart.getWidth() + 1);
        
        this.minBoundValue = this.lowerBound.getValue();
        this.maxBoundValue = this.higherBound.getValue();
        
        this.lowerBound.setBoundRange(this.minBoundValue, this.maxBoundValue);
        this.higherBound.setBoundRange(this.minBoundValue, this.maxBoundValue);

        var boundListener = new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                if (lowerBound.getValue() > higherBound.getValue()) {
                    var tmp = lowerBound;
                    lowerBound = higherBound;
                    higherBound = tmp;
                }

                actionEmitter.triggerActionEvent(
                        new ActionEvent(
                                e.getSource(),
                                ActionEvent.ACTION_PERFORMED,
                                HistogramComponent.ACTION_COMMAND_BOUNDS_MOVED
                        )
                );
            }
        };

        this.lowerBound.addActionListener(boundListener);
        this.higherBound.addActionListener(boundListener);
    }

    /**
     * Set values to be displayed in the histogram.
     *
     * @param values values
     */
    public void setValues(Collection<Double> values) {
        this.bucketHistogram.setValues(values);
        this.chart.setValues(this.bucketHistogram.getBuckets());
    }

    private double mapToHistogramValues(int value) {
        return Mapping.linear(
                value,
                this.minBoundValue + 1, this.maxBoundValue - 1,
                this.bucketHistogram.getMinimum(), this.bucketHistogram.getMaximum()
        );
    }

    /**
     * @return lower value from the duo of bound-sliders
     */
    public double getLowerBoundValue() {
        return this.mapToHistogramValues(this.lowerBound.getValue());
    }

    /**
     * @return higher value from the duo of bound-sliders
     */
    public double getHigherBoundValue() {
        return this.mapToHistogramValues(this.higherBound.getValue());
    }

    /**
     * Delegate of {@link ActionEmitter}.
     *
     * @param listener listener
     * @return {@code true} if {@code listener} was successfully added
     * @see ActionEmitter#addActionListener(ActionListener)
     */
    public boolean addActionListener(ActionListener listener) {
        return this.actionEmitter.addActionListener(listener);
    }

    /**
     * Delegate of {@link ActionEmitter}.
     *
     * @param listener listener
     * @return {@code true} if {@code listener} was successfully removed
     * @see ActionEmitter#removeActionListener(ActionListener)
     */
    public boolean removeActionListener(ActionListener listener) {
        return this.actionEmitter.removeActionListener(listener);
    }

    /**
     * Moves bounds to their original position.
     */
    public void resetBounds() {
        this.lowerBound.setValue(this.minBoundValue);
        this.higherBound.setValue(this.maxBoundValue);
    }

    /**
     * This method is called from within the constructor to initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is always
     * regenerated by the Form Editor.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        layers = new javax.swing.JLayeredPane();
        lowerBound = new cz.fidentis.analyst.gui.elements.histogram.components.Bound();
        higherBound = new cz.fidentis.analyst.gui.elements.histogram.components.Bound();
        chart = new cz.fidentis.analyst.gui.elements.histogram.components.LineChartAreaPanel();

        setOpaque(false);

        layers.setFocusable(false);
        layers.setPreferredSize(new java.awt.Dimension(600, 300));
        layers.add(lowerBound);
        lowerBound.setBounds(0, 0, 15, 420);
        layers.add(higherBound);
        higherBound.setBounds(585, 0, 15, 420);

        javax.swing.GroupLayout chartLayout = new javax.swing.GroupLayout(chart);
        chart.setLayout(chartLayout);
        chartLayout.setHorizontalGroup(
            chartLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGap(0, 584, Short.MAX_VALUE)
        );
        chartLayout.setVerticalGroup(
            chartLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGap(0, 410, Short.MAX_VALUE)
        );

        layers.add(chart);
        chart.setBounds(8, 5, 584, 410);

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
        this.setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addComponent(layers, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addGap(0, 0, Short.MAX_VALUE))
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addComponent(layers, javax.swing.GroupLayout.DEFAULT_SIZE, 420, Short.MAX_VALUE)
        );
    }// </editor-fold>//GEN-END:initComponents


    // Variables declaration - do not modify//GEN-BEGIN:variables
    private cz.fidentis.analyst.gui.elements.histogram.components.LineChartAreaPanel chart;
    private cz.fidentis.analyst.gui.elements.histogram.components.Bound higherBound;
    private javax.swing.JLayeredPane layers;
    private cz.fidentis.analyst.gui.elements.histogram.components.Bound lowerBound;
    // End of variables declaration//GEN-END:variables
}
Loading