1 /* 2 * This file is part of ELKI: 3 * Environment for Developing KDD-Applications Supported by Index-Structures 4 * 5 * Copyright (C) 2018 6 * ELKI Development Team 7 * 8 * This program is free software: you can redistribute it and/or modify 9 * it under the terms of the GNU Affero General Public License as published by 10 * the Free Software Foundation, either version 3 of the License, or 11 * (at your option) any later version. 12 * 13 * This program is distributed in the hope that it will be useful, 14 * but WITHOUT ANY WARRANTY; without even the implied warranty of 15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 * GNU Affero General Public License for more details. 17 * 18 * You should have received a copy of the GNU Affero General Public License 19 * along with this program. If not, see <http://www.gnu.org/licenses/>. 20 */ 21 package de.lmu.ifi.dbs.elki.visualization.visualizers.optics; 22 23 import org.apache.batik.util.SVG12Constants; 24 import org.apache.batik.util.SVGConstants; 25 import org.w3c.dom.Element; 26 import org.w3c.dom.events.Event; 27 import org.w3c.dom.svg.SVGPoint; 28 29 import de.lmu.ifi.dbs.elki.algorithm.clustering.optics.ClusterOrder; 30 import de.lmu.ifi.dbs.elki.data.Clustering; 31 import de.lmu.ifi.dbs.elki.data.model.Model; 32 import de.lmu.ifi.dbs.elki.utilities.io.FormatUtil; 33 import de.lmu.ifi.dbs.elki.visualization.VisualizationTask; 34 import de.lmu.ifi.dbs.elki.visualization.VisualizationTree; 35 import de.lmu.ifi.dbs.elki.visualization.VisualizerContext; 36 import de.lmu.ifi.dbs.elki.visualization.VisualizationTask.RenderFlag; 37 import de.lmu.ifi.dbs.elki.visualization.batikutil.DragableArea; 38 import de.lmu.ifi.dbs.elki.visualization.css.CSSClass; 39 import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot; 40 import de.lmu.ifi.dbs.elki.visualization.opticsplot.OPTICSCut; 41 import de.lmu.ifi.dbs.elki.visualization.opticsplot.OPTICSPlot; 42 import de.lmu.ifi.dbs.elki.visualization.projections.Projection; 43 import de.lmu.ifi.dbs.elki.visualization.projector.OPTICSProjector; 44 import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; 45 import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil; 46 import de.lmu.ifi.dbs.elki.visualization.visualizers.VisFactory; 47 import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization; 48 49 /** 50 * Visualizes a cut in an OPTICS Plot to select an Epsilon value and generate a 51 * new clustering result. 52 * 53 * @author Heidi Kolb 54 * @author Erich Schubert 55 * @since 0.4.0 56 * 57 * @stereotype factory 58 * @navassoc - create - Instance 59 */ 60 public class OPTICSPlotCutVisualization implements VisFactory { 61 /** 62 * A short name characterizing this Visualizer. 63 */ 64 private static final String NAME = "OPTICS Cut"; 65 66 /** 67 * Constructor. 68 */ OPTICSPlotCutVisualization()69 public OPTICSPlotCutVisualization() { 70 super(); 71 } 72 73 @Override processNewResult(VisualizerContext context, Object result)74 public void processNewResult(VisualizerContext context, Object result) { 75 VisualizationTree.findVis(context, result).filter(OPTICSProjector.class).forEach(p -> { 76 context.addVis(p, new VisualizationTask(this, NAME, p.getResult(), null) // 77 .level(VisualizationTask.LEVEL_INTERACTIVE) // 78 .with(RenderFlag.NO_THUMBNAIL).with(RenderFlag.NO_EXPORT)); 79 }); 80 } 81 82 @Override makeVisualization(VisualizerContext context, VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj)83 public Visualization makeVisualization(VisualizerContext context, VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { 84 return new Instance(context, task, plot, width, height, proj); 85 } 86 87 @Override allowThumbnails(VisualizationTask task)88 public boolean allowThumbnails(VisualizationTask task) { 89 // Don't use thumbnails 90 return false; 91 } 92 93 /** 94 * Instance. 95 * 96 * @author Heidi Kolb 97 * @author Erich Schubert 98 */ 99 public class Instance extends AbstractOPTICSVisualization implements DragableArea.DragListener { 100 /** 101 * CSS-Styles 102 */ 103 protected static final String CSS_LINE = "opticsPlotLine"; 104 105 /** 106 * CSS-Styles 107 */ 108 protected static final String CSS_EPSILON = "opticsPlotEpsilonValue"; 109 110 /** 111 * The current epsilon value. 112 */ 113 private double epsilon = 0.0; 114 115 /** 116 * Sensitive (clickable) area 117 */ 118 private DragableArea eventarea = null; 119 120 /** 121 * The label element 122 */ 123 private Element elemText = null; 124 125 /** 126 * The line element 127 */ 128 private Element elementLine = null; 129 130 /** 131 * The drag handle element 132 */ 133 private Element elementPoint = null; 134 135 /** 136 * Constructor. 137 * 138 * @param context Visualizer context 139 * @param task Task 140 * @param plot Plot to draw to 141 * @param width Embedding width 142 * @param height Embedding height 143 * @param proj Projection 144 */ Instance(VisualizerContext context, VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj)145 public Instance(VisualizerContext context, VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) { 146 super(context, task, plot, width, height, proj); 147 } 148 149 @Override fullRedraw()150 public void fullRedraw() { 151 incrementalRedraw(); 152 } 153 154 @Override incrementalRedraw()155 public void incrementalRedraw() { 156 if(layer == null) { 157 makeLayerElement(); 158 addCSSClasses(); 159 } 160 161 // TODO make the number of digits configurable 162 final String label = (epsilon > 0.0) ? FormatUtil.NF4.format(epsilon) : ""; 163 // compute absolute y-value of bar 164 final double yAct = getYFromEpsilon(epsilon); 165 166 if(elemText == null) { 167 elemText = svgp.svgText(StyleLibrary.SCALE * 1.05, yAct, label); 168 SVGUtil.setAtt(elemText, SVGConstants.SVG_CLASS_ATTRIBUTE, CSS_EPSILON); 169 layer.appendChild(elemText); 170 } 171 else { 172 elemText.setTextContent(label); 173 SVGUtil.setAtt(elemText, SVGConstants.SVG_Y_ATTRIBUTE, yAct); 174 } 175 176 // line and handle 177 if(elementLine == null) { 178 elementLine = svgp.svgLine(0, yAct, StyleLibrary.SCALE * 1.04, yAct); 179 SVGUtil.addCSSClass(elementLine, CSS_LINE); 180 layer.appendChild(elementLine); 181 } 182 else { 183 SVGUtil.setAtt(elementLine, SVG12Constants.SVG_Y1_ATTRIBUTE, yAct); 184 SVGUtil.setAtt(elementLine, SVG12Constants.SVG_Y2_ATTRIBUTE, yAct); 185 } 186 if(elementPoint == null) { 187 elementPoint = svgp.svgCircle(StyleLibrary.SCALE * 1.04, yAct, StyleLibrary.SCALE * 0.004); 188 SVGUtil.addCSSClass(elementPoint, CSS_LINE); 189 layer.appendChild(elementPoint); 190 } 191 else { 192 SVGUtil.setAtt(elementPoint, SVG12Constants.SVG_CY_ATTRIBUTE, yAct); 193 } 194 195 if(eventarea == null) { 196 eventarea = new DragableArea(svgp, StyleLibrary.SCALE, -StyleLibrary.SCALE * 0.01, // 197 StyleLibrary.SCALE * 0.1, plotheight + StyleLibrary.SCALE * 0.02, this); 198 layer.appendChild(eventarea.getElement()); 199 } 200 } 201 202 @Override destroy()203 public void destroy() { 204 super.destroy(); 205 eventarea.destroy(); 206 } 207 208 /** 209 * Get epsilon from y-value 210 * 211 * @param y y-Value 212 * @return epsilon 213 */ getEpsilonFromY(double y)214 protected double getEpsilonFromY(double y) { 215 OPTICSPlot opticsplot = optics.getOPTICSPlot(context); 216 y = (y < 0) ? 0 : (y > plotheight) ? 1. : y / plotheight; 217 return optics.getOPTICSPlot(context).scaleFromPixel(y * opticsplot.getHeight()); 218 } 219 220 /** 221 * Get y-value from epsilon 222 * 223 * @param epsilon epsilon 224 * @return y-Value 225 */ getYFromEpsilon(double epsilon)226 protected double getYFromEpsilon(double epsilon) { 227 OPTICSPlot opticsplot = optics.getOPTICSPlot(context); 228 int h = opticsplot.getHeight(); 229 double y = opticsplot.getScale().getScaled(epsilon, h - .5, .5) / (double) h * plotheight; 230 return (y < 0.) ? 0. : (y > plotheight) ? plotheight : y; 231 } 232 233 @Override startDrag(SVGPoint start, Event evt)234 public boolean startDrag(SVGPoint start, Event evt) { 235 epsilon = getEpsilonFromY(plotheight - start.getY()); 236 // opvis.unsetEpsilonExcept(this); 237 svgp.requestRedraw(this.task, this); 238 return true; 239 } 240 241 @Override duringDrag(SVGPoint start, SVGPoint end, Event evt, boolean inside)242 public boolean duringDrag(SVGPoint start, SVGPoint end, Event evt, boolean inside) { 243 if(inside) { 244 epsilon = getEpsilonFromY(plotheight - end.getY()); 245 } 246 // opvis.unsetEpsilonExcept(this); 247 svgp.requestRedraw(this.task, this); 248 return true; 249 } 250 251 @Override endDrag(SVGPoint start, SVGPoint end, Event evt, boolean inside)252 public boolean endDrag(SVGPoint start, SVGPoint end, Event evt, boolean inside) { 253 if(inside) { 254 epsilon = getEpsilonFromY(plotheight - end.getY()); 255 // opvis.unsetEpsilonExcept(this); 256 257 // FIXME: replace an existing optics cut result! 258 final ClusterOrder order = optics.getResult(); 259 Clustering<Model> cl = OPTICSCut.makeOPTICSCut(order, epsilon); 260 order.addChildResult(cl); 261 } 262 svgp.requestRedraw(this.task, this); 263 return true; 264 } 265 266 /** 267 * Reset the epsilon value. 268 */ unsetEpsilon()269 public void unsetEpsilon() { 270 epsilon = 0.0; 271 } 272 273 /** 274 * Adds the required CSS-Classes 275 */ addCSSClasses()276 private void addCSSClasses() { 277 // Class for the epsilon-value 278 final StyleLibrary style = context.getStyleLibrary(); 279 if(!svgp.getCSSClassManager().contains(CSS_EPSILON)) { 280 final CSSClass label = new CSSClass(svgp, CSS_EPSILON); 281 label.setStatement(SVGConstants.CSS_FILL_PROPERTY, style.getTextColor(StyleLibrary.AXIS_LABEL)); 282 label.setStatement(SVGConstants.CSS_FONT_FAMILY_PROPERTY, style.getFontFamily(StyleLibrary.AXIS_LABEL)); 283 label.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, style.getTextSize(StyleLibrary.AXIS_LABEL)); 284 svgp.addCSSClassOrLogError(label); 285 } 286 // Class for the epsilon cut line 287 if(!svgp.getCSSClassManager().contains(CSS_LINE)) { 288 final CSSClass lcls = new CSSClass(svgp, CSS_LINE); 289 lcls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, style.getColor(StyleLibrary.PLOT)); 290 lcls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, 0.5 * style.getLineWidth(StyleLibrary.PLOT)); 291 svgp.addCSSClassOrLogError(lcls); 292 } 293 } 294 } 295 } 296