package de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj;
/*
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 gnu.trove.map.TObjectIntMap;
import gnu.trove.map.hash.TObjectIntHashMap;
import java.util.List;
import org.apache.batik.util.SVGConstants;
import org.w3c.dom.Element;
import de.lmu.ifi.dbs.elki.data.Cluster;
import de.lmu.ifi.dbs.elki.data.Clustering;
import de.lmu.ifi.dbs.elki.data.model.Model;
import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy.Iter;
import de.lmu.ifi.dbs.elki.utilities.pairs.DoubleDoublePair;
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.css.CSSClass;
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.style.ClusterStylingPolicy;
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.style.marker.MarkerLibrary;
import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
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.AbstractVisualization;
import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
/**
* Visualizer, displaying the key for a clustering.
*
* TODO: re-add automatic sizing depending on the number of clusters.
*
* TODO: also show in scatter plot detail view.
*
* @author Erich Schubert
* @since 0.3
*
* @apiviz.stereotype factory
* @apiviz.uses Instance oneway - - «create»
*/
public class KeyVisualization extends AbstractVisFactory {
/**
* Name for this visualizer.
*/
private static final String NAME = "Cluster Key";
@Override
public void processNewResult(VisualizerContext context, Object start) {
// Ensure there is a clustering result:
Hierarchy.Iter> it = VisualizationTree.filterResults(context, start, Clustering.class);
if(!it.valid()) {
return;
}
Hierarchy.Iter i2 = VisualizationTree.filter(context, VisualizationTask.class);
for(; i2.valid(); i2.advance()) {
if(i2.get().getFactory() instanceof KeyVisualization) {
return; // At most one key per plot.
}
}
final VisualizationTask task = new VisualizationTask(NAME, context, context.getStylingPolicy(), null, this);
task.level = VisualizationTask.LEVEL_STATIC;
task.addUpdateFlags(VisualizationTask.ON_STYLEPOLICY);
task.reqwidth = 1.;
task.reqheight = 1.;
context.addVis(context.getStylingPolicy(), task);
}
/**
* Compute the size of the clustering.
*
* @param c Clustering
* @return Array storing the depth and the number of leaf nodes.
*/
protected static int[] findDepth(Clustering c) {
final Hierarchy> hier = c.getClusterHierarchy();
int[] size = { 0, 0 };
for(Iter> iter = c.iterToplevelClusters(); iter.valid(); iter.advance()) {
findDepth(hier, iter.get(), size);
}
return size;
}
/**
* Recursive depth computation.
*
* @param hier Hierarchy
* @param cluster Current cluster
* @param size Counting array.
*/
private static void findDepth(Hierarchy> hier, Cluster cluster, int[] size) {
if(hier.numChildren(cluster) > 0) {
for(Iter> iter = hier.iterChildren(cluster); iter.valid(); iter.advance()) {
findDepth(hier, iter.get(), size);
}
size[0] += 1; // Depth
}
else {
size[1] += 1; // Leaves
}
}
/**
* Compute the preferred number of columns.
*
* @param width Target width
* @param height Target height
* @param numc Number of clusters
* @param maxwidth Max width of entries
* @return Preferred number of columns
*/
protected static int getPreferredColumns(double width, double height, int numc, double maxwidth) {
// Maximum width (compared to height) of labels - guess.
// FIXME: do we really need to do this three-step computation?
// Number of rows we'd use in a squared layout:
final double rows = Math.ceil(Math.pow(numc * maxwidth, height / (width + height)));
// Given this number of rows (plus one for header), use this many columns:
return (int) Math.ceil(numc / (rows + 1));
}
@Override
public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
return new Instance(task, plot, width, height);
}
@Override
public boolean allowThumbnails(VisualizationTask task) {
return false;
}
/**
* Instance
*
* @author Erich Schubert
*
* @apiviz.has Clustering oneway - - visualizes
*/
public class Instance extends AbstractVisualization {
/**
* CSS class for key captions.
*/
private static final String KEY_CAPTION = "key-caption";
/**
* CSS class for key entries.
*/
private static final String KEY_ENTRY = "key-entry";
/**
* CSS class for hierarchy plot lines
*/
private static final String KEY_HIERLINE = "key-hierarchy";
/**
* Constructor.
*
* @param task Visualization task
* @param plot Plot to draw to
* @param width Embedding width
* @param height Embedding height
*/
public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height) {
super(task, plot, width, height);
addListeners();
}
@Override
public void fullRedraw() {
StylingPolicy pol = context.getStylingPolicy();
if(!(pol instanceof ClusterStylingPolicy)) {
Element label = svgp.svgText(0.1, 0.7, "No clustering selected.");
SVGUtil.setCSSClass(label, KEY_CAPTION);
layer.appendChild(label);
return;
}
@SuppressWarnings("unchecked")
Clustering clustering = (Clustering) ((ClusterStylingPolicy) pol).getClustering();
StyleLibrary style = context.getStyleLibrary();
MarkerLibrary ml = style.markers();
final List> allcs = clustering.getAllClusters();
final List> topcs = clustering.getToplevelClusters();
setupCSS(svgp);
layer = svgp.svgElement(SVGConstants.SVG_G_TAG);
// Add a label for the clustering.
{
Element label = svgp.svgText(0.1, 0.7, clustering.getLongName());
SVGUtil.setCSSClass(label, KEY_CAPTION);
layer.appendChild(label);
}
double kwi, khe;
if(allcs.size() == topcs.size()) {
// Maximum width (compared to height) of labels - guess.
// FIXME: compute from labels?
final double maxwidth = 10.;
// Flat clustering. Use multiple columns.
final int numc = allcs.size();
final int cols = getPreferredColumns(getWidth(), getHeight(), numc, maxwidth);
final int rows = (int) Math.ceil(numc / (double) cols);
// We use a coordinate system based on rows, so columns are at
// c*maxwidth
int i = 0;
for(Cluster c : allcs) {
final int col = i / rows;
final int row = i % rows;
ml.useMarker(svgp, layer, 0.3 + maxwidth * col, row + 1.5, i, 0.3);
Element label = svgp.svgText(0.7 + maxwidth * col, row + 1.7, c.getNameAutomatic());
SVGUtil.setCSSClass(label, KEY_ENTRY);
layer.appendChild(label);
i++;
}
kwi = cols * maxwidth;
khe = rows;
}
else {
// For consistent keying:
TObjectIntMap> cnum = new TObjectIntHashMap<>(allcs.size());
int i = 0;
for(Cluster c : allcs) {
cnum.put(c, i);
i++;
}
// Hierarchical clustering. Draw recursively.
DoubleDoublePair size = new DoubleDoublePair(0., 1.), pos = new DoubleDoublePair(0., 1.);
Hierarchy> hier = clustering.getClusterHierarchy();
for(Cluster cluster : topcs) {
drawHierarchy(svgp, ml, size, pos, 0, cluster, cnum, hier);
}
kwi = size.first;
khe = size.second;
}
final double margin = style.getSize(StyleLibrary.MARGIN);
final String transform = SVGUtil.makeMarginTransform(getWidth(), getHeight(), kwi, khe, margin / StyleLibrary.SCALE);
SVGUtil.setAtt(layer, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, transform);
}
private double drawHierarchy(SVGPlot svgp, MarkerLibrary ml, DoubleDoublePair size, DoubleDoublePair pos, int depth, Cluster cluster, TObjectIntMap> cnum, Hierarchy> hier) {
final double maxwidth = 8.;
DoubleDoublePair subpos = new DoubleDoublePair(pos.first + maxwidth, pos.second);
int numc = hier.numChildren(cluster);
double posy;
if(numc > 0) {
double[] mids = new double[numc];
Iter> iter = hier.iterChildren(cluster);
for(int i = 0; iter.valid(); iter.advance(), i++) {
mids[i] = drawHierarchy(svgp, ml, size, subpos, depth, iter.get(), cnum, hier);
}
// Center:
posy = (pos.second + subpos.second) * .5;
for(int i = 0; i < numc; i++) {
Element line = svgp.svgLine(pos.first + maxwidth - 1., posy + .5, pos.first + maxwidth, mids[i] + .5);
SVGUtil.setCSSClass(line, KEY_HIERLINE);
layer.appendChild(line);
}
// Use vertical extends of children:
pos.second = subpos.second;
}
else {
posy = pos.second + .5;
pos.second += 1.;
}
ml.useMarker(svgp, layer, 0.3 + pos.first, posy + 0.5, cnum.get(cluster), 0.3);
Element label = svgp.svgText(0.7 + pos.first, posy + 0.7, cluster.getNameAutomatic());
SVGUtil.setCSSClass(label, KEY_ENTRY);
layer.appendChild(label);
size.first = Math.max(size.first, pos.first + maxwidth);
size.second = Math.max(size.second, pos.second);
return posy;
}
/**
* Registers the Tooltip-CSS-Class at a SVGPlot.
*
* @param svgp the SVGPlot to register the Tooltip-CSS-Class.
*/
protected void setupCSS(SVGPlot svgp) {
final StyleLibrary style = context.getStyleLibrary();
final double fontsize = style.getTextSize(StyleLibrary.KEY);
final String fontfamily = style.getFontFamily(StyleLibrary.KEY);
final String color = style.getColor(StyleLibrary.KEY);
CSSClass keycaption = new CSSClass(svgp, KEY_CAPTION);
keycaption.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, fontsize);
keycaption.setStatement(SVGConstants.CSS_FONT_FAMILY_PROPERTY, fontfamily);
keycaption.setStatement(SVGConstants.CSS_FILL_PROPERTY, color);
keycaption.setStatement(SVGConstants.CSS_FONT_WEIGHT_PROPERTY, SVGConstants.CSS_BOLD_VALUE);
svgp.addCSSClassOrLogError(keycaption);
CSSClass keyentry = new CSSClass(svgp, KEY_ENTRY);
keyentry.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, fontsize);
keyentry.setStatement(SVGConstants.CSS_FONT_FAMILY_PROPERTY, fontfamily);
keyentry.setStatement(SVGConstants.CSS_FILL_PROPERTY, color);
svgp.addCSSClassOrLogError(keyentry);
CSSClass hierline = new CSSClass(svgp, KEY_HIERLINE);
hierline.setStatement(SVGConstants.CSS_STROKE_PROPERTY, color);
hierline.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth("key.hierarchy") / StyleLibrary.SCALE);
svgp.addCSSClassOrLogError(hierline);
svgp.updateStyleElement();
}
}
}