StackGenVis: Alignment of Data, Algorithms, and Models for Stacking Ensemble Learning Using Performance Metrics
https://doi.org/10.1109/TVCG.2020.3030352
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
325 lines
11 KiB
325 lines
11 KiB
import { select, event } from 'd3-selection';
|
|
import { scaleLinear } from 'd3-scale';
|
|
import { hierarchy, pack } from 'd3-hierarchy';
|
|
import { transition } from 'd3-transition';
|
|
import { interpolate } from 'd3-interpolate';
|
|
import zoomable from 'd3-zoomable';
|
|
import Kapsule from 'kapsule';
|
|
import tinycolor from 'tinycolor2';
|
|
import accessorFn from 'accessor-fn';
|
|
|
|
function styleInject(css, ref) {
|
|
if (ref === void 0) ref = {};
|
|
var insertAt = ref.insertAt;
|
|
|
|
if (!css || typeof document === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
var head = document.head || document.getElementsByTagName('head')[0];
|
|
var style = document.createElement('style');
|
|
style.type = 'text/css';
|
|
|
|
if (insertAt === 'top') {
|
|
if (head.firstChild) {
|
|
head.insertBefore(style, head.firstChild);
|
|
} else {
|
|
head.appendChild(style);
|
|
}
|
|
} else {
|
|
head.appendChild(style);
|
|
}
|
|
|
|
if (style.styleSheet) {
|
|
style.styleSheet.cssText = css;
|
|
} else {
|
|
style.appendChild(document.createTextNode(css));
|
|
}
|
|
}
|
|
|
|
var css = ".circlepack-viz {\n cursor: move;\n}\n\n.circlepack-viz circle {\n cursor: pointer;\n stroke: lightgrey;\n stroke-opacity: .4;\n opacity: .85;\n transition-property: stroke-opacity, opacity;\n transition-duration: .4s;\n}\n\n.circlepack-viz circle:hover {\n stroke-opacity: 1;\n opacity: 1;\n transition-duration: .05s;\n}\n\n.circlepack-viz text {\n font-size: 12px;\n font-family: sans-serif;\n pointer-events: none;\n dominant-baseline: middle;\n text-anchor: middle;\n fill: #404041;\n}\n\n.circlepack-viz text.light {\n fill: #F7F7F7;\n}\n\n.circlepack-tooltip {\n display: none;\n position: absolute;\n max-width: 320px;\n white-space: nowrap;\n padding: 5px;\n border-radius: 3px;\n font: 12px sans-serif;\n color: #eee;\n background: rgba(0,0,0,0.65);\n pointer-events: none;\n}\n\n.circlepack-tooltip .tooltip-title {\n font-weight: bold;\n text-align: center;\n margin-bottom: 5px;\n}";
|
|
styleInject(css);
|
|
|
|
var LABELS_WIDTH_OPACITY_SCALE = scaleLinear().domain([4, 8]).clamp(true); // px per char
|
|
|
|
var TRANSITION_DURATION = 800;
|
|
var circlepack = Kapsule({
|
|
props: {
|
|
width: {
|
|
"default": window.innerWidth,
|
|
onChange: function onChange(_, state) {
|
|
state.needsReparse = true;
|
|
}
|
|
},
|
|
height: {
|
|
"default": window.innerHeight,
|
|
onChange: function onChange(_, state) {
|
|
state.needsReparse = true;
|
|
}
|
|
},
|
|
data: {
|
|
onChange: function onChange(_, state) {
|
|
state.needsReparse = true;
|
|
}
|
|
},
|
|
children: {
|
|
"default": 'children',
|
|
onChange: function onChange(_, state) {
|
|
state.needsReparse = true;
|
|
}
|
|
},
|
|
sort: {
|
|
onChange: function onChange(_, state) {
|
|
state.needsReparse = true;
|
|
}
|
|
},
|
|
label: {
|
|
"default": function _default(d) {
|
|
return d.name;
|
|
}
|
|
},
|
|
size: {
|
|
"default": 'value',
|
|
onChange: function onChange(_, state) {
|
|
this.zoomReset();
|
|
state.needsReparse = true;
|
|
}
|
|
},
|
|
padding: {
|
|
"default": 4,
|
|
onChange: function onChange(_, state) {
|
|
state.needsReparse = true;
|
|
}
|
|
},
|
|
color: {
|
|
"default": function _default(d) {
|
|
return 'lightgrey';
|
|
}
|
|
},
|
|
minCircleRadius: {
|
|
"default": 3
|
|
},
|
|
excludeRoot: {
|
|
"default": false,
|
|
onChange: function onChange(_, state) {
|
|
state.needsReparse = true;
|
|
}
|
|
},
|
|
showLabels: {
|
|
"default": true
|
|
},
|
|
showTooltip: {
|
|
"default": function _default(d) {
|
|
return true;
|
|
},
|
|
triggerUpdate: false
|
|
},
|
|
tooltipTitle: {
|
|
"default": null,
|
|
triggerUpdate: false
|
|
},
|
|
tooltipContent: {
|
|
"default": function _default(d) {
|
|
return '';
|
|
},
|
|
triggerUpdate: false
|
|
},
|
|
onClick: {
|
|
triggerUpdate: false
|
|
},
|
|
onHover: {
|
|
triggerUpdate: false
|
|
}
|
|
},
|
|
methods: {
|
|
zoomBy: function zoomBy(state, k) {
|
|
state.zoom.zoomBy(k, TRANSITION_DURATION);
|
|
return this;
|
|
},
|
|
zoomReset: function zoomReset(state) {
|
|
state.zoom.zoomReset(TRANSITION_DURATION);
|
|
return this;
|
|
},
|
|
zoomToNode: function zoomToNode(state) {
|
|
var d = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
var node = d.__dataNode;
|
|
|
|
if (node) {
|
|
var ZOOM_REL_PADDING = 0.12;
|
|
var k = Math.max(1, Math.min(state.width, state.height) / (node.r * 2) * (1 - ZOOM_REL_PADDING));
|
|
var tr = {
|
|
k: k,
|
|
x: -Math.max(0, Math.min(state.width * (1 - 1 / k), // Don't pan out of chart boundaries
|
|
node.x - state.width / k / 2 // Center circle in view
|
|
)),
|
|
y: -Math.max(0, Math.min(state.height * (1 - 1 / k), node.y - state.height / k / 2))
|
|
};
|
|
state.zoom.zoomTo(tr, TRANSITION_DURATION);
|
|
}
|
|
|
|
return this;
|
|
},
|
|
_parseData: function _parseData(state) {
|
|
if (state.data) {
|
|
var hierData = hierarchy(state.data, accessorFn(state.children)).sum(accessorFn(state.size));
|
|
|
|
if (state.sort) {
|
|
hierData.sort(state.sort);
|
|
}
|
|
|
|
pack().padding(state.padding).size([state.width, state.height])(hierData);
|
|
hierData.descendants().forEach(function (d, i) {
|
|
d.id = i; // Mark each node with a unique ID
|
|
|
|
d.data.__dataNode = d; // Dual-link data nodes
|
|
});
|
|
state.layoutData = hierData.descendants().filter(state.excludeRoot ? function (d) {
|
|
return d.depth > 0;
|
|
} : function () {
|
|
return true;
|
|
});
|
|
}
|
|
}
|
|
},
|
|
stateInit: function stateInit() {
|
|
return {
|
|
zoom: zoomable()
|
|
};
|
|
},
|
|
init: function init(domNode, state) {
|
|
var _this = this;
|
|
|
|
var el = select(domNode).append('div').attr('class', 'circlepack-viz');
|
|
state.svg = el.append('svg');
|
|
state.canvas = state.svg.append('g'); // tooltips
|
|
|
|
state.tooltip = select('body').append('div').attr('class', 'chart-tooltip circlepack-tooltip'); // tooltip cleanup on unmount
|
|
|
|
domNode.addEventListener('DOMNodeRemoved', function (e) {
|
|
if (e.target === this) {
|
|
state.tooltip.remove();
|
|
}
|
|
});
|
|
state.canvas.on('mousemove', function () {
|
|
state.tooltip.style('left', event.pageX + 'px').style('top', event.pageY + 'px').style('transform', "translate(-".concat(event.offsetX / state.width * 100, "%, 21px)")); // adjust horizontal position to not exceed canvas boundaries
|
|
}); // zoom/pan
|
|
|
|
state.zoom(state.svg).svgEl(state.canvas).onChange(function (tr, prevTr, duration) {
|
|
if (state.showLabels && !duration) {
|
|
// Scale labels immediately if not animating
|
|
state.canvas.selectAll('text').attr('transform', "scale(".concat(1 / tr.k, ")"));
|
|
} // Prevent using transitions when using mouse wheel to zoom
|
|
|
|
|
|
state.skipTransitionsOnce = !duration;
|
|
|
|
state._rerender();
|
|
});
|
|
state.svg.on('click', function () {
|
|
return (state.onClick || _this.zoomReset)(null);
|
|
}) // By default reset zoom when clicking on canvas
|
|
.on('mouseover', function () {
|
|
return state.onHover && state.onHover(null);
|
|
});
|
|
},
|
|
update: function update(state) {
|
|
var _this2 = this;
|
|
|
|
if (state.needsReparse) {
|
|
this._parseData();
|
|
|
|
state.needsReparse = false;
|
|
}
|
|
|
|
state.svg.style('width', state.width + 'px').style('height', state.height + 'px');
|
|
state.zoom.translateExtent([[0, 0], [state.width, state.height]]);
|
|
if (!state.layoutData) return;
|
|
var zoomTr = state.zoom.current();
|
|
var cell = state.canvas.selectAll('.node').data(state.layoutData.filter(function (d) {
|
|
return (// Show only circles in scene that are larger than the threshold
|
|
d.x + d.r > -zoomTr.x / zoomTr.k && d.x - d.r < (state.width - zoomTr.x) / zoomTr.k && d.y + d.r > -zoomTr.y / zoomTr.k && d.y - d.r < (state.height - zoomTr.y) / zoomTr.k && d.r >= state.minCircleRadius / zoomTr.k
|
|
);
|
|
}), function (d) {
|
|
return d.id;
|
|
});
|
|
var nameOf = accessorFn(state.label);
|
|
var colorOf = accessorFn(state.color);
|
|
var animate = !state.skipTransitionsOnce;
|
|
state.skipTransitionsOnce = false;
|
|
var transition$1 = transition().duration(animate ? TRANSITION_DURATION : 0); // Exiting
|
|
|
|
cell.exit().transition(transition$1).remove(); // Entering
|
|
|
|
var newCell = cell.enter().append('g').attr('class', 'node').attr('transform', function (d) {
|
|
return "translate(".concat(d.x, ",").concat(d.y, ")");
|
|
});
|
|
newCell.append('circle').attr('id', function (d) {
|
|
return "circle-".concat(d.id);
|
|
}).attr('r', 0).style('stroke-width', 1).on('click', function (d) {
|
|
event.stopPropagation();
|
|
|
|
(state.onClick || _this2.zoomToNode)(d.data);
|
|
}).on('mouseover', function (d) {
|
|
event.stopPropagation();
|
|
state.onHover && state.onHover(d.data);
|
|
state.tooltip.style('display', state.showTooltip(d.data, d) ? 'inline' : 'none');
|
|
state.tooltip.html("\n <div class=\"tooltip-title\">\n ".concat(state.tooltipTitle ? state.tooltipTitle(d.data, d) : getNodeStack(d).slice(state.excludeRoot ? 1 : 0).map(function (d) {
|
|
return nameOf(d.data);
|
|
}).join(' → '), "\n </div>\n ").concat(state.tooltipContent(d.data, d), "\n "));
|
|
}).on('mouseout', function () {
|
|
state.tooltip.style('display', 'none');
|
|
});
|
|
newCell.append('clipPath').attr('id', function (d) {
|
|
return "clip-".concat(d.id);
|
|
}).append('use').attr('xlink:href', function (d) {
|
|
return "#circle-".concat(d.id);
|
|
});
|
|
var label = newCell.append('g').attr('clip-path', function (d) {
|
|
return "url(#clip-".concat(d.id, ")");
|
|
}).append('g').attr('class', 'label-container').append('text').attr('class', 'path-label'); // Entering + Updating
|
|
|
|
var allCells = cell.merge(newCell);
|
|
allCells.transition(transition$1).attr('transform', function (d) {
|
|
return "translate(".concat(d.x, ",").concat(d.y, ")");
|
|
});
|
|
allCells.select('circle').transition(transition$1).attr('r', function (d) {
|
|
return d.r;
|
|
}).style('fill', function (d) {
|
|
return colorOf(d.data, d.parent);
|
|
}).style('stroke-width', 1 / zoomTr.k);
|
|
allCells.select('g.label-container').style('display', state.showLabels ? null : 'none');
|
|
|
|
if (state.showLabels) {
|
|
// Update previous scale
|
|
var prevK = state.prevK || 1;
|
|
state.prevK = zoomTr.k;
|
|
allCells.select('text.path-label').classed('light', function (d) {
|
|
return !tinycolor(colorOf(d.data, d.parent)).isLight();
|
|
}).text(function (d) {
|
|
return nameOf(d.data);
|
|
}).transition(transition$1).style('opacity', function (d) {
|
|
return LABELS_WIDTH_OPACITY_SCALE(d.r * 2 * zoomTr.k / nameOf(d.data).length);
|
|
}).attrTween('transform', function () {
|
|
var kTr = interpolate(prevK, zoomTr.k);
|
|
return function (t) {
|
|
return "scale(".concat(1 / kTr(t), ")");
|
|
};
|
|
});
|
|
} //
|
|
|
|
|
|
function getNodeStack(d) {
|
|
var stack = [];
|
|
var curNode = d;
|
|
|
|
while (curNode) {
|
|
stack.unshift(curNode);
|
|
curNode = curNode.parent;
|
|
}
|
|
|
|
return stack;
|
|
}
|
|
}
|
|
});
|
|
|
|
export default circlepack;
|
|
|