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.
 
 
 
 
FeatureEnVi/frontend/src/components/Heatmap.vue

583 lines
20 KiB

<template>
<div>
<div id="Heatmap"></div>
<div id="LegendHeat"></div>
</div>
</template>
<script>
import * as d3Base from 'd3'
import { EventBus } from '../main.js'
import $ from 'jquery'
import * as colorbr from 'colorbrewer'
// attach all d3 plugins to the d3 library
const d3 = Object.assign(d3Base)
const colorbrewer = Object.assign(colorbr)
export default {
name: "Heatmap",
data () {
return {
GetResultsAll: '',
Toggles: '',
limitation: 0,
flag: false,
classesNumber: 10,
cellSize: 16,
ModelsIDHeatStack: [],
highlighted: [],
responsiveWidthHeight: [],
FeaturesAccuracyStore: [],
FeaturesStore: [],
PermImpEliStore: [],
featureUniStore: [],
modelIdsStore: [],
keyLocal: 0,
activeModels: 0,
flagLocal: false,
smallScreenMode: '0px',
dataFI: [],
featureData: [],
}
},
methods: {
Refresh () {
if (this.flagLocal) {
var tempFeatures = this.FeaturesStore.slice(this.activeModels,this.activeModels+1)[0]
var tempFeaturesTable = []
var FeatureTemp = []
var tempModelIds = this.modelIdsStore.slice(this.activeModels,this.activeModels+1)[0]
for (let i = 0; i < tempFeatures.length; i++) {
tempFeaturesTable.push(i)
}
for (let i = 0; i < tempModelIds.length; i++) {
FeatureTemp.push(tempFeaturesTable)
}
EventBus.$emit('SendSelectedFeaturesEvent', FeatureTemp)
}
},
reset () {
var svg = d3.select("#Heatmap");
svg.selectAll("*").remove();
var svgLeg = d3.select("#LegendHeat");
svgLeg.selectAll("*").remove();
},
Heatmap () {
// Clear Heatmap first
var svg = d3.select("#Heatmap");
svg.selectAll("*").remove();
var featureUni = JSON.parse(this.dataFI[0])
var algorithms = []
algorithms.push("Univariate FS")
algorithms.push("Permutation FI")
algorithms.push("Accuracy-based FI")
algorithms.push("Average")
algorithms.push("# Action #")
var PermImpEli = JSON.parse(this.dataFI[1])
var FeaturesAccuracy = JSON.parse(this.dataFI[2])
var Features = this.featureData[0]
let arr = Object.values(featureUni.Score);
let minUni = Math.min(...arr);
let maxUni = Math.max(...arr);
let len = Features.length
let indicesYAxis = []
for (let i = 0; i < len; i++) {
indicesYAxis[i] = [Features[i]]
}
let indicesXAxis = []
var temp = []
var len2 = algorithms.length
for (let i = 0; i < len2; i++) {
temp = []
temp.push("R")
temp.push(algorithms[i].toString())
indicesXAxis[i] = temp
}
var values = []
var modelData = []
for (let j = 0; j <len2; j++) {
var data = []
for (let i = 0; i < len; i++) {
if (algorithms[j] == "Univariate FS") {
values[j] = ((Object.values(featureUni.Score)[i]-minUni)/(maxUni-minUni))
} else if (algorithms[j] == "Permutation FI") {
values[j] = PermImpEli[i][0]
} else if (algorithms[j] == "Accuracy-based FI") {
values[j] = FeaturesAccuracy[i][0]
} else if (algorithms[j] == "Average") {
values[j] = ((((Object.values(featureUni.Score)[i]-minUni)/(maxUni-minUni)))+(PermImpEli[i][0])+(FeaturesAccuracy[i][0]))/(len-2)
} else {
values[j] = -2
}
data.push(values[j])
}
modelData.push(data)
}
var transposedArray = []
transposedArray = modelData[0].map((col, i) => modelData.map(row => row[i]))
var dataAll = {"columns":indicesXAxis,"index":indicesYAxis,"data":transposedArray}
this.heatmap_display(dataAll, "#Heatmap");
},
heatmap_display(data, heatmapId) {
var featuresAddRem = []
var cellSize = this.cellSize
//##########################################################################
// Patrick.Brockmann@lsce.ipsl.fr
//##########################################################################
//==================================================
// References
// http://bl.ocks.org/Soylent/bbff6cc507dca2f48792
// http://bost.ocks.org/mike/selection/
// http://bost.ocks.org/mike/join/
// http://stackoverflow.com/questions/9481497/understanding-how-d3-js-binds-data-to-nodes
// http://bost.ocks.org/mike/miserables/
// http://bl.ocks.org/ianyfchang/8119685
//==================================================
var tooltip = d3.select(heatmapId)
.append("div")
.style("position", "absolute")
.style("visibility", "hidden");
// define the zoomListener which calls the zoom function on the "zoom" event constrained within the scaleExtents
const zoom = d3.zoom()
.scaleExtent([0.1, 3]) //zoom limit
.on('zoom', () => {
svg.attr('transform', d3.event.transform) // updated for d3 v4
})
//==================================================
var viewerHeight = 820
var widthHeatmap = 392
var viewerPosTop = 70;
var viewerPosTopHeat = 110;
var viewerPosLeft = 100;
var legendElementWidth = cellSize * 2.5;
// http://bl.ocks.org/mbostock/5577023
var colors = colorbrewer.RdYlGn[this.classesNumber];
// http://bl.ocks.org/mbostock/3680999
var svg;
//==================================================
var arr = data.data;
var row_number = arr.length;
var col_number = arr[0].length;
var colorScale = d3.scaleQuantize()
.domain([0.0, 1.0])
.range(colors);
svg = d3.select(heatmapId).append("svg")
.attr("width", widthHeatmap)
.attr("height", viewerHeight)
.call(zoom)
//.call(zoom.transform, d3.zoomIdentity.translate(200, 20).scale(0.25)) //initial size
.append('svg:g')
.attr("transform", "translate(" + viewerPosLeft + "," + viewerPosTopHeat + ")");
svg.append('defs')
.append('pattern')
.attr('id', 'diagonalHatch')
.attr('patternUnits', 'userSpaceOnUse')
.attr('width', 4)
.attr('height', 4)
.append('path')
.attr('d', 'M-1,1 l2,-2 M0,4 l4,-4 M3,5 l2,-2')
.attr('stroke', '#000000')
.attr('stroke-width', 1);
var rowSortOrder = false;
var colSortOrder = false;
var rowLabels = svg.append("g")
.attr("class", "rowLabels")
.selectAll(".rowLabel")
.data(data.index)
.enter().append("text")
.text(function(d) {
return d.count > 1 ? d.join("/") : d;
})
.style('font-weight', function(d) {
if (d[0] == "# Action #" || d[0] == "Average") {
return "bold"
}
})
.attr("x", 0)
.attr("y", function(d, i) {
return (i * cellSize);
})
.style("text-anchor", "end")
.attr("transform", function(d, i) {
return "translate(-3," + cellSize / 1.5 + ")";
})
.attr("class", "rowLabel mono")
.attr("id", function(d, i) {
return "rowLabel_" + i;
})
.on('mouseover', function(d, i) {
d3.select('#rowLabel_' + i).classed("hover", true);
})
.on('mouseout', function(d, i) {
d3.select('#rowLabel_' + i).classed("hover", false);
})
.on("click", function(d, i) {
rowSortOrder = !rowSortOrder;
sortByValues("r", i, rowSortOrder);
d3.select("#order").property("selectedIndex", 0);
//$("#order").jqxComboBox({selectedIndex: 0});
});
var colLabels = svg.append("g")
.attr("class", "colLabels")
.selectAll(".colLabel")
.data(data.columns)
.enter().append("text")
.text(function(d) {
d.shift();
return d.count > 1 ? d.reverse().join("/") : d.reverse();
})
.attr("x", 0)
.attr("y", function(d, i) {
return (i * cellSize);
})
.style("text-anchor", "left")
.style('font-weight',function(d,i){
/*if (d[0] === "Average") {
return "bold"
}*/
})
.attr("transform", function(d, i) {
return "translate(" + cellSize / 2 + ", -3) rotate(-90) rotate(45, 0, " + (i * cellSize) + ")";
})
.attr("class", "colLabel mono")
.attr("id", function(d, i) {
return "colLabel_" + i;
})
.on('mouseover', function(d, i) {
d3.select('#colLabel_' + i).classed("hover", true);
})
.on('mouseout', function(d, i) {
d3.select('#colLabel_' + i).classed("hover", false);
})
.on("click", function(d, i) {
colSortOrder = !colSortOrder;
sortByValues("c", i, colSortOrder);
d3.select("#order").property("selectedIndex", 0);
});
var row = svg.selectAll(".row")
.data(data.data)
.enter().append("g")
.attr("id", function(d) {
return d.idx;
})
.attr("class", "row");
svg.append("text").attr("x", 10).attr("y", -65).text("Technique").style("font-size", "16px").attr("alignment-baseline","top")
svg.append("text").attr("transform", "rotate(-90)").attr("x", -33).attr("y", -50).style("text-anchor", "middle").style("font-size", "16px").text("Feature"); // -130 before for HeartC
var heatMap = row.selectAll(".cell")
.data(function(d) {
return d;
})
.enter().append("svg:rect")
.attr("id", function(d, i, j){
var k = Array.prototype.indexOf.call(j[i].parentNode.parentNode.childNodes,j[i].parentNode) - 3;
return k.toString()+i.toString();
})
.attr("x", function(d, i) {
return i * cellSize;
})
.attr("y", function(d, i, j) {
var k = Array.prototype.indexOf.call(j[i].parentNode.parentNode.childNodes,j[i].parentNode) - 3;
return k * cellSize;
})
.attr("rx", 4)
.attr("ry", 4)
.attr("class", function(d, i, j) {
var k = Array.prototype.indexOf.call(j[i].parentNode.parentNode.childNodes,j[i].parentNode) - 3;
return "cell bordered cr" + k + " cc" + i;
})
.attr("row", function(d, i, j) {
var k = Array.prototype.indexOf.call(j[i].parentNode.parentNode.childNodes,j[i].parentNode) - 3;
return k;
})
.attr("col", function(d, i, j) {
return i;
})
.attr("width", cellSize)
.attr("height", cellSize)
.style("fill", function(d) {
if (d == -1) return "url(#diagonalHatch)"
else if (d == -2) return "yellow"
else return colorScale(d)
})
.on('mouseover', function(d, i, j) {
})
.on('mouseout', function(d, i, j) {
})
.on("mousemove", function(d, i) {
})
.on('click', function(d, i, j) {
var rowsExtracted = svg.selectAll(".row")._groups[0]
var k = Array.prototype.indexOf.call(j[i].parentNode.parentNode.childNodes,j[i].parentNode) - 3;
d3.select(this).style("fill", function(d) {
if (d3.select(this).style("fill") === "yellow" || d3.select(this).style("fill") === "url(\"#diagonalHatch\")") {
if (d3.select(this).style("fill") === "url(\"#diagonalHatch\")"){
if (d == -2) {
const index = featuresAddRem.indexOf(k);
if (index > -1) {
featuresAddRem.splice(index, 1);
}
EventBus.$emit('addFeature', featuresAddRem)
return 'yellow'
} else {
return colorScale(d)
}
} else {
featuresAddRem.push(k)
EventBus.$emit('removeFeatures', featuresAddRem)
return "url(#diagonalHatch)"
}
} else {
return colorScale(d)
}
})
});
var svgLeg = d3.select("#LegendHeat");
svgLeg.selectAll("*").remove();
var svgLeg = d3.select("#LegendHeat").append("svg")
.attr("width", widthHeatmap)
.attr("height", viewerHeight*0.12)
var legend = svgLeg.append('g')
.attr("class", "legend")
.attr("transform", "translate(0,0)")
.selectAll(".legendElement")
.data([0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9])
.enter().append("g")
.attr("class", "legendElement");
legend.append("svg:rect")
.attr("x", function(d, i) {
return legendElementWidth * i;
})
.attr("y", viewerPosTop)
.attr("class", "cellLegend bordered")
.attr("width", legendElementWidth)
.attr("height", cellSize / 2)
.style("fill", function(d, i) {
return colors[i];
});
legend.append("text")
.attr("class", "mono legendElement")
.text(function(d) {
return "≥" + Math.round(d * 100) / 100;
})
.attr("x", function(d, i) {
return legendElementWidth * i;
})
.attr("y", viewerPosTop + cellSize);
svgLeg.append("text").attr("x", 75).attr("y", 55).text("Normalized feature importance (FI)").style("font-size", "16px").attr("alignment-baseline","top")
//==================================================
// Change ordering of cells
function sortByValues(rORc, i, sortOrder) {
var t = svg.transition().duration(1000);
var values = [];
var sorted;
d3.selectAll(".c" + rORc + i)
.filter(function(d) {
if (d != null) values.push(d);
else values.push(-999); // to handle NaN
});
if (rORc == "r") { // sort on cols
sorted = d3.range(col_number).sort(function(a, b) {
if (sortOrder) {
return values[b] - values[a];
} else {
return values[a] - values[b];
}
});
t.selectAll(".cell")
.attr("x", function(d) {
var col = parseInt(d3.select(this).attr("col"));
return sorted.indexOf(col) * cellSize;
});
t.selectAll(".colLabel")
.attr("y", function(d, i) {
return sorted.indexOf(i) * cellSize;
})
.attr("transform", function(d, i) {
return "translate(" + cellSize / 2 + ", -3) rotate(-90) rotate(45, 0, " + (sorted.indexOf(i) * cellSize) + ")";
});
} else { // sort on rows
sorted = d3.range(row_number).sort(function(a, b) {
if (sortOrder) {
return values[b] - values[a];
} else {
return values[a] - values[b];
}
});
t.selectAll(".cell")
.attr("y", function(d) {
var row = parseInt(d3.select(this).attr("row"));
return sorted.indexOf(row) * cellSize;
});
t.selectAll(".rowLabel")
.attr("y", function(d, i) {
return sorted.indexOf(i) * cellSize;
})
.attr("transform", function(d, i) {
return "translate(-3," + cellSize / 1.5 + ")";
});
}
}
//==================================================
d3.select("#order").on("change", function() {
var newOrder = d3.select("#order").property("value");
this.changeOrder(newOrder, heatmapId);
});
//==================================================
d3.select("#palette")
.on("keyup", function() {
var newPalette = d3.select("#palette").property("value");
if (newPalette != null) // when interfaced with jQwidget, the ComboBox handles keyup event but value is then not available ?
this.changePalette(newPalette, heatmapId);
})
.on("change", function() {
var newPalette = d3.select("#palette").property("value");
this.changePalette(newPalette, heatmapId);
});
//==================================================
},
changeOrder(newOrder, heatmapId) {
var svg = d3.select(heatmapId);
var cellSize = this.cellSize
var t = svg.transition().duration(1000);
if (newOrder == "sortinit_col") { // initial sort on cols (alphabetically if produced like this)
t.selectAll(".cell")
.attr("x", function(d) {
var col = parseInt(d3.select(this).attr("col"));
return col * cellSize;
});
t.selectAll(".colLabel")
.attr("y", function(d, i) {
return i * cellSize;
})
.attr("transform", function(d, i) {
return "translate(" + cellSize / 2 + ", -3) rotate(-90) rotate(45, 0, " + (i * cellSize) + ")";
});
} else if (newOrder == "sortinit_row") { // initial sort on rows (alphabetically if produced like this)
t.selectAll(".cell")
.attr("y", function(d) {
var row = parseInt(d3.select(this).attr("row"));
return row * cellSize;
});
t.selectAll(".rowLabel")
.attr("y", function(d, i) {
return i * cellSize;
})
.attr("transform", function(d, i) {
return "translate(-3," + cellSize / 1.5 + ")";
});
} else if (newOrder == "sortinit_col_row") { // initial sort on rows and cols (alphabetically if produced like this)
t.selectAll(".cell")
.attr("x", function(d) {
var col = parseInt(d3.select(this).attr("col"));
return col * cellSize;
})
.attr("y", function(d) {
var row = parseInt(d3.select(this).attr("row"));
return row * cellSize;
});
t.selectAll(".colLabel")
.attr("y", function(d, i) {
return i * cellSize;
})
.attr("transform", function(d, i) {
return "translate(" + cellSize / 2 + ", -3) rotate(-90) rotate(45, 0, " + (i * cellSize) + ")";
});
t.selectAll(".rowLabel")
.attr("y", function(d, i) {
return i * cellSize;
})
.attr("transform", function(d, i) {
return "translate(-3," + cellSize / 1.5 + ")";
});
}
},
reset () {
var svg = d3.select("#Heatmap");
svg.selectAll("*").remove();
},
brush () {
var columnLabels = document.getElementsByClassName('colLabels')[0];
var modelIds = JSON.parse(this.GetResultsAll[13])
var selectedIds = []
for (let i = 0; i < this.highlighted.length; i++) {
let looping = this.highlighted[i]
selectedIds.push(looping)
}
for (let i = 0; i < modelIds.length; i++) {
columnLabels.childNodes[i].style.fill = "#000";
}
for (let i = 0; i < selectedIds.length; i++) {
let index = modelIds.indexOf(selectedIds[i])
columnLabels.childNodes[index].style.fill = "#AF4427";
}
}
},
mounted () {
EventBus.$on('HeatmapCall', data => { this.dataFI = data })
EventBus.$on('HeatmapCall', this.Heatmap)
EventBus.$on('quad', data => { this.featureData = data })
EventBus.$on('reset', this.reset)
}
}
</script>
<style>
.heatmap {
font-size: 9px;
font-family: monospace;
}
rect.bordered {
stroke: #E6E6E6;
stroke-width:2px;
}
text.mono {
font-size: 9px;
font-family: monospace;
fill: #000;
}
text.legendElement {
font-size: 10px;
}
</style>