package org.basex.gui.view.plot;
import static org.basex.core.Text.*;
import static org.basex.gui.GUIConstants.*;
import static org.basex.util.Token.*;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Transparency;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.util.Arrays;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.DefaultComboBoxModel;
import javax.swing.SwingUtilities;
import org.basex.data.Data;
import org.basex.data.Nodes;
import org.basex.gui.GUIConstants.Fill;
import org.basex.gui.GUIProp;
import org.basex.gui.layout.BaseXBack;
import org.basex.gui.layout.BaseXCheckBox;
import org.basex.gui.layout.BaseXCombo;
import org.basex.gui.layout.BaseXLabel;
import org.basex.gui.layout.BaseXLayout;
import org.basex.gui.layout.BaseXPopup;
import org.basex.gui.layout.BaseXSlider;
import org.basex.gui.view.View;
import org.basex.gui.view.ViewNotifier;
import org.basex.gui.view.ViewRect;
import org.basex.index.StatsType;
import org.basex.util.list.IntList;
/**
* A scatter plot visualization of the database.
*
* @author BaseX Team 2005-12, BSD License
* @author Lukas Kircher
*/
public final class PlotView extends View {
/** Whitespace between captions. */
static final int CAPTIONWHITESPACE = 10;
/** Rotate factor. */
private static final double ROTATE = Math.sin(30);
/** Plot margin: top, left, bottom, right margin. */
private static final int[] MARGIN = new int[4];
/** Maximum length of axis caption text. */
private static final int MAXL = 11;
/** Position where over-length text is cut off. */
private static final int CUTOFF = 10;
/** X axis selector. */
final BaseXCombo xCombo;
/** Y axis selector. */
final BaseXCombo yCombo;
/** Item selector combo. */
final BaseXCombo itemCombo;
/** Dot size in plot view. */
final BaseXSlider dots;
/** Data reference. */
PlotData plotData;
/** Keeps track of changes in the plot. */
boolean plotChanged;
/** Indicates if global marked nodes should be drawn. */
boolean drawSubNodes;
/** Indicates if the buffered image for marked nodes has to be redrawn. */
boolean markingChanged;
/** Logarithmic display. */
private final BaseXCheckBox xLog;
/** Logarithmic display. */
private final BaseXCheckBox yLog;
/** Bounding box which supports selection of multiple items. */
private final ViewRect selectionBox;
/** Item image. */
private BufferedImage itemImg;
/** Marked item image. */
private BufferedImage itemImgMarked;
/** Focused item image. */
private BufferedImage itemImgFocused;
/** Child node of marked node. */
private BufferedImage itemImgSub;
/** Buffered plot image. */
private BufferedImage plotImg;
/** Buffered image of marked items. */
private BufferedImage markedImg;
/** X coordinate of mouse pointer. */
private int mouseX;
/** Y coordinate of mouse pointer. */
private int mouseY;
/** Current plot height. */
private int plotHeight;
/** Current plot width. */
private int plotWidth;
/** Flag for mouse dragging actions. */
private boolean dragging;
/** Indicates if a filter operation is self implied or was triggered by
* another view. */
private boolean rightClick;
/** Context which is displayed in the plot after a context change which was
* triggered by the plot itself. */
private Nodes nextContext;
/**
* Default constructor.
* @param man view manager
*/
public PlotView(final ViewNotifier man) {
super(PLOTVIEW, man);
border(5).layout(new BorderLayout());
final BaseXBack panel = new BaseXBack(Fill.NONE).layout(new BorderLayout());
Box box = new Box(BoxLayout.X_AXIS);
xLog = new BaseXCheckBox(PLOTLOG, false, gui);
xLog.setSelected(gui.gprop.is(GUIProp.PLOTXLOG));
xLog.addActionListener(new ActionListener() {
@Override
public void actionPerformed(final ActionEvent e) {
gui.gprop.invert(GUIProp.PLOTXLOG);
refreshUpdate();
}
});
dots = new BaseXSlider(new ActionListener() {
@Override
public void actionPerformed(final ActionEvent e) {
gui.gprop.set(GUIProp.PLOTDOTS, dots.value());
refreshLayout();
}
}, -6, 6, gui.gprop.num(GUIProp.PLOTDOTS), gui);
BaseXLayout.setWidth(dots, 40);
yLog = new BaseXCheckBox(PLOTLOG, false, gui);
yLog.setSelected(gui.gprop.is(GUIProp.PLOTYLOG));
yLog.addActionListener(new ActionListener() {
@Override
public void actionPerformed(final ActionEvent e) {
gui.gprop.invert(GUIProp.PLOTYLOG);
refreshUpdate();
}
});
box.add(yLog);
box.add(Box.createHorizontalGlue());
box.add(dots);
box.add(Box.createHorizontalGlue());
box.add(xLog);
panel.add(box, BorderLayout.NORTH);
box = new Box(BoxLayout.X_AXIS);
xCombo = new BaseXCombo(gui);
xCombo.addActionListener(new ActionListener() {
@Override
public void actionPerformed(final ActionEvent e) {
setAxis(plotData.xAxis, xCombo);
}
});
yCombo = new BaseXCombo(gui);
yCombo.addActionListener(new ActionListener() {
@Override
public void actionPerformed(final ActionEvent e) {
setAxis(plotData.yAxis, yCombo);
}
});
itemCombo = new BaseXCombo(gui);
itemCombo.addActionListener(new ActionListener() {
@Override
public void actionPerformed(final ActionEvent e) {
final String item = (String) itemCombo.getSelectedItem();
plotData.xAxis.log = gui.gprop.is(GUIProp.PLOTXLOG);
plotData.yAxis.log = gui.gprop.is(GUIProp.PLOTYLOG);
if(plotData.setItem(item)) {
plotChanged = true;
markingChanged = true;
final String[] keys =
plotData.getCategories(token(item)).toStringArray();
xCombo.setModel(new DefaultComboBoxModel(keys));
yCombo.setModel(new DefaultComboBoxModel(keys));
if(keys.length > 0) {
// choose name category as default for horizontal axis
xCombo.setSelectedIndex(Math.min(1, keys.length));
yCombo.setSelectedIndex(0);
}
}
drawSubNodes = true;
markingChanged = true;
repaint();
}
});
box.add(yCombo);
box.add(Box.createHorizontalStrut(3));
box.add(new BaseXLabel("Y"));
box.add(Box.createHorizontalGlue());
box.add(itemCombo);
box.add(Box.createHorizontalGlue());
box.add(new BaseXLabel("X"));
box.add(Box.createHorizontalStrut(3));
box.add(xCombo);
panel.add(box, BorderLayout.SOUTH);
add(panel, BorderLayout.SOUTH);
new BaseXPopup(this, POPUP);
selectionBox = new ViewRect();
refreshLayout();
}
/**
* Changes the axis assignment.
* @param ax plot axis
* @param cb combo box
*/
void setAxis(final PlotAxis ax, final BaseXCombo cb) {
final String cs = (String) cb.getSelectedItem();
if(!ax.setAxis(cs)) return;
plotChanged = true;
markingChanged = true;
repaint();
// prevent both combo boxes to show the same category
final BaseXCombo ocb = cb == xCombo ? yCombo : xCombo;
if(cs.equals(ocb.getSelectedItem())) {
final int i = ocb.getSelectedIndex();
ocb.setSelectedIndex(i > 0 ? i - 1 : i + 1);
}
}
/**
* Creates a buffered image for items.
* @param focus create image of focused item if true
* @param marked create image of marked item
* @param markedSub child node of marked node
* @return item image
*/
private BufferedImage itemImage(final boolean focus,
final boolean marked, final boolean markedSub) {
final int size = Math.max(1, gui.gprop.num(GUIProp.FONTSIZE) +
gui.gprop.num(GUIProp.PLOTDOTS) - (focus ? 2 :
marked || markedSub ? 4 : 6));
final BufferedImage img = new BufferedImage(size, size,
Transparency.TRANSLUCENT);
final Graphics g = img.getGraphics();
smooth(g);
Color c = color1A;
if(marked) c = colormark1A;
if(markedSub) c = colormark2A;
if(focus) c = color4;
g.setColor(c);
g.fillOval(0, 0, size, size);
return img;
}
/**
* Precalculates the plot and returns the result as buffered image.
*/
private void createPlotImage() {
plotImg = new BufferedImage(getWidth(), getHeight(),
Transparency.BITMASK);
final Graphics g = plotImg.getGraphics();
smooth(g);
// draw axis and grid
drawAxis(g, true);
drawAxis(g, false);
// draw items
g.setColor(color4);
for(int i = 0; i < plotData.pres.length; ++i) {
drawItem(g, plotData.xAxis.co[i],
plotData.yAxis.co[i], false, false, false);
}
}
@Override
public void paintComponent(final Graphics g) {
super.paintComponent(g);
if(plotData == null) {
refreshInit();
return;
}
final int w = getWidth();
final int h = getHeight();
plotWidth = w - (MARGIN[1] + MARGIN[3]);
plotHeight = h - (MARGIN[0] + MARGIN[2]);
final int sz = sizeFactor();
g.setFont(font);
g.setColor(Color.black);
final Data data = gui.context.data();
final boolean nd = data == null || !data.meta.pathindex;
if(nd || plotWidth - sz < 0 || plotHeight - sz < 0) {
BaseXLayout.drawCenter(g, nd ? NO_DATA : NO_SPACE, w, h / 2 - MARGIN[0]);
return;
}
// draw buffered plot image
if(plotImg == null || plotChanged) createPlotImage();
g.drawImage(plotImg, 0, 0, this);
// draw buffered image of marked items
if(markingChanged || markedImg == null) createMarkedNodes();
g.drawImage(markedImg, 0, 0, this);
if(plotData.pres.length < 1) return;
gui.painting = true;
/*
* Possibly, the focused node is not shown in this view (if it was focused
* in another view). In this case, the ancestor with the smallest distance
* to the focused node is selected.
*/
int focused = gui.context.focused;
if(focused != -1) {
final int itmID = data.tagindex.id(plotData.item);
int k = data.kind(focused);
int name = data.name(focused);
while(focused > 0 && itmID != name) {
focused = data.parent(focused, k);
if(focused > -1) {
k = data.kind(focused);
name = data.name(focused);
}
}
}
// draw focused item
final int f = plotData.findPre(focused);
if(f > -1) {
// determine number of overlapping nodes (plotting second)
final int ol = overlappingNodes(f).length;
if(!dragging) {
final double x1 = plotData.xAxis.co[f];
final double y1 = plotData.yAxis.co[f];
drawItem(g, x1, y1, true, false, false);
// draw focused x and y value
g.setFont(font);
final int textH = g.getFontMetrics().getHeight();
final String x = formatString(true, focused);
final String y = formatString(false, focused);
String label = x.length() > 16 ? x.substring(0, 14) + ".." : x;
if(!x.isEmpty() && !y.isEmpty()) label += " | ";
label += y.length() > 16 ? y.substring(0, 14) + ".." : y;
final int xa = calcCoordinate(true, x1) + 15;
int ya = calcCoordinate(false, y1) + gui.gprop.num(GUIProp.PLOTDOTS);
final int ww = getWidth();
final byte[] nm = data.attValue(data.nameID, focused);
String name = nm != null ? string(nm) : "";
if(!name.isEmpty() && plotData.xAxis.attrID != data.nameID &&
plotData.yAxis.attrID != data.nameID) {
if(ol > 1) name = ol + "x: " + name + ", ...";
final int lw = BaseXLayout.width(g, label);
if(ya < MARGIN[0] + textH && xa < w - lw) {
ya += 2 * textH - gui.gprop.num(GUIProp.PLOTDOTS);
}
if(xa > w - lw)
BaseXLayout.drawTooltip(g, name + COLS + label, xa, ya, ww, 10);
else {
BaseXLayout.drawTooltip(g, name, xa, ya - textH, ww, 10);
BaseXLayout.drawTooltip(g, label, xa, ya, ww, 10);
}
} else {
if(ol > 1) label = label.isEmpty() ? ol + "x" :
ol + "x: " + label + ", ...";
BaseXLayout.drawTooltip(g, label, xa, ya, ww, 10);
}
}
}
// draw selection box
if(dragging) {
g.setColor(color2A);
final int selW = selectionBox.w;
final int selH = selectionBox.h;
final int x1 = selectionBox.x;
final int y1 = selectionBox.y;
g.fillRect(selW > 0 ? x1 : x1 + selW, selH > 0 ? y1 : y1 + selH,
Math.abs(selW), Math.abs(selH));
g.setColor(color3A);
g.drawRect(selW > 0 ? x1 : x1 + selW, selH > 0 ? y1 : y1 + selH,
Math.abs(selW), Math.abs(selH));
}
markingChanged = false;
plotChanged = false;
gui.painting = false;
}
/**
* Draws marked nodes.
*/
private void createMarkedNodes() {
final Data data = gui.context.data();
markedImg = new BufferedImage(getWidth(), getHeight(),
Transparency.BITMASK);
final Graphics gi = markedImg.getGraphics();
smooth(gi);
final Nodes marked = gui.context.marked;
if(marked.size() == 0) return;
final int[] m = Arrays.copyOf(marked.list, marked.list.length);
int i = 0;
// no child nodes of the marked context nodes are marked
if(!drawSubNodes) {
while(i < m.length) {
final int pi = plotData.findPre(m[i]);
if(pi > -1) drawItem(gi, plotData.xAxis.co[pi],
plotData.yAxis.co[pi], false, true, false);
++i;
}
return;
}
// if nodes are marked in another view, the given nodes as well as their
// descendants are checked for intersection with the nodes displayed in
// the plot
Arrays.sort(m);
final int[] p = plotData.pres;
int k = plotData.findPre(m[0]);
if(k > -1) {
drawItem(gi, plotData.xAxis.co[k], plotData.yAxis.co[k], false,
true, false);
++k;
} else {
k = -k;
--k;
}
// context change (triggered by another view).
// descendants of marked node set are also checked for intersection
// with currently plotted nodes
while(i < m.length && k < p.length) {
final int a = m[i];
final int b = p[k];
final int ns = data.size(a, data.kind(a)) - 1;
if(a == b) {
drawItem(gi, plotData.xAxis.co[k], plotData.yAxis.co[k], false,
true, false);
++k;
} else if(a + ns >= b) {
if(a < b) drawItem(gi, plotData.xAxis.co[k], plotData.yAxis.co[k],
false, false, true);
++k;
} else {
++i;
}
}
}
/**
* Draws a plot item at the given position. If the item to be drawn is
* focused the focused item buffered image is used.
* @param g graphics reference
* @param x x coordinate
* @param y y coordinate
* @param focus a focused item is drawn
* @param marked item is marked
* @param sub item is a child of a marked node
*/
private void drawItem(final Graphics g, final double x, final double y,
final boolean focus, final boolean marked, final boolean sub) {
final int x1 = calcCoordinate(true, x);
final int y1 = calcCoordinate(false, y);
final BufferedImage img = focus ? itemImgFocused : marked ?
itemImgMarked : sub ? itemImgSub : itemImg;
final int size = img.getWidth() / 2;
g.drawImage(img, x1 - size, y1 - size, this);
}
/**
* Draws the x axis of the plot.
* @param g graphics reference
* @param drawX drawn axis is x axis
*/
private void drawAxis(final Graphics g, final boolean drawX) {
g.setColor(color2A);
final int sz = sizeFactor();
// the painting space provided for items which lack no value
final int pWidth = plotWidth - sz;
final int pHeight = plotHeight - sz;
final PlotAxis axis = drawX ? plotData.xAxis : plotData.yAxis;
// drawing horizontal axis line
if(drawX) {
if(plotChanged) {
if(plotData.pres.length > 0) axis.calcCaption(pWidth);
final StatsType type = plotData.xAxis.type;
xLog.setEnabled((type == StatsType.DOUBLE ||
type == StatsType.INTEGER) &&
Math.abs(axis.min - axis.max) >= 1);
}
} else {
// drawing vertical axis line
if(plotChanged) {
if(plotData.pres.length > 0) axis.calcCaption(pHeight);
final StatsType type = plotData.yAxis.type;
yLog.setEnabled((type == StatsType.DOUBLE ||
type == StatsType.INTEGER) &&
Math.abs(axis.min - axis.max) >= 1);
}
}
if(plotData.pres.length < 1) {
drawCaptionAndGrid(g, drawX, "", 0);
drawCaptionAndGrid(g, drawX, "", 1);
return;
}
// getting some axis specific data
final StatsType type = axis.type;
final int nrCaptions = axis.nrCaptions;
final double step = axis.actlCaptionStep;
final double capRange = 1.0d / (nrCaptions - 1);
g.setFont(font);
// draw axis and assignment for TEXT data
if(type == StatsType.TEXT) {
final int nrCats = axis.nrCats;
final double[] coSorted = Arrays.copyOf(axis.co, axis.co.length);
// draw min / max caption
drawCaptionAndGrid(g, drawX, nrCats > 1 ? string(axis.firstCat) : "", 0);
drawCaptionAndGrid(g, drawX, nrCats > 1 ? string(axis.lastCat) : "", 1);
// return if insufficient plot space
if(nrCaptions == 0) return;
// get sorted axis item coordinates
Arrays.sort(coSorted);
// optimum caption position
double op = capRange;
final int cl = coSorted.length;
int i = 0;
// find first non .0d coordinate value
while(i < cl && coSorted[i] == 0) ++i;
// find nearest position for next axis caption
while(i < cl && op < 1.0d - 0.4d * capRange) {
if(coSorted[i] > op) {
final double distL = Math.abs(coSorted[i - 1] - op);
final double distG = Math.abs(coSorted[i] - op);
op = distL < distG ? coSorted[i - 1] : coSorted[i];
int j = 0;
// find value for given plot position
while(j < axis.co.length && axis.co[j] != op) ++j;
drawCaptionAndGrid(g, drawX,
string(axis.getValue(plotData.pres[j])), op);
// increase to next optimum caption position
op += capRange;
}
++i;
}
if(nrCats == 1) {
op = .5d;
int j = 0;
// find value for given plot position
while(j < axis.co.length && axis.co[j] != op) ++j;
drawCaptionAndGrid(g, drawX,
string(axis.getValue(plotData.pres[j])), op);
}
// axis is drawn for numerical data, type INT/DBL
} else {
final boolean noRange = axis.max - axis.min == 0;
// draw min and max grid line
drawIntermediateGridLine(g, drawX, 0, null);
drawIntermediateGridLine(g, drawX, 1, null);
// return if insufficient plot space
if(nrCaptions == 0) return;
// if min equal max, draw min in plot middle
if(noRange) {
drawCaptionAndGrid(g, drawX, BaseXLayout.value(axis.min), .5d);
return;
}
int c = 0;
// draw LOGARITHMIC SCALE
if(axis.log) {
int l;
double a;
double b;
// draw labels for negative values
if(axis.min < 0) {
l = 0;
a = -1;
while(a >= axis.min) {
if(a <= axis.max && adequateDistance(drawX, a, 0)) {
drawCaptionAndGrid(g, drawX, BaseXLayout.value(a),
axis.calcPosition(a));
}
final int lim = (int) (-1 * Math.pow(10, l + 1));
double last = a;
b = 2 * a;
while(b > lim && b >= axis.min) {
if(adequateDistance(drawX, last, b) &&
adequateDistance(drawX, lim, b) &&
b < axis.max) {
drawIntermediateGridLine(g, drawX, axis.calcPosition(b),
BaseXLayout.value(b));
last = b;
}
b += a;
}
++l;
a = -1 * Math.pow(10, l);
}
}
// draw 0 label if necessary
if(0 >= axis.min && 0 <= axis.max)
drawCaptionAndGrid(g, drawX, BaseXLayout.value(0),
axis.calcPosition(0));
// draw labels > 0
if(axis.max > 0) {
l = 0;
a = 1;
while(a <= axis.max) {
if(a >= axis.min && adequateDistance(drawX, a, 0)) {
drawCaptionAndGrid(g, drawX, BaseXLayout.value(a),
axis.calcPosition(a));
}
final int lim = (int) Math.pow(10, l + 1);
double last = a;
b = 2 * a;
while(b < lim && b <= axis.max) {
if(adequateDistance(drawX, last, b) &&
adequateDistance(drawX, lim, b) &&
b > axis.min) {
drawIntermediateGridLine(g, drawX, axis.calcPosition(b),
BaseXLayout.value(b));
last = b;
}
b += a;
}
++l;
a = Math.pow(10, l);
}
}
// draw LINEAR SCALE
} else {
// draw captions between min and max
double d = axis.calcPosition(axis.startvalue);
double f = axis.startvalue;
while(d < 1.0d - .25d / nrCaptions) {
++c;
drawCaptionAndGrid(g, drawX, BaseXLayout.value(f), d);
f += step;
d = axis.calcPosition(f);
}
// draw min/max labels if little space available
if(c < 2) {
drawCaptionAndGrid(g, drawX, BaseXLayout.value(axis.min), 0.0);
drawCaptionAndGrid(g, drawX, BaseXLayout.value(axis.max), 1.0);
}
}
}
}
/**
* Determines if two points on the axis have an adequate distance.
* @param drawX drawX
* @param a first point
* @param b second point
* @return a and b have adequate distance
*/
private boolean adequateDistance(final boolean drawX, final double a,
final double b) {
final double t = drawX ? 1.8d : 1.3d;
final PlotAxis axis = drawX ? plotData.xAxis : plotData.yAxis;
return Math.abs(calcCoordinate(drawX, axis.calcPosition(a)) -
calcCoordinate(drawX, axis.calcPosition(b))) >= sizeFactor() / t;
}
/**
* Draws an axis caption to the specified position.
* @param g graphics reference
* @param drawX draw caption on x axis
* @param caption given caption string
* @param d relative position in plot view depending on axis
*/
private void drawCaptionAndGrid(final Graphics g, final boolean drawX,
final String caption, final double d) {
String cap = caption;
// if label is too long, it is is chopped to the first characters
if(cap.length() > MAXL) cap = cap.substring(0, CUTOFF) + "..";
final int pos = calcCoordinate(drawX, d);
final int h = getHeight();
final int w = getWidth();
final int textH = g.getFontMetrics().getHeight();
final int fs = gui.gprop.num(GUIProp.FONTSIZE);
final int imgW = BaseXLayout.width(g, cap) + fs;
final BufferedImage img = createCaptionImage(g, cap, false, imgW);
// ... after that
// the image and the grid line are drawn beside x / y axis
g.setColor(color2A);
if(drawX) {
final int y = h - MARGIN[2];
g.drawImage(img, pos - imgW + textH - fs + 3, y, this);
g.drawLine(pos, MARGIN[0], pos, y + fs / 2);
g.drawLine(pos - 1, MARGIN[0], pos - 1, y + fs / 2);
} else {
g.drawImage(img, MARGIN[1] - imgW - fs / 2, pos - fs, this);
g.drawLine(MARGIN[1] - fs / 2, pos, w - MARGIN[3], pos);
g.drawLine(MARGIN[1] - fs / 2, pos + 1, w - MARGIN[3], pos + 1);
}
}
/**
* Creates a buffered image for a given string which serves as axis caption.
* @param g Graphics reference
* @param caption caption string
* @param im intermediate caption (lighter color)
* @param imgW image width
* @return buffered image
*/
private BufferedImage createCaptionImage(final Graphics g,
final String caption, final boolean im, final int imgW) {
final int textH = g.getFontMetrics().getHeight();
final int fs = gui.gprop.num(GUIProp.FONTSIZE);
// caption labels are rotated, for both x and y axis. first a buffered
// image is created which displays the rotated label ...
final int imgH = 160;
final BufferedImage img = new BufferedImage(imgW, imgH,
Transparency.BITMASK);
final Graphics2D g2d = img.createGraphics();
smooth(g2d);
g2d.rotate(ROTATE, imgW, textH);
g2d.setFont(font);
g2d.setColor(im ? color3 : Color.black);
g2d.drawString(caption, fs, fs);
return img;
}
/**
* Draws intermediate grid lines without caption.
* @param g Graphics reference
* @param drawX draw line for x axis
* @param d relative position of grid line
* @param caption caption to draw. if cap = null, no caption is drawn
*/
private void drawIntermediateGridLine(final Graphics g, final boolean drawX,
final double d, final String caption) {
String cap = caption;
final int pos = calcCoordinate(drawX, d);
final int h = getHeight();
final int w = getWidth();
final int fs = gui.gprop.num(GUIProp.FONTSIZE);
final int sf = sizeFactor();
g.setColor(color2A);
if(cap != null) {
if(cap.length() > MAXL) cap = cap.substring(0, CUTOFF) + "..";
final int textH = g.getFontMetrics().getHeight();
final int imgW = BaseXLayout.width(g, cap) + fs;
final BufferedImage img = createCaptionImage(g, cap, true, imgW);
final int y = h - MARGIN[2];
if(drawX) {
g.drawImage(img, pos - imgW + textH - fs + 3, y - textH / 3, this);
g.drawLine(pos, MARGIN[0], pos, h - MARGIN[2]);
} else {
g.drawImage(img, MARGIN[1] - imgW - fs / 2, pos - fs, this);
g.drawLine(MARGIN[1], pos, w - MARGIN[3], pos);
}
} else {
if(drawX) {
g.drawLine(pos, MARGIN[0], pos, h - MARGIN[2] - sf);
} else {
g.drawLine(MARGIN[1] + sf, pos, w - MARGIN[3], pos);
}
}
}
/**
* Returns a coordinate for a specific double value of an item.
* @param d relative coordinate of specific item
* @param drawX calculated value is x value
* @return absolute coordinate
*/
private int calcCoordinate(final boolean drawX, final double d) {
final int sz = sizeFactor();
if(drawX) {
// items with value -1 lack a value for the specific attribute
if(d == -1) return (int) (MARGIN[1] + sz * .35d);
final int width = getWidth();
final int xSpace = width - (MARGIN[1] + MARGIN[3]) - sz;
return (int) (d * xSpace) + MARGIN[1] + sz;
}
final int height = getHeight();
if(d == -1) return height - MARGIN[2] - sz / 4;
final int ySpace = height - (MARGIN[0] + MARGIN[2]) - sz;
return ySpace - (int) (d * ySpace) + MARGIN[0];
}
@Override
public void refreshContext(final boolean more, final boolean quick) {
// all plot data is recalculated, assignments stay the same
plotData.refreshItems(nextContext != null && more && rightClick ?
nextContext : gui.context.current(), !more || !rightClick);
plotData.xAxis.log = gui.gprop.is(GUIProp.PLOTXLOG);
plotData.xAxis.refreshAxis();
plotData.yAxis.log = gui.gprop.is(GUIProp.PLOTYLOG);
plotData.yAxis.refreshAxis();
nextContext = null;
drawSubNodes = !rightClick;
rightClick = false;
plotChanged = true;
markingChanged = true;
repaint();
}
@Override
public void refreshFocus() {
repaint();
}
@Override
public void refreshInit() {
plotData = null;
final Data data = gui.context.data();
if(data == null || !visible()) return;
plotData = new PlotData(gui.context);
final String[] items = plotData.getItems().toStringArray();
itemCombo.setModel(new DefaultComboBoxModel(items));
// set first item and trigger assignment of axis assignments
if(items.length > 0) itemCombo.setSelectedIndex(0);
drawSubNodes = true;
markingChanged = true;
plotChanged = true;
repaint();
}
@Override
public void refreshLayout() {
itemImg = itemImage(false, false, false);
itemImgMarked = itemImage(false, true, false);
itemImgFocused = itemImage(true, false, false);
itemImgSub = itemImage(false, false, true);
final int sz = sizeFactor() / 2;
MARGIN[0] = sz + 7;
MARGIN[1] = sz * 6;
MARGIN[2] = 55 + sz * 7;
MARGIN[3] = sz + 3;
plotChanged = true;
markingChanged = true;
if(plotData == null) return;
repaint();
}
@Override
public void refreshMark() {
drawSubNodes = true;
markingChanged = true;
repaint();
}
@Override
public void refreshUpdate() {
refreshContext(false, true);
}
@Override
public boolean visible() {
return gui.gprop.is(GUIProp.SHOWPLOT);
}
@Override
public void visible(final boolean v) {
gui.gprop.set(GUIProp.SHOWPLOT, v);
}
@Override
protected boolean db() {
return true;
}
/**
* Locates the nearest item to the mouse pointer.
* @return item focused
*/
private boolean focus() {
final int size = itemImg.getWidth() / 2;
int focusedPre = gui.context.focused;
// if mouse pointer is outside of the plot the focused item is set to -1,
// focus may be refreshed, if necessary
if(mouseX < MARGIN[1] ||
mouseX > getWidth() - MARGIN[3] + size ||
mouseY < MARGIN[0] - size || mouseY > getHeight() - MARGIN[2]) {
// focused item already -1, no refresh needed
if(focusedPre == -1) {
return false;
}
gui.notify.focus(-1, this);
return true;
}
// find focused item.
focusedPre = -1;
int dist = Integer.MAX_VALUE;
// all displayed items are tested for focus
for(int i = 0; i < plotData.pres.length && dist != 0; ++i) {
// coordinates and distances for current tested item are calculated
final int x = calcCoordinate(true, plotData.xAxis.co[i]);
final int y = calcCoordinate(false, plotData.yAxis.co[i]);
final int distX = Math.abs(mouseX - x);
final int distY = Math.abs(mouseY - y);
final int sz = sizeFactor() / 4;
// if x and y distances are smaller than offset value and the
// x and y distances combined is smaller than the actual minimal
// distance of any item tested so far, the current item is considered
// as a focus candidate
if(distX < sz && distY < sz) {
final int currDist = distX * distY;
if(currDist < dist) {
dist = currDist;
focusedPre = plotData.pres[i];
}
}
}
// if the focus changed, views are refreshed
if(focusedPre != gui.context.focused) {
gui.notify.focus(focusedPre, this);
return true;
}
return false;
}
/**
* Determines all nodes lying under the mouse cursor (or the currently
* focused node).
* @param pre position of pre value in the sorted array of plotted nodes
* @return nodes
*/
private int[] overlappingNodes(final int pre) {
final IntList il = new IntList();
// get coordinates for focused item
final int mx = calcCoordinate(true, plotData.xAxis.co[pre]);
final int my = calcCoordinate(false, plotData.yAxis.co[pre]);
for(int i = 0; i < plotData.pres.length; ++i) {
// get coordinates for current item
final int x = calcCoordinate(true, plotData.xAxis.co[i]);
final int y = calcCoordinate(false, plotData.yAxis.co[i]);
if(mx == x && my == y) {
il.add(plotData.pres[i]);
}
}
return il.toArray();
}
/**
* Returns a standardized size factor for painting the plot.
* @return size value
*/
private int sizeFactor() {
return Math.max(2, gui.gprop.num(GUIProp.FONTSIZE) << 1);
}
/**
* Formats an axis caption.
* @param drawX x/y axis flag
* @param focused pre value of the focused node
* @return formatted string
*/
private String formatString(final boolean drawX, final int focused) {
final PlotAxis axis = drawX ? plotData.xAxis : plotData.yAxis;
final byte[] val = axis.getValue(focused);
if(val.length == 0) return "";
return axis.type == StatsType.TEXT || axis.type == StatsType.CATEGORY ?
string(val) : BaseXLayout.value(toDouble(val));
}
@Override
public void mouseMoved(final MouseEvent e) {
if(gui.updating || gui.painting) return;
mouseX = e.getX();
mouseY = e.getY();
if(focus()) repaint();
}
@Override
public void mouseDragged(final MouseEvent e) {
if(gui.updating || e.isShiftDown()) return;
if(dragging) {
// to avoid significant offset between coordinates of mouse click and the
// start coordinates of the bounding box, mouseX and mouseY are determined
// by mousePressed()
mouseX = e.getX();
mouseY = e.getY();
}
final int h = getHeight();
final int w = getWidth();
final int th = 14;
final int lb = MARGIN[1] - th;
final int rb = w - MARGIN[3] + th;
final int tb = MARGIN[0] - th;
final int bb = h - MARGIN[2] + th;
// flag which indicates if mouse pointer is located on the plot inside
final boolean inBox = mouseY > tb && mouseY < bb &&
mouseX > lb && mouseX < rb;
if(!dragging && !inBox) return;
// first time method is called when mouse dragged
if(!dragging) {
dragging = true;
selectionBox.x = mouseX;
selectionBox.y = mouseY;
}
// keeps selection box on the plot inside. if mouse pointer is outside box
// the corners of the selection box are set to the predefined values s.a.
int x = mouseX;
int y = mouseY;
if(!inBox) {
if(mouseX < lb) {
if(mouseY > bb) {
x = lb;
y = bb;
} else if(mouseY < tb) {
x = lb;
y = tb;
} else {
x = lb;
}
} else if(mouseX > rb) {
if(mouseY > bb) {
x = rb;
y = bb;
} else if(mouseY < tb) {
x = rb;
y = tb;
} else {
x = rb;
}
} else if(mouseY < tb) {
y = tb;
} else {
y = bb;
}
}
selectionBox.w = x - selectionBox.x;
selectionBox.h = y - selectionBox.y;
// searches for items located in the selection box
final IntList il = new IntList();
for(int i = 0; i < plotData.pres.length; ++i) {
x = calcCoordinate(true, plotData.xAxis.co[i]);
y = calcCoordinate(false, plotData.yAxis.co[i]);
if(selectionBox.contains(x, y)) il.add(plotData.pres[i]);
}
gui.notify.mark(new Nodes(il.toArray(), gui.context.data()), this);
nextContext = gui.context.marked;
drawSubNodes = false;
markingChanged = true;
repaint();
}
@Override
public void mouseReleased(final MouseEvent e) {
if(gui.updating || gui.painting) return;
dragging = false;
repaint();
}
@Override
public void mousePressed(final MouseEvent e) {
if(gui.updating || gui.painting) return;
markingChanged = true;
mouseX = e.getX();
mouseY = e.getY();
focus();
// determine if a following context filter operation is possibly triggered
// by popup menu
final boolean r = !SwingUtilities.isLeftMouseButton(e);
if(r) { rightClick = true; return; }
// no item is focused. no nodes marked after mouse click
if(gui.context.focused == -1) {
gui.notify.mark(new Nodes(gui.context.data()), this);
return;
}
// node marking if item focused. if more than one icon is in focus range
// all of these are marked. focus range means exact same x AND y coordinate.
final int pre = plotData.findPre(gui.context.focused);
final int[] il = overlappingNodes(pre);
// right mouse or shift down
if(e.isShiftDown()) {
final Nodes marked = gui.context.marked;
marked.union(il);
gui.notify.mark(marked, this);
// double click
} else if(e.getClickCount() == 2) {
// context change also self implied, thus right click set to true
rightClick = true;
final Nodes marked = new Nodes(gui.context.data());
marked.union(il);
gui.notify.context(marked, false, null);
// simple mouse click
} else {
final Nodes marked = new Nodes(il, gui.context.data());
gui.notify.mark(marked, this);
}
nextContext = gui.context.marked;
}
@Override
public void componentResized(final ComponentEvent e) {
markingChanged = true;
plotChanged = true;
if(!gui.updating) repaint();
}
}