package de.lmu.ifi.dbs.elki.visualization.visualizers.histogram; import java.util.Arrays; /* This file is part of ELKI: Environment for Developing KDD-Applications Supported by Index-Structures Copyright (C) 2015 Ludwig-Maximilians-Universität München Lehr- und Forschungseinheit für Datenbanksysteme ELKI Development Team This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program 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 Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import org.apache.batik.util.SVGConstants; import org.w3c.dom.Element; import de.lmu.ifi.dbs.elki.data.DoubleVector; import de.lmu.ifi.dbs.elki.data.NumberVector; import de.lmu.ifi.dbs.elki.database.ids.DBIDIter; import de.lmu.ifi.dbs.elki.database.relation.Relation; import de.lmu.ifi.dbs.elki.database.relation.RelationUtil; import de.lmu.ifi.dbs.elki.logging.LoggingUtil; import de.lmu.ifi.dbs.elki.math.DoubleMinMax; import de.lmu.ifi.dbs.elki.math.scales.LinearScale; import de.lmu.ifi.dbs.elki.result.ResultUtil; import de.lmu.ifi.dbs.elki.result.SamplingResult; import de.lmu.ifi.dbs.elki.utilities.datastructures.histogram.DoubleArrayStaticHistogram; import de.lmu.ifi.dbs.elki.utilities.exceptions.ObjectNotFoundException; import de.lmu.ifi.dbs.elki.utilities.optionhandling.AbstractParameterizer; import de.lmu.ifi.dbs.elki.utilities.optionhandling.OptionID; import de.lmu.ifi.dbs.elki.utilities.optionhandling.constraints.CommonConstraints; import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.Parameterization; import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.Flag; import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.IntParameter; import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary; import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; import de.lmu.ifi.dbs.elki.visualization.css.CSSClassManager.CSSNamingConflict; import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; import de.lmu.ifi.dbs.elki.visualization.projections.Projection; import de.lmu.ifi.dbs.elki.visualization.projector.HistogramProjector; import de.lmu.ifi.dbs.elki.visualization.style.ClassStylingPolicy; import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; import de.lmu.ifi.dbs.elki.visualization.style.StylingPolicy; import de.lmu.ifi.dbs.elki.visualization.svg.SVGPath; import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot; import de.lmu.ifi.dbs.elki.visualization.svg.SVGSimpleLinearAxis; import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory; import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; /* This file is part of ELKI: Environment for Developing KDD-Applications Supported by Index-Structures Copyright (C) 2015 Ludwig-Maximilians-Universität München Lehr- und Forschungseinheit für Datenbanksysteme ELKI Development Team This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program 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 Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ /** * Generates a SVG-Element containing a histogram representing the distribution * of the database's objects. * * @author Remigius Wojdanowski * @author Erich Schubert * @since 0.3 * * @apiviz.stereotype factory * @apiviz.uses Instance oneway - - «create» */ public class ColoredHistogramVisualizer extends AbstractVisFactory { /** * Name for this visualizer. */ private static final String CNAME = "Histograms"; /** * Settings */ protected Parameterizer settings; /** * Number of bins to use in histogram. */ private static final int DEFAULT_BINS = 80; /** * Constructor. * * @param settings Settings */ public ColoredHistogramVisualizer(Parameterizer settings) { super(); this.settings = settings; } @Override public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { return new Instance(task, plot, width, height, proj); } @Override public void processNewResult(VisualizerContext context, Object start) { VisualizationTree.findNew(context, start, HistogramProjector.class, // new VisualizationTree.Handler1>() { @Override public void process(VisualizerContext context, HistogramProjector p) { // register self final VisualizationTask task = new VisualizationTask(CNAME, context, p, p.getRelation(), ColoredHistogramVisualizer.this); task.level = VisualizationTask.LEVEL_DATA; task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_STYLEPOLICY); context.addVis(p, task); } }); } @Override public boolean allowThumbnails(VisualizationTask task) { // Don't use thumbnails return false; } /** * Instance * * @author Remigius Wojdanowski * * @apiviz.has NumberVector oneway - - visualizes * * @param Type of the DatabaseObject being visualized. */ // FIXME: cache histogram instead of recomputing it? public class Instance extends AbstractHistogramVisualization { /** * Generic tag to indicate the type of element. Used in IDs, CSS-Classes * etc. */ public static final String BIN = "bin"; /** * The database we visualize */ private Relation relation; /** * Sampling result */ private SamplingResult sample; /** * Constructor. * * @param task Visualization task * @param plot Plot to draw to * @param width Embedding width * @param height Embedding height * @param proj Projection */ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { super(task, plot, width, height, proj); this.relation = task.getRelation(); this.sample = ResultUtil.getSamplingResult(relation); addListeners(); } @Override public void fullRedraw() { StyleLibrary style = context.getStyleLibrary(); double margin = style.getSize(StyleLibrary.MARGIN); layer = SVGUtil.svgElement(svgp.getDocument(), SVGConstants.SVG_G_TAG); double xsize = Projection.SCALE * getWidth() / getHeight(); double ysize = Projection.SCALE; final String transform = SVGUtil.makeMarginTransform(getWidth(), getHeight(), xsize, ysize, margin); SVGUtil.setAtt(layer, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, transform); // Styling policy final StylingPolicy spol = context.getStylingPolicy(); final ClassStylingPolicy cspol; if(spol instanceof ClassStylingPolicy) { cspol = (ClassStylingPolicy) spol; } else { cspol = null; } // TODO also use min style? setupCSS(svgp, (cspol != null) ? cspol.getMaxStyle() : 0); // Create histograms final int off = (cspol != null) ? cspol.getMinStyle() : 0; final int numc = (cspol != null) ? (cspol.getMaxStyle() - cspol.getMinStyle()) : 0; DoubleMinMax minmax = new DoubleMinMax(); final double frac = 1. / relation.size(); // TODO: sampling? final int cols = numc + 1; DoubleArrayStaticHistogram histogram = new DoubleArrayStaticHistogram(settings.bins, -.5, .5, cols); if(cspol != null) { for(int snum = 0; snum < numc; snum++) { double[] inc = new double[cols]; inc[0] = frac; inc[snum + 1] = frac; for(DBIDIter iter = cspol.iterateClass(snum + off); iter.valid(); iter.advance()) { if(!sample.getSample().contains(iter)) { continue; // TODO: can we test more efficiently than this? } try { double pos = proj.fastProjectDataToRenderSpace(relation.get(iter)) * Projection.INVSCALE; histogram.increment(pos, inc); } catch(ObjectNotFoundException e) { // Ignore. The object was probably deleted from the database } } } } else { // Actual data distribution. double[] inc = new double[cols]; inc[0] = frac; for(DBIDIter iditer = relation.iterDBIDs(); iditer.valid(); iditer.advance()) { double pos = proj.fastProjectDataToRenderSpace(relation.get(iditer)) * Projection.INVSCALE; histogram.increment(pos, inc); } } // for scaling, get the maximum occurring value in the bins: for(DoubleArrayStaticHistogram.Iter iter = histogram.iter(); iter.valid(); iter.advance()) { for(double val : iter.getValue()) { minmax.put(val); } } LinearScale yscale = new LinearScale(0, minmax.getMax()); LinearScale xscale = new LinearScale(histogram.getCoverMinimum(), histogram.getCoverMaximum()); // Axis. TODO: Add an AxisVisualizer for this? try { SVGSimpleLinearAxis.drawAxis(svgp, layer, yscale, 0, ysize, 0, 0, SVGSimpleLinearAxis.LabelStyle.LEFTHAND, style); // draw axes that are non-trivial final int dimensionality = RelationUtil.dimensionality(relation); final double[] vec = new double[dimensionality]; double orig = proj.fastProjectScaledToRender(vec); for(int d = 0; d < dimensionality; d++) { Arrays.fill(vec, 0.); vec[d] = 1; // projected endpoint of axis double ax = proj.fastProjectScaledToRender(vec); if(ax < orig || ax > orig) { final double left = (orig / Projection.SCALE + 0.5) * xsize; final double right = (ax / Projection.SCALE + 0.5) * xsize; SVGSimpleLinearAxis.drawAxis(svgp, layer, proj.getScale(d), left, ysize, right, ysize, SVGSimpleLinearAxis.LabelStyle.RIGHTHAND, style); } } } catch(CSSNamingConflict e) { LoggingUtil.exception("CSS class exception in axis class.", e); } // Visualizing if(!settings.curves) { for(DoubleArrayStaticHistogram.Iter iter = histogram.iter(); iter.valid(); iter.advance()) { double lpos = xscale.getScaled(iter.getLeft()); double rpos = xscale.getScaled(iter.getRight()); double stack = 0.0; final int start = numc > 0 ? 1 : 0; for(int key = start; key < cols; key++) { double val = yscale.getScaled(iter.getValue()[key]); Element row = SVGUtil.svgRect(svgp.getDocument(), xsize * lpos, ysize * (1 - (val + stack)), xsize * (rpos - lpos), ysize * val); stack = stack + val; SVGUtil.addCSSClass(row, BIN + (off + key - 1)); layer.appendChild(row); } } } else { double left = xscale.getScaled(histogram.getCoverMinimum()); double right = left; SVGPath[] paths = new SVGPath[cols]; double[] lasty = new double[cols]; for(int i = 0; i < cols; i++) { paths[i] = new SVGPath(xsize * left, ysize * 1); lasty[i] = 0; } // draw histogram lines for(DoubleArrayStaticHistogram.Iter iter = histogram.iter(); iter.valid(); iter.advance()) { left = xscale.getScaled(iter.getLeft()); right = xscale.getScaled(iter.getRight()); for(int i = 0; i < cols; i++) { double val = yscale.getScaled(iter.getValue()[i]); if(lasty[i] > val || lasty[i] < val) { paths[i].lineTo(xsize * left, ysize * (1 - lasty[i])); paths[i].lineTo(xsize * left, ysize * (1 - val)); paths[i].lineTo(xsize * right, ysize * (1 - val)); lasty[i] = val; } } } // close and insert all lines. for(int i = 0; i < cols; i++) { if(lasty[i] != 0) { paths[i].lineTo(xsize * right, ysize * (1 - lasty[i])); } paths[i].lineTo(xsize * right, ysize * 1); Element elem = paths[i].makeElement(svgp); SVGUtil.addCSSClass(elem, BIN + (off + i - 1)); layer.appendChild(elem); } } svgp.updateStyleElement(); } /** * Generate the needed CSS classes. * * @param svgp Plot context * @param numc Number of classes we need. */ private void setupCSS(SVGPlot svgp, int numc) { final StyleLibrary style = context.getStyleLibrary(); ColorLibrary colors = style.getColorSet(StyleLibrary.PLOT); CSSClass allInOne = new CSSClass(svgp, BIN + -1); if(!settings.curves) { allInOne.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_BLACK_VALUE); allInOne.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, 1.0); } else { allInOne.setStatement(SVGConstants.CSS_STROKE_PROPERTY, SVGConstants.CSS_BLACK_VALUE); allInOne.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.PLOT)); allInOne.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE); } svgp.addCSSClassOrLogError(allInOne); for(int clusterID = 0; clusterID < numc; clusterID++) { CSSClass bin = new CSSClass(svgp, BIN + clusterID); if(!settings.curves) { bin.setStatement(SVGConstants.CSS_FILL_PROPERTY, colors.getColor(clusterID)); } else { bin.setStatement(SVGConstants.CSS_STROKE_PROPERTY, colors.getColor(clusterID)); bin.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.PLOT)); bin.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE); } svgp.addCSSClassOrLogError(bin); } } } /** * Parameterization class. * * @author Erich Schubert * * @apiviz.exclude */ public static class Parameterizer extends AbstractParameterizer { /** * Flag to specify the "curves" rendering style. * *

* Key: {@code -histogram.curves} *

*/ public static final OptionID STYLE_CURVES_ID = new OptionID("projhistogram.curves", "Use curves instead of the stacked histogram style."); /** * Parameter to specify the number of bins to use in histogram. * *

* Key: {@code -projhistogram.bins} Default: 80 *

*/ public static final OptionID HISTOGRAM_BINS_ID = new OptionID("projhistogram.bins", "Number of bins in the distribution histogram"); /** * Internal storage of the curves flag. */ protected boolean curves = false; /** * Number of bins to use in the histogram. */ protected int bins = DEFAULT_BINS; @Override protected void makeOptions(Parameterization config) { super.makeOptions(config); Flag curvesF = new Flag(STYLE_CURVES_ID); if(config.grab(curvesF)) { curves = curvesF.isTrue(); } IntParameter binsP = new IntParameter(HISTOGRAM_BINS_ID, DEFAULT_BINS); binsP.addConstraint(CommonConstraints.GREATER_THAN_ONE_INT); if(config.grab(binsP)) { bins = binsP.intValue(); } } @Override protected ColoredHistogramVisualizer makeInstance() { return new ColoredHistogramVisualizer(this); } } }