blob: 1ebc789418046ae401709fe625d5560caf47d88d [file] [log] [blame]
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:html';
import 'dart:math' as Math;
import 'package:observatory_2/utils.dart';
abstract class TreeMap<T> {
int getArea(T node);
String getBackground(T node);
String getLabel(T node);
String getTooltip(T node) => getLabel(node);
T getParent(T node);
Iterable<T> getChildren(T node);
void onSelect(T node);
void onDetails(T node);
void showIn(T node, DivElement content) {
final w = content.offsetWidth.toDouble();
final h = content.offsetHeight.toDouble();
final topTile = _createTreemapTile(node, w, h, 0, content);
topTile.style.width = "${w}px";
topTile.style.height = "${h}px";
topTile.style.border = "none";
content.children = [topTile];
}
Element _createTreemapTile(
T node, double width, double height, int depth, DivElement content) {
final div = new DivElement();
div.className = "treemapTile";
div.style.background = getBackground(node);
div.onDoubleClick.listen((event) {
event.stopPropagation();
if (depth == 0) {
onSelect(getParent(node)); // Zoom out.
} else {
onSelect(node); // Zoom in.
}
});
div.onContextMenu.listen((event) {
event.stopPropagation();
onDetails(node);
});
double left = 0.0;
double top = 0.0;
const kPadding = 5;
const kBorder = 1;
left += kPadding - kBorder;
top += kPadding - kBorder;
width -= 2 * kPadding;
height -= 2 * kPadding;
div.title = getTooltip(node);
if (width < 10 || height < 10) {
// Too small: don't render label or children.
return div;
}
div.append(new SpanElement()..text = getLabel(node));
const kLabelHeight = 9.0;
top += kLabelHeight;
height -= kLabelHeight;
if (depth > 2) {
// Too deep: don't render children.
return div;
}
if (width < 4 || height < 4) {
// Too small: don't render children.
return div;
}
final children = <T>[];
for (T c in getChildren(node)) {
// Size 0 children seem to confuse the layout algorithm (accumulating
// rounding errors?).
if (getArea(c) > 0) {
children.add(c);
}
}
children.sort((a, b) => getArea(b) - getArea(a));
final double scale = width * height / getArea(node);
// Bruls M., Huizing K., van Wijk J.J. (2000) Squarified Treemaps. In: de
// Leeuw W.C., van Liere R. (eds) Data Visualization 2000. Eurographics.
// Springer, Vienna.
for (int rowStart = 0; // Index of first child in the next row.
rowStart < children.length;) {
// Prefer wider rectangles, the better to fit text labels.
const double GOLDEN_RATIO = 1.61803398875;
final bool verticalSplit = (width / height) > GOLDEN_RATIO;
double space;
if (verticalSplit) {
space = height;
} else {
space = width;
}
double rowMin = getArea(children[rowStart]) * scale;
double rowMax = rowMin;
double rowSum = 0.0;
double lastRatio = 0.0;
int rowEnd; // One after index of last child in the next row.
for (rowEnd = rowStart; rowEnd < children.length; rowEnd++) {
double size = getArea(children[rowEnd]) * scale;
if (size < rowMin) rowMin = size;
if (size > rowMax) rowMax = size;
rowSum += size;
double ratio = Math.max((space * space * rowMax) / (rowSum * rowSum),
(rowSum * rowSum) / (space * space * rowMin));
if ((lastRatio != 0) && (ratio > lastRatio)) {
// Adding the next child makes the aspect ratios worse: remove it and
// add the row.
rowSum -= size;
break;
}
lastRatio = ratio;
}
double rowLeft = left;
double rowTop = top;
double rowSpace = rowSum / space;
for (int i = rowStart; i < rowEnd; i++) {
T child = children[i];
double size = getArea(child) * scale;
double childWidth;
double childHeight;
if (verticalSplit) {
childWidth = rowSpace;
childHeight = size / childWidth;
} else {
childHeight = rowSpace;
childWidth = size / childHeight;
}
Element childDiv = _createTreemapTile(
child, childWidth, childHeight, depth + 1, content);
childDiv.style.left = "${rowLeft}px";
childDiv.style.top = "${rowTop}px";
// Oversize the final div by kBorder to make the borders overlap.
childDiv.style.width = "${childWidth + kBorder}px";
childDiv.style.height = "${childHeight + kBorder}px";
div.append(childDiv);
if (verticalSplit)
rowTop += childHeight;
else
rowLeft += childWidth;
}
if (verticalSplit) {
left += rowSpace;
width -= rowSpace;
} else {
top += rowSpace;
height -= rowSpace;
}
rowStart = rowEnd;
}
return div;
}
}
abstract class NormalTreeMap<T> extends TreeMap<T> {
int getSize(T node);
String getName(T node);
String getType(T node);
int getArea(T node) => getSize(node);
String getLabel(T node) {
String name = getName(node);
String size = Utils.formatSize(getSize(node));
return "$name [$size]";
}
String getBackground(T node) {
int hue = getType(node).hashCode % 360;
return "hsl($hue,60%,60%)";
}
}