FeatureEnVi: Visual Analytics for Feature Engineering Using Stepwise Selection and Semi-Automatic Extraction Approaches
https://doi.org/10.1109/TVCG.2022.3141040
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.
1126 lines
37 KiB
1126 lines
37 KiB
<template>
|
|
<div>
|
|
<div id="tree-container" style="min-height: 820px"></div>
|
|
<div id="toolbar">
|
|
<div class="tool">
|
|
<div class="tlabel">Zoom</div>
|
|
<div class="tbuttons">
|
|
<div class="button" data-key="187" title="Zoom In">+</div>
|
|
<div class="button" data-key="189" title="Zoom Out">−</div>
|
|
</div>
|
|
</div>
|
|
<div class="tool">
|
|
<div class="tlabel">Rotate</div>
|
|
<div class="tbuttons">
|
|
<div class="button" data-key="33" title="Rotate CCW" style="font-size:0.9em">↺</div>
|
|
<div class="button" data-key="34" title="Rotate CW" style="font-size:0.9em">↻</div>
|
|
</div>
|
|
</div>
|
|
<div class="tool">
|
|
<div class="tlabel">Select</div>
|
|
<div class="tbuttons">
|
|
<div class="button" data-key="-38" title="Select Previous" style="font-size:0.9em">↥</div>
|
|
<div class="button" data-key="-40" title="Select Next" style="font-size:0.9em">↧</div>
|
|
</div>
|
|
</div>
|
|
<div class="tool">
|
|
<div class="tlabel">View</div>
|
|
<div class="tbuttons">
|
|
<div class="button" data-key="36" title="Center Root">⌂</div>
|
|
<div class="button" data-key="35" title="Center Selected" style="font-size:0.8em">◉</div>
|
|
</div>
|
|
</div>
|
|
<div class="tool">
|
|
<div class="tlabel">Toggle</div>
|
|
<div class="tbuttons">
|
|
<div class="button" data-key="32" title="Toggle Node">1</div>
|
|
<div class="button" data-key="13" title="Toggle Level">⊕</div>
|
|
<div class="button" data-key="191" title="Toggle Root">/</div>
|
|
</div>
|
|
</div>
|
|
<div class="tool">
|
|
<div class="tlabel" style="text-align:left" title="Change Root"> Navigate</div>
|
|
<div id="selection" class="tlabel"></div>
|
|
</div>
|
|
<div class="tool">
|
|
<div id="help">Labels</div>
|
|
<div id="legendTarget" style="min-width: 492.7px; min-height: 50px; margin-top:-10px"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="contextmenu">
|
|
<div data-key="32"><span class="expcol">Expand</span> Node</div>
|
|
<div data-key="13">Expand 1 Level</div>
|
|
<div data-key="-13">Expand Full Tree</div>
|
|
<div data-key="35">Center This Node</div>
|
|
<div data-key="36">Center Root</div>
|
|
<div data-key="191">Set Root</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { EventBus } from '../main.js'
|
|
import * as greadability from '../greadability.js'
|
|
import * as Plotly from 'plotly.js'
|
|
import * as d3Base from 'd3'
|
|
import $ from 'jquery'
|
|
|
|
// attach all d3 plugins to the d3 library
|
|
const d3v5 = Object.assign(d3Base)
|
|
|
|
export default {
|
|
name: 'FeatureSpaceOverview',
|
|
data () {
|
|
return {
|
|
colorsReceive: [],
|
|
activeLeaf: -1,
|
|
overallData: [],
|
|
keepRoot: 1
|
|
}
|
|
},
|
|
methods: {
|
|
reset () {
|
|
var svg = d3.select("#tree-container");
|
|
svg.selectAll("*").remove();
|
|
var svg = d3.select("#legendTarget")
|
|
svg.selectAll("*").remove();
|
|
},
|
|
// Get JSON data
|
|
initializeRadialTree() {
|
|
var svg = d3.select("#legendTarget")
|
|
svg.selectAll("*").remove();
|
|
var svg = d3.select("#tree-container");
|
|
svg.selectAll("*").remove();
|
|
|
|
var features = this.colorsReceive
|
|
var activeLeafLoc = this.activeLeaf
|
|
var listofNodes = this.overallData[34]
|
|
|
|
var featuresQuad1 = []
|
|
var featuresQuad2 = []
|
|
var featuresQuad3 = []
|
|
var featuresQuad4 = []
|
|
var featuresQuad5 = []
|
|
|
|
for (let i = 0; i < features[4].length; i++) {
|
|
featuresQuad1.push({"name": features[0][i].key,
|
|
"children": [
|
|
{"name": "r", "lin_color": features[0][i].value[0].valueIns},
|
|
{"name": "r_E", "lin_color": features[0][i].value[1].valueIns},
|
|
{"name": "r_2", "lin_color": features[0][i].value[2].valueIns},
|
|
{"name": "r_10", "lin_color": features[0][i].value[3].valueIns},
|
|
],
|
|
"lin_color": features[0][i].value[0].valueIns+features[0][i].value[1].valueIns+features[0][i].value[3].valueIns+features[0][i].value[3].valueIns
|
|
})
|
|
featuresQuad2.push({"name": features[1][i].key,
|
|
"children": [
|
|
{"name": "r", "lin_color": features[1][i].value[0].valueIns},
|
|
{"name": "r_E", "lin_color": features[1][i].value[1].valueIns},
|
|
{"name": "r_2", "lin_color": features[1][i].value[2].valueIns},
|
|
{"name": "r_10", "lin_color": features[1][i].value[3].valueIns},
|
|
],
|
|
"lin_color": features[1][i].value[0].valueIns+features[1][i].value[1].valueIns+features[1][i].value[3].valueIns+features[1][i].value[3].valueIns
|
|
})
|
|
featuresQuad3.push({"name": features[2][i].key,
|
|
"children": [
|
|
{"name": "r", "lin_color": features[2][i].value[0].valueIns},
|
|
{"name": "r_E", "lin_color": features[2][i].value[1].valueIns},
|
|
{"name": "r_2", "lin_color": features[2][i].value[2].valueIns},
|
|
{"name": "r_10", "lin_color": features[2][i].value[3].valueIns},
|
|
],
|
|
"lin_color": features[2][i].value[0].valueIns+features[2][i].value[1].valueIns+features[2][i].value[3].valueIns+features[2][i].value[3].valueIns
|
|
})
|
|
featuresQuad4.push({"name": features[3][i].key,
|
|
"children": [
|
|
{"name": "r", "lin_color": features[3][i].value[0].valueIns},
|
|
{"name": "r_E", "lin_color": features[3][i].value[1].valueIns},
|
|
{"name": "r_2", "lin_color": features[3][i].value[2].valueIns},
|
|
{"name": "r_10", "lin_color": features[3][i].value[3].valueIns},
|
|
],
|
|
"lin_color": features[3][i].value[0].valueIns+features[3][i].value[1].valueIns+features[3][i].value[3].valueIns+features[3][i].value[3].valueIns
|
|
})
|
|
featuresQuad5.push({"name": features[4][i].key,
|
|
"children": [
|
|
{"name": "r", "lin_color": features[4][i].value[0].valueIns},
|
|
{"name": "r_E", "lin_color": features[4][i].value[1].valueIns},
|
|
{"name": "r_2", "lin_color": features[4][i].value[2].valueIns},
|
|
{"name": "r_10", "lin_color": features[4][i].value[3].valueIns},
|
|
],
|
|
"lin_color": features[4][i].value[0].valueIns+features[4][i].value[1].valueIns+features[4][i].value[3].valueIns+features[4][i].value[3].valueIns
|
|
})
|
|
}
|
|
|
|
var spaceSlice1=0
|
|
var spaceSlice2 = 0
|
|
var spaceSlice3 = 0
|
|
var spaceSlice4 = 0
|
|
var spaceSlice5 = 0
|
|
|
|
for (let i = 0; i < featuresQuad5.length; i++) {
|
|
spaceSlice4 = spaceSlice4 + featuresQuad4[i].lin_color
|
|
spaceSlice5 = spaceSlice5 + featuresQuad5[i].lin_color
|
|
spaceSlice1 = spaceSlice1 + featuresQuad1[i].lin_color
|
|
spaceSlice2 = spaceSlice2 + featuresQuad2[i].lin_color
|
|
spaceSlice3 = spaceSlice3 + featuresQuad3[i].lin_color
|
|
}
|
|
|
|
var treeData = {
|
|
"name": "Data",
|
|
"children": [
|
|
{"name": "Worst",
|
|
"children": featuresQuad4,
|
|
"lin_color": spaceSlice4
|
|
},
|
|
{"name": "All",
|
|
"children": featuresQuad5,
|
|
"lin_color": spaceSlice5
|
|
},
|
|
{"name": "Best",
|
|
"children": featuresQuad1,
|
|
"lin_color": spaceSlice1
|
|
},
|
|
{"name": "Good",
|
|
"children": featuresQuad2,
|
|
"lin_color": spaceSlice2
|
|
},
|
|
{"name": "Bad",
|
|
"children": featuresQuad3,
|
|
"lin_color": spaceSlice3
|
|
},
|
|
]
|
|
}
|
|
|
|
var DURATION = 700; // d3 animation duration
|
|
var STAGGERN = 4; // delay for each node
|
|
var STAGGERD = 200; // delay for each depth
|
|
var NODE_DIAMETER = 8; // diameter of circular nodes
|
|
var MIN_ZOOM = 0.5; // minimum zoom allowed
|
|
var MAX_ZOOM = 10; // maximum zoom allowed
|
|
var HAS_CHILDREN_COLOR = 'lightsteelblue';
|
|
var SELECTED_COLOR = 'yellow'; // color of selected node
|
|
var ZOOM_INC = 0.04; // zoom factor per animation frame
|
|
var PAN_INC = 3; // pan per animation frame
|
|
var ROT_INC = 0.3; // rotation per animation frame
|
|
|
|
var counter = 0; // node ids
|
|
var curNode; // currently selected node
|
|
var curPath; // array of nodes in the path to the currently selected node
|
|
|
|
// size of the diagram
|
|
var width = 820;
|
|
var height = 820;
|
|
|
|
// current pan, zoom, and rotation
|
|
var curX = width / 2;
|
|
var curY = height / 2;
|
|
var curZ = 1.28; // current zoom
|
|
var curR = 270; // current rotation
|
|
|
|
// keyboard key codes
|
|
var KEY_PLUS = 187; // + (zoom in)
|
|
var KEY_MINUS = 189; // - (zoom out)
|
|
var KEY_SLASH = 191; // / (slash)
|
|
var KEY_PAGEUP = 33; // (rotate CCW)
|
|
var KEY_PAGEDOWN = 34; // (rotate CW)
|
|
var KEY_LEFT = 37; // left arrow
|
|
var KEY_UP = 38; // up arrow
|
|
var KEY_RIGHT = 39; // right arrow
|
|
var KEY_DOWN = 40; // down arrow
|
|
var KEY_SPACE = 32; // (expand node)
|
|
var KEY_RETURN = 13; // (expand tree)
|
|
var KEY_HOME = 36; // (center root)
|
|
var KEY_END = 35; // (center selection)
|
|
|
|
// d3 diagonal projection for use by the node paths
|
|
var diagonal= d3.svg.diagonal.radial()
|
|
.projection(function(d) {
|
|
return [d.y, d.x / 180 * Math.PI];
|
|
});
|
|
|
|
// d3 tree layout
|
|
var tree = d3.layout.tree()
|
|
// .nodeSize([4.5, 120])
|
|
.size([360, Math.min(width, height) / 2 - 120])
|
|
.separation(function(a, b) {
|
|
return a.depth === 0 ? 1 : (a.parent === b.parent ? 1 : 2) / a.depth;
|
|
});
|
|
|
|
// define the svgBase, attaching a class for styling and the zoomListener
|
|
var svgBase = d3.select('#tree-container').append('svg')
|
|
.attr('width', width)
|
|
.attr('height', height)
|
|
.on('mousedown', mousedown);
|
|
|
|
// Group which holds all nodes and manages pan, zoom, rotate
|
|
var svgGroup = svgBase.append('g')
|
|
.attr('transform', 'translate(' + curX + ',' + curY + ')');
|
|
|
|
d3.select(document) // set up document events
|
|
.on('wheel', wheel) // zoom, rotate
|
|
.on('keydown', keydown)
|
|
.on('keyup', keyup);
|
|
d3.select(window).on('resize', resize);
|
|
d3.selectAll('.button')
|
|
.on('mousedown', tooldown)
|
|
.on('mouseup', toolup);
|
|
d3.select('#selection').on('mousedown', switchroot);
|
|
d3.select('#contextmenu').on('mouseup', menuSelection);
|
|
|
|
// Define the data root
|
|
var root = treeData;
|
|
root.x0 = curY;
|
|
root.y0 = 0;
|
|
selectNode(treeData.children[this.keepRoot]); // current selected node
|
|
|
|
// Collapse all children of root's children before rendering
|
|
// if (root.children) {
|
|
// root.children.forEach(function(child) {
|
|
// collapseTree(child);
|
|
// });
|
|
// }
|
|
|
|
update(root, true); // Layout the tree initially and center on the root node
|
|
|
|
// update the tree
|
|
// source - source node of the update
|
|
// transition - whether to do a transition
|
|
function update(source, transition) {
|
|
|
|
var duration = transition ?
|
|
(d3.event && d3.event.altKey ? DURATION * 4 : DURATION) : 0;
|
|
|
|
// Compute the new tree layout.
|
|
var nodes = tree.nodes(root);
|
|
var links = tree.links(nodes);
|
|
|
|
// Update the view
|
|
svgGroup.transition().duration(duration)
|
|
.attr('transform',
|
|
'rotate(' + curR + ' ' + curX + ' ' + curY +
|
|
')translate(' + curX + ' ' + curY +
|
|
')scale(' + curZ + ')');
|
|
|
|
// Update the nodes…
|
|
var node = svgGroup.selectAll('g.node')
|
|
.data(nodes, function(d) {
|
|
return d.id || (d.id = ++counter);
|
|
});
|
|
|
|
// Enter any new nodes at the parent's previous position
|
|
var nodeEnter = node.enter().insert('g', ':first-child')
|
|
.attr('class', 'node')
|
|
.attr('transform', 'rotate(' + (source.x0 - 90) + ')translate(' + source.y0 + ')')
|
|
.on('click', click)
|
|
.on('dblclick', dblclick)
|
|
.on('contextmenu', showContextMenu);
|
|
// .on('mousedown', suppress);
|
|
|
|
//d3.select('#some-id').dispatch('click');
|
|
|
|
nodeEnter.append('circle')
|
|
.attr('r', 1e-6)
|
|
.style('fill', function(d) {
|
|
return d._children ? HAS_CHILDREN_COLOR : 'white';
|
|
});
|
|
|
|
nodeEnter.append('text')
|
|
.text(function(d) {
|
|
return d.name;
|
|
})
|
|
.style('opacity', 0.9)
|
|
.style('fill-opacity', 0)
|
|
.attr('transform', function() {
|
|
return ((source.x0 + curR) % 360 <= 180 ?
|
|
'translate(8)scale(' :
|
|
'rotate(180)translate(-8)scale('
|
|
) + reduceZ() + ')';
|
|
});
|
|
|
|
// update existing graph nodes
|
|
|
|
// Change the circle fill depending on whether it has children and is collapsed
|
|
node.select('circle')
|
|
.attr('r', NODE_DIAMETER * reduceZ())
|
|
.style('fill', function(d) {
|
|
if (d.name == 'Data') {
|
|
return d._children ? 'white' : 'white';
|
|
} else {
|
|
if (activeLeafLoc != -1) {
|
|
if (d.name == listofNodes[activeLeafLoc]) {
|
|
return d._children ? 'yellow' : 'yellow'
|
|
}
|
|
else {
|
|
return d._children ? '#D3D3D3' : '#D3D3D3'
|
|
}
|
|
} else {
|
|
return d._children ? '#D3D3D3' : '#D3D3D3'
|
|
}
|
|
}
|
|
}).attr('stroke', function(d) {
|
|
if(d.name == 'Data') {
|
|
return d.selected ? SELECTED_COLOR : 'white';
|
|
} else {
|
|
return d.selected ? SELECTED_COLOR : 'black';
|
|
}
|
|
}).attr('stroke-width', function(d) {
|
|
if(d.name == 'Data') {
|
|
return d.selected ? 0 : 0;
|
|
} else if (d.name == 'All' || d.name == 'Best' || d.name == 'Good' || d.name == 'Bad' || d.name == 'Worst'){
|
|
return d.selected ? 2 : 2;
|
|
} else {
|
|
return d.selected ? 0 : 0;
|
|
}
|
|
});
|
|
|
|
node.select('text')
|
|
.attr('text-anchor', function(d) {
|
|
return (d.x + curR) % 360 <= 180 ? 'start' : 'end';
|
|
}).attr('transform', function(d) {
|
|
return ((d.x + curR) % 360 <= 180 ?
|
|
'translate(8)scale(' :
|
|
'rotate(180)translate(-8)scale('
|
|
) + reduceZ() +')';
|
|
}).attr('fill', function(d) {
|
|
return d.selected ? 'black' : 'black';
|
|
}).attr('dy', '.35em');
|
|
|
|
var nodeUpdate = node.transition().duration(duration)
|
|
.delay( transition ? function(d, i) {
|
|
return i * STAGGERN +
|
|
Math.abs(d.depth - curNode.depth) * STAGGERD; } : 0)
|
|
.attr('transform', function(d) {
|
|
return 'rotate(' + (d.x - 90) + ')translate(' + d.y + ')';
|
|
});
|
|
|
|
nodeUpdate.select('circle')
|
|
.attr('r', NODE_DIAMETER * reduceZ());
|
|
// .style('fill', function(d) {
|
|
// return d._children ? HAS_CHILDREN_COLOR : 'white';
|
|
// });
|
|
|
|
nodeUpdate.select('text')
|
|
.style('fill-opacity', 1);
|
|
|
|
// Transition exiting nodes to the parent's new position and remove
|
|
var nodeExit = node.exit().transition().duration(duration)
|
|
.delay( transition ? function(d, i) {
|
|
return i * STAGGERN; } : 0)
|
|
.attr('transform', function() {
|
|
return 'rotate(' + (source.x - 90) +')translate(' + source.y + ')';
|
|
}).remove();
|
|
|
|
nodeExit.select('circle').attr('r', 0);
|
|
nodeExit.select('text').style('fill-opacity', 0);
|
|
|
|
// Update the links…
|
|
var link = svgGroup.selectAll('path.link')
|
|
.data(links, function(d) {
|
|
return d.target.id;
|
|
});
|
|
|
|
// Enter any new links at the parent's previous position
|
|
link.enter().insert('path', 'g')
|
|
.attr('class', 'link')
|
|
.attr('d', function() {
|
|
var o = {
|
|
x: source.x0,
|
|
y: source.y0
|
|
};
|
|
return diagonal({
|
|
source: o,
|
|
target: o
|
|
});
|
|
}).style("stroke", function(d){
|
|
if (d.target.lin_color > 0) {
|
|
return '#33a02c'
|
|
} else if (d.target.lin_color < 0) {
|
|
return '#e31a1c'
|
|
} else {
|
|
return '#D3D3D3'
|
|
}
|
|
})
|
|
|
|
// Transition links to their new position
|
|
link.transition().duration(duration)
|
|
.delay( transition ? function(d, i) {
|
|
return i * STAGGERN +
|
|
Math.abs(d.source.depth - curNode.depth) * STAGGERD;
|
|
// Math.max(0, d.source.depth - curNode.depth) * STAGGERD;
|
|
} : 0)
|
|
.attr('d', diagonal);
|
|
|
|
// Transition exiting nodes to the parent's new position
|
|
link.exit().transition().duration(duration)
|
|
.attr('d', function() {
|
|
var o = {
|
|
x: source.x0,
|
|
y: source.y0
|
|
};
|
|
return diagonal({
|
|
source: o,
|
|
target: o
|
|
});
|
|
}).remove();
|
|
|
|
// Stash the old positions for transition
|
|
nodes.forEach(function(d) {
|
|
d.x0 = d.x;
|
|
d.y0 = d.y;
|
|
});
|
|
} // end update
|
|
|
|
// Helper functions for collapsing and expanding nodes
|
|
|
|
// Toggle expand / collapse
|
|
function toggle(d) {
|
|
if (d.children) {
|
|
d._children = d.children;
|
|
d.children = null;
|
|
} else if (d._children) {
|
|
d.children = d._children;
|
|
d._children = null;
|
|
}
|
|
}
|
|
|
|
function toggleTree(d) {
|
|
if (d.children) {
|
|
collapseTree(d);
|
|
} else {
|
|
expandTree(d);
|
|
}
|
|
}
|
|
|
|
function expand(d) {
|
|
if (d._children) {
|
|
d.children = d._children;
|
|
d._children = null;
|
|
}
|
|
}
|
|
|
|
// expand all children, whether expanded or collapsed
|
|
function expandTree(d) {
|
|
if (d._children) {
|
|
d.children = d._children;
|
|
d._children = null;
|
|
}
|
|
if (d.children) {
|
|
d.children.forEach(expandTree);
|
|
}
|
|
}
|
|
|
|
function collapse(d) {
|
|
if (d.children) {
|
|
d._children = d.children;
|
|
d.children = null;
|
|
}
|
|
}
|
|
|
|
// collapse all children
|
|
function collapseTree(d) {
|
|
if (d.children) {
|
|
d._children = d.children;
|
|
d.children = null;
|
|
}
|
|
if (d._children) {
|
|
d._children.forEach(collapseTree);
|
|
}
|
|
}
|
|
|
|
// expand one level of tree
|
|
function expand1Level(d) {
|
|
var q = [d]; // non-recursive
|
|
var cn;
|
|
var done = null;
|
|
while (q.length > 0) {
|
|
cn = q.shift();
|
|
if (done !== null && done < cn.depth) { return; }
|
|
if (cn._children) {
|
|
done = cn.depth;
|
|
cn.children = cn._children;
|
|
cn._children = null;
|
|
cn.children.forEach(collapse);
|
|
}
|
|
if (cn.children) { q = q.concat(cn.children); }
|
|
}
|
|
// no nodes to open
|
|
}
|
|
|
|
// highlight selected node
|
|
function selectNode(node) {
|
|
if (curNode) {
|
|
delete curNode.selected;
|
|
}
|
|
curNode = node;
|
|
curNode.selected = true;
|
|
curPath = []; // filled in by fullpath
|
|
d3.select('#selection').html(fullpath(node));
|
|
}
|
|
|
|
// for displaying full path of node in tree
|
|
function fullpath(d, idx) {
|
|
idx = idx || 0;
|
|
curPath.push(d);
|
|
return '/<span class="nodepath'+
|
|
'" data-sel="'+ idx +'" title="Set Root to '+ d.name +'">' +
|
|
d.name + '</span>';
|
|
}
|
|
|
|
// d3 event handlers
|
|
|
|
function switchroot() {
|
|
d3.event.preventDefault();
|
|
var pathelms = document.querySelectorAll('#selection .nodepath');
|
|
for (var i = 0; i < pathelms.length; i++) {
|
|
pathelms[i].classList.remove('highlight');
|
|
}
|
|
var target = d3.event.target;
|
|
var node = curPath[+target.dataset.sel];
|
|
if (d3.event.shiftKey) {
|
|
if (curNode !== node) {
|
|
selectNode(node);
|
|
}
|
|
} else {
|
|
root = node;
|
|
target.classList.add('highlight');
|
|
}
|
|
update(root, true);
|
|
}
|
|
|
|
function resize() { // window resize
|
|
var oldwidth = width;
|
|
var oldheight = height;
|
|
width = window.innerWidth - 20;
|
|
height = window.innerHeight - 20;
|
|
tree.size([360, Math.min(width, height) / 2 - 120]);
|
|
svgBase.attr('width', width).attr('height', height);
|
|
curX += (width - oldwidth) / 2;
|
|
curY += (height - oldheight) / 2;
|
|
svgGroup.attr('transform', 'rotate(' + curR + ' ' + curX + ' ' + curY +
|
|
')translate(' + curX + ' ' + curY + ')scale(' + curZ + ')');
|
|
update(root);
|
|
}
|
|
|
|
function click(d) { // select node
|
|
if (d3.event.defaultPrevented || d === curNode) { return; } // suppressed
|
|
d3.event.preventDefault();
|
|
selectNode(d);
|
|
update(d);
|
|
var sendSliceID = 4
|
|
var rootID = 1
|
|
if (d.name == "Best") {
|
|
sendSliceID = 0
|
|
rootID = 2
|
|
} else if (d.name == "Good") {
|
|
sendSliceID = 1
|
|
rootID = 3
|
|
} else if (d.name == "Bad") {
|
|
sendSliceID = 2
|
|
rootID = 4
|
|
} else if (d.name == "Worst") {
|
|
sendSliceID = 3
|
|
rootID = 0
|
|
} else {
|
|
sendSliceID = 4
|
|
rootID = 1
|
|
}
|
|
EventBus.$emit('keepRootFun', rootID)
|
|
EventBus.$emit('updateSlice', sendSliceID)
|
|
}
|
|
|
|
function dblclick(d) { // Toggle children of node
|
|
if (d3.event.defaultPrevented) { return; } // click suppressed
|
|
d3.event.preventDefault();
|
|
if (d3.event.shiftKey) {
|
|
expand1Level(d); // expand node by one level
|
|
} else {
|
|
toggle(d);
|
|
}
|
|
update(d, true);
|
|
}
|
|
|
|
function tooldown(d) { // tool button pressed
|
|
d3.event.preventDefault();
|
|
d3.select(d3.event.target).on('mouseout', toolup);
|
|
var key = +d3.event.target.dataset.key;
|
|
keydown(Math.abs(key), key < 0 || d3.event.shiftKey);
|
|
}
|
|
|
|
function toolup() { // tool button released
|
|
d3.event.preventDefault();
|
|
d3.select(d3.event.target).on('mouseout', null);
|
|
keyup(Math.abs(+d3.event.target.dataset.key));
|
|
}
|
|
|
|
// right click, show context menu and select this node
|
|
function showContextMenu(d) {
|
|
d3.event.preventDefault();
|
|
d3.selectAll('.expcol').text(d.children ? 'Collapse' : 'Expand');
|
|
d3.select('#contextmenu').style({
|
|
left: (d3.event.pageX + 3) + 'px',
|
|
top: (d3.event.pageY + 8) + 'px',
|
|
display: 'block'
|
|
});
|
|
d3.select(document).on('mouseup', hideContextMenu);
|
|
selectNode(d);
|
|
update(d);
|
|
}
|
|
|
|
function hideContextMenu() {
|
|
d3.select('#contextmenu').style('display', 'none');
|
|
d3.select(document).on('mouseup', null);
|
|
}
|
|
|
|
function menuSelection() {
|
|
d3.event.preventDefault();
|
|
var key = +d3.event.target.dataset.key;
|
|
keydown(Math.abs(key), key < 0 || d3.event.shiftKey);
|
|
}
|
|
|
|
var startposX, startposY; // initial position on mouse button down for pan
|
|
|
|
function mousedown() { // pan
|
|
d3.event.preventDefault();
|
|
if (d3.event.which !== 1 || d3.event.ctrlKey) { return; } // ingore other mouse buttons
|
|
startposX = curX - d3.event.clientX;
|
|
startposY = curY - d3.event.clientY;
|
|
d3.select(document).on('mousemove', mousemove, true);
|
|
d3.select(document).on('mouseup', mouseup, true);
|
|
}
|
|
|
|
function mousemove() {
|
|
d3.event.preventDefault();
|
|
curX = startposX + d3.event.clientX;
|
|
curY = startposY + d3.event.clientY;
|
|
setview();
|
|
}
|
|
|
|
function mouseup() {
|
|
d3.select(document).on('mousemove', null);
|
|
d3.select(document).on('mouseup', null);
|
|
}
|
|
|
|
var keysdown = []; // which keys are currently down
|
|
var moveX = 0, moveY = 0, moveZ = 0, moveR = 0; // animations
|
|
var aniRequest = null;
|
|
|
|
function wheel() { // mousewheel
|
|
var dz, newZ;
|
|
var slow = d3.event.altKey ? 0.25 : 1;
|
|
if (d3.event.wheelDeltaY !== 0) { // up-down
|
|
dz = Math.pow(1.2, d3.event.wheelDeltaY * 0.001 * slow);
|
|
newZ = limitZ(curZ * dz);
|
|
dz = newZ / curZ;
|
|
curZ = newZ;
|
|
|
|
curX -= (d3.event.clientX - curX) * (dz - 1);
|
|
curY -= (d3.event.clientY - curY) * (dz - 1);
|
|
setview();
|
|
}
|
|
if (d3.event.wheelDeltaX !== 0) { // left-right
|
|
curR = limitR(curR + d3.event.wheelDeltaX * 0.01 * slow);
|
|
update(root);
|
|
}
|
|
}
|
|
|
|
// keyboard shortcuts
|
|
function keydown(key, shift) {
|
|
if (!key) {
|
|
key = d3.event.which; // fake key
|
|
shift = d3.event.shiftKey;
|
|
}
|
|
var parch; // parent's children
|
|
var slow = d3.event.altKey ? 0.25 : 1;
|
|
if (keysdown.indexOf(key) >= 0) { return; } // defeat auto repeat
|
|
switch (key) {
|
|
case KEY_PLUS: // zoom in
|
|
moveZ = ZOOM_INC * slow;
|
|
break;
|
|
case KEY_MINUS: // zoom out
|
|
moveZ = -ZOOM_INC * slow;
|
|
break;
|
|
case KEY_SLASH: // toggle root to selection
|
|
root = root === curNode ? treeData : curNode;
|
|
update(root, true);
|
|
curPath = []; // filled in by fullpath
|
|
d3.select('#selection').html(fullpath(curNode));
|
|
return;
|
|
case KEY_PAGEUP: // rotate counterclockwise
|
|
moveR = -ROT_INC * slow;
|
|
break;
|
|
case KEY_PAGEDOWN: // zoom out
|
|
moveR = ROT_INC * slow; // rotate clockwise
|
|
break;
|
|
case KEY_LEFT: // left arrow
|
|
if (shift) { // move selection to parent
|
|
if (!curNode) {
|
|
selectNode(root);
|
|
} else if (curNode.parent) {
|
|
selectNode(curNode.parent);
|
|
}
|
|
update(curNode);
|
|
return;
|
|
}
|
|
moveX = -PAN_INC * slow;
|
|
break;
|
|
case KEY_UP: // up arrow
|
|
if (shift) { // move selection to previous child
|
|
if (!curNode) {
|
|
selectNode(root);
|
|
} else if (curNode.parent) {
|
|
parch = curNode.parent.children;
|
|
selectNode(parch[(parch.indexOf(curNode) +
|
|
parch.length - 1) % parch.length]);
|
|
}
|
|
update(curNode);
|
|
return;
|
|
}
|
|
moveY = -PAN_INC * slow;
|
|
break;
|
|
case KEY_RIGHT: // right arrow
|
|
if (shift) { // move selection to first/last child
|
|
if (!curNode) {
|
|
selectNode(root);
|
|
} else {
|
|
if (curNode.children) {
|
|
selectNode(curNode.children[d3.event.altKey ?
|
|
curNode.children.length - 1 : 0]);
|
|
}
|
|
}
|
|
update(curNode);
|
|
return;
|
|
}
|
|
moveX = PAN_INC * slow;
|
|
break;
|
|
case KEY_DOWN: // down arrow
|
|
if (shift) { // move selection to next child
|
|
if (!curNode) {
|
|
selectNode(root);
|
|
} else if (curNode.parent) {
|
|
parch = curNode.parent.children;
|
|
selectNode(parch[(parch.indexOf(curNode) + 1) % parch.length]);
|
|
}
|
|
update(curNode);
|
|
return;
|
|
}
|
|
moveY = PAN_INC * slow;
|
|
break;
|
|
case KEY_SPACE: // expand/collapse node
|
|
if (!curNode) {
|
|
selectNode(root);
|
|
}
|
|
toggle(curNode);
|
|
update(curNode, true);
|
|
return;
|
|
case KEY_RETURN: // expand/collapse tree
|
|
if (!curNode) {
|
|
selectNode(root);
|
|
}
|
|
if (shift) {
|
|
expandTree(curNode);
|
|
} else {
|
|
expand1Level(curNode);
|
|
}
|
|
update(curNode, true);
|
|
return;
|
|
case KEY_HOME: // reset transform
|
|
if (shift) {
|
|
root = treeData;
|
|
}
|
|
curX = width / 2;
|
|
curY = height / 2;
|
|
curR = limitR(90 - root.x);
|
|
curZ = 1;
|
|
update(root, true);
|
|
return;
|
|
case KEY_END: // zoom to selection
|
|
if (!curNode) { return; }
|
|
curX = width / 2 - curNode.y * curZ;
|
|
curY = height / 2;
|
|
curR = limitR(90 - curNode.x);
|
|
update(curNode, true);
|
|
return;
|
|
default: return; // ignore other keys
|
|
} // break jumps to here
|
|
keysdown.push(key);
|
|
// start animation if anything happening
|
|
if (keysdown.length > 0 && aniRequest === null) {
|
|
aniRequest = requestAnimationFrame(frame);
|
|
}
|
|
}
|
|
|
|
function keyup(key) {
|
|
key = key || d3.event.which;
|
|
var pos = keysdown.indexOf(key);
|
|
if (pos < 0) { return; }
|
|
|
|
switch (key) {
|
|
case KEY_PLUS: // zoom out
|
|
case KEY_MINUS: // zoom in
|
|
moveZ = 0;
|
|
break;
|
|
case KEY_PAGEUP: // rotate CCW
|
|
case KEY_PAGEDOWN: // rotate CW
|
|
moveR = 0;
|
|
break;
|
|
case KEY_LEFT: // left arrow
|
|
case KEY_RIGHT: // right arrow
|
|
moveX = 0;
|
|
break;
|
|
case KEY_UP: // up arrow
|
|
case KEY_DOWN: // down arrow
|
|
moveY = 0;
|
|
break;
|
|
}
|
|
keysdown.splice(pos, 1); // remove key
|
|
if (keysdown.length > 0 || aniRequest === null) { return; }
|
|
cancelAnimationFrame(aniRequest);
|
|
aniRequest = aniTime = null;
|
|
}
|
|
|
|
var aniTime = null;
|
|
|
|
// update animation frame
|
|
function frame(frametime) {
|
|
var diff = aniTime ? (frametime - aniTime) / 16 : 0;
|
|
aniTime = frametime;
|
|
|
|
var dz = Math.pow(1.2, diff * moveZ);
|
|
var newZ = limitZ(curZ * dz);
|
|
dz = newZ / curZ;
|
|
curZ = newZ;
|
|
curX += diff * moveX - (width / 2- curX) * (dz - 1);
|
|
curY += diff * moveY - (height / 2 - curY) * (dz - 1);
|
|
curR = limitR(curR + diff * moveR);
|
|
setview();
|
|
aniRequest = requestAnimationFrame(frame);
|
|
}
|
|
|
|
// enforce zoom extent
|
|
function limitZ(z) {
|
|
return Math.max(Math.min(z, MAX_ZOOM), MIN_ZOOM);
|
|
}
|
|
|
|
// keep rotation between 0 and 360
|
|
function limitR(r) {
|
|
return (r + 360) % 360;
|
|
}
|
|
|
|
// limit size of text and nodes as scale increases
|
|
function reduceZ() {
|
|
return Math.pow(1.1, -curZ);
|
|
}
|
|
|
|
// set view with no animation
|
|
function setview() {
|
|
svgGroup.attr('transform', 'rotate(' + curR + ' ' + curX + ' ' + curY +
|
|
')translate(' + curX + ' ' + curY + ')scale(' + curZ + ')');
|
|
svgGroup.selectAll('text')
|
|
.attr('text-anchor', function(d) {
|
|
return (d.x + curR) % 360 <= 180 ? 'start' : 'end';
|
|
})
|
|
.attr('transform', function(d) {
|
|
return ((d.x + curR) % 360 <= 180 ?
|
|
'translate(8)scale(' :
|
|
'rotate(180)translate(-8)scale('
|
|
) + reduceZ() +')';
|
|
});
|
|
svgGroup.selectAll('circle').attr('r', NODE_DIAMETER * reduceZ());
|
|
}
|
|
|
|
var legendRectSize = 14;
|
|
var legendSpacing = 3;
|
|
var color = d3v5.scaleOrdinal(d3v5.schemeDark2)
|
|
var labelsData = JSON.parse(this.overallData[1])
|
|
|
|
var svgLegend = d3v5.select('#legendTarget').append('svg')
|
|
.attr('width', 130)
|
|
.attr('height', 60)
|
|
|
|
var legend = svgLegend.selectAll('.legend') // NEW
|
|
.data(labelsData) // NEW
|
|
.enter() // NEW
|
|
.append('g') // NEW
|
|
.attr('class', 'legend') // NEW
|
|
.attr('transform', function(d, i) { // NEW
|
|
var height = legendRectSize + legendSpacing; // NEW
|
|
var offset = height * color.domain().length / 2;
|
|
var horz = 25 // NEW
|
|
var vert = i * height - offset; // NEW
|
|
return 'translate(' + horz + ',' + vert + ')'; // NEW
|
|
}); // NEW
|
|
|
|
legend.append('rect')
|
|
.attr('width', legendRectSize)
|
|
.attr('height', legendRectSize)
|
|
.style('fill', function (d) { return color(d) });
|
|
|
|
legend.append('text')
|
|
.attr('class', 'legendLab')
|
|
.attr('x', legendRectSize + legendSpacing)
|
|
.attr('y', legendRectSize - legendSpacing)
|
|
.text(function(d) { return d; });
|
|
}
|
|
},
|
|
mounted () {
|
|
|
|
EventBus.$on('keepRootFun', data => { this.keepRoot = data })
|
|
EventBus.$on('quad', data => { this.overallData = data })
|
|
|
|
EventBus.$on('overviewCall', data => { this.colorsReceive = data })
|
|
EventBus.$on('overviewCall', this.initializeRadialTree)
|
|
|
|
EventBus.$on('brushLink', data => { this.activeLeaf = data })
|
|
EventBus.$on('brushLink', this.initializeRadialTree)
|
|
|
|
EventBus.$on('reset', this.reset)
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
|
|
text {
|
|
font-family: sans-serif;
|
|
}
|
|
|
|
svg {
|
|
display: block;
|
|
}
|
|
|
|
#toolbar {
|
|
display: -webkit-box;
|
|
display: moz-box-flex;
|
|
display: -ms-flexbox;
|
|
display: -webkit-flex;
|
|
flex-wrap: wrap;
|
|
top: 10px;
|
|
margin-top: 25px;
|
|
left: 10px;
|
|
font-family: sans-serif;
|
|
box-shadow: 1px 1px 3px;
|
|
background-color: #ECEFF1;
|
|
}
|
|
.tool {
|
|
display: flex;
|
|
flex-direction: column;
|
|
border: solid black 1px;
|
|
padding: 1px;
|
|
}
|
|
.tlabel {
|
|
text-align: center;
|
|
padding: 3px 2px 1px 2px;
|
|
}
|
|
.tbuttons {
|
|
display: flex;
|
|
display: -webkit-box;
|
|
display: moz-box-flex;
|
|
display: -ms-flexbox;
|
|
display: -webkit-flex;
|
|
flex-wrap: wrap;
|
|
flex-direction: row;
|
|
}
|
|
.button {
|
|
border: outset gray 2px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
padding: 1px 3px;
|
|
min-width: 12px;
|
|
text-align: center;
|
|
margin: 2px;
|
|
font-family: "Courier New", Courier, monospace;
|
|
background-color: white;
|
|
}
|
|
.button:hover {
|
|
background-color: yellow;
|
|
}
|
|
.button:active {
|
|
border: inset gray 2px;
|
|
}
|
|
#selection {
|
|
margin: 2px 5px;
|
|
vertical-align: middle;
|
|
font-family: "Courier New", Courier, monospace;
|
|
color: black;
|
|
background-color: white;
|
|
}
|
|
#selection span.nodepath:hover {
|
|
background-color: rgba(0, 0, 25, 0.1);
|
|
cursor: pointer;
|
|
}
|
|
#selection span.nodepath.highlight {
|
|
font-weight: bold;
|
|
}
|
|
#contextmenu {
|
|
display: none;
|
|
position: absolute;
|
|
border: solid black 1px;
|
|
box-shadow: 2px 2px 3px gray;
|
|
padding: 2px;
|
|
background-color: white;
|
|
font-family: Roboto, Helvetica, sans-serif;
|
|
font-size: 0.9em;
|
|
}
|
|
#contextmenu div {
|
|
padding: 4px 10px;
|
|
cursor: pointer;
|
|
}
|
|
#contextmenu div:hover {
|
|
background-color: rgba(0, 0, 25, 0.1);
|
|
}
|
|
#contextmenu span.expcol {
|
|
pointer-events: none;
|
|
}
|
|
#help {
|
|
position: relative;
|
|
bottom: -1.7em;
|
|
transform: rotate(270deg);
|
|
-webkit-transform: rotate(270deg);
|
|
-moz-transform: rotate(270deg);
|
|
-o-transform: rotate(270deg);
|
|
width: 100px;
|
|
text-align: center;
|
|
margin-top: -10px;
|
|
margin-left: -38px;
|
|
}
|
|
#help a {
|
|
color: black;
|
|
text-decoration: none;
|
|
background-color: white;
|
|
}
|
|
#help:hover a {
|
|
background-color: yellow;
|
|
}
|
|
|
|
#tree-container, svg {
|
|
overflow: hidden;
|
|
}
|
|
.node {
|
|
cursor: pointer;
|
|
}
|
|
.node circle {
|
|
fill: #fff;
|
|
}
|
|
.node text {
|
|
font-size: 12px;
|
|
font-family: Roboto, sans-serif;
|
|
text-shadow: 4px 4px 3px white, -4px -4px 3px white;
|
|
}
|
|
.node text:hover {
|
|
font-size: 1.2em;
|
|
transition: font-size 0.1s;
|
|
}
|
|
.node text:not(:hover) {
|
|
transition: font-size 1s;
|
|
transition-delay: 0.5s;
|
|
}
|
|
.link {
|
|
fill: none;
|
|
stroke: #D3D3D3;
|
|
}
|
|
|
|
.legendLab {
|
|
font-size: 14px !important;
|
|
}
|
|
|
|
</style> |