let normalizedVotesAll; //The data set to be represented, normalized to [0,1].
let normalizedVotesIndividual = [];
//Cumulative density functions, used by the histogram normalization mapping
let histogramIndividual = [];
let histogramAll;
let cdfArrayIndividual = [];
let cdfArrayAll;
let minimumAll;
let minimumIndividual = [];
let maximumAll;
let maximumIndividual;
//DOM Elements
let select_segmentation;
let select_colorScheme;
let select_identificationMode;
let select_scope;
let range_binNumber;
let range_localMy;
let range_localSig;
let range_seg;
//Value of DOM Elements, should always up-to-date. Used for increased performance.
let state_segmentation;
let state_colorScheme;
let state_identificationMode;
let state_scope;
let value_binNumber;
let value_localMy;
let value_localSig;
let value_seg;
$(document).ready(function(){
initInputValues();
segSliderOnChange();
onIdentificationModeChanged();
setTimeout(function () {calcAndSaveQuartilesAndRanges(); updateColors()}, 1000);
});
/**
* Sets up Listeners for input elements and assigns initial values
*/
function initInputValues(){
select_segmentation = document.getElementById("slct_segmentation");
select_colorScheme = document.getElementById("slct_colorScheme");
select_identificationMode = document.getElementById("slct_identificationMode");
select_scope = document.getElementById("slct_scope");
range_binNumber = document.getElementById("binNumber");
range_localMy = document.getElementById("localMy");
range_localSig = document.getElementById("localSig");
range_seg = document.getElementById("segNumber");
//assign initial values
state_segmentation = select_segmentation.value;
state_colorScheme = select_colorScheme.value;
state_identificationMode = select_identificationMode.value;
state_scope = select_scope.value;
value_binNumber = range_binNumber.value;
value_localMy = range_localMy.value;
value_localSig = range_localSig.value;
value_seg = range_seg.value;
}
/**
* Called if the Segmentation-mode changed.
* It saves the state redraws the maps.
*/
function onSegmentationChanged(){
state_segmentation = select_segmentation.value;
updateColors();
}
/**
* Called if the Scope-selection is changed.
* It saves the state and redraws the maps.
*/
function onScopeChanged(){
state_scope = select_scope.value;
updateColors()
}
/**
* saves ranges for each map;
*/
function calcAndSaveQuartilesAndRanges() {
if (votes[0]) {
for (var i = 0; i < 4; i++) {
const filteredVotes = votes[i].values().filter(function (value) {
return !Number.isNaN(value);
});
filteredVotes.sort();
ranges[i][0] = filteredVotes[0];
ranges[i][1] = filteredVotes[filteredVotes.length - 1];
quartiles[i][0] = filteredVotes[Math.floor(filteredVotes.length * 0.25) - 1];
quartiles[i][1] = filteredVotes[Math.floor(filteredVotes.length * 0.50) - 1];
quartiles[i][2] = filteredVotes[Math.floor(filteredVotes.length * 0.75) - 1];
}
}
}
/**
* normalizes data to [0,1] for further use in histograms and box-whisker parameters.
* @param {Array} data ALL data that will be represented in the svg map.
* @returns {Array} normalized data to [0,1].
*/
function normalizeData(data){
//filters out NaN values
const filteredData = data.filter(function (value) {
return !Number.isNaN(value);
});
/*
let maxVal = Math.max.apply(Math, filteredData);
let minVal = Math.min.apply(Math, filteredData);
let normalizedFilteredData = [];
for (let i=0; i<filteredData.length; i++){
normalizedFilteredData[i] = (filteredData[i] - minVal)/maxVal;
}
normalizedFilteredData.sort();*/
return filteredData.sort();
}
/**
* normalizes all four map data for its own and saves the result into normalizedVotesInvividual array.
*/
function normalizeAllDataIndividually(){
//normalize individualData
for (let i = 0; i < 4; i++){
normalizedVotesIndividual[i] = normalizeData(votes[i].values());
}
}
/**
* normalizes all four concatenated map data and saves the result into normalizedVotesAll
*/
function normalizeAllDataAsOne(){
let allVotes = votes[0].values();
for (let i = 1; i < 4; i++){
Array.prototype.push.apply(allVotes,votes[i]);
}
normalizedVotesAll = normalizeData(allVotes);
}
/**
* Calculates and saves the CDFs (cumulative density functions) used for histogram normalization.
* This is done for each map data individually as well as for all concatenated data.
*/
function calculateAndSaveCDFs(){
cdfArrayAll = calculateCDF(histogram(normalizedVotesAll, value_binNumber));
for (let i = 0; i < 4; i++){
cdfArrayIndividual[i] = calculateCDF(histogram(normalizedVotesIndividual[i], value_binNumber));
}
}
/**
* Called if the Color-Mode-selection is changed.
* It accordingly hides or shows range sliders for bin numbers in case of Histogram Normalization mode and redraws the maps.
*/
function onColorSchemeChanged(){
state_colorScheme = select_colorScheme.value;
if (state_colorScheme === "histNorm") {
document.getElementById("binNumberContainer").style.visibility = "visible";
}
binSliderOnChange()
}
/**
* Called if the parameter range sliders are changed.
* It saves the values and redraws the maps.
*/
function onLocalizationSlidersChanged(){
value_localMy = range_localMy.value;
value_localSig = range_localSig.value;
document.getElementById("localMyValue").innerHTML = "Value: "+parseFloat(value_localMy).toFixed(2);
document.getElementById("localSigValue").innerHTML = "Sharpness: "+parseFloat(value_localSig).toFixed(1);
updateColors()
}
/**
* Called if the Task-Selection is changed from identification to localization and vice versa.
* It accordingly hides or shows range sliders for adjusting parameters for localization and redraws the maps.
*/
function onIdentificationModeChanged(){
state_identificationMode = select_identificationMode.value;
if (state_identificationMode === "localization"){
document.getElementById("localizationContainer").style.visibility="visible";
}else{
document.getElementById("localizationContainer").style.visibility="hidden";
}
onLocalizationSlidersChanged();
updateColors();
}
/**
* Called if the slider for the number of histogram bins is changed.
* Recalculates histograms and CDF arrays
*/
function binSliderOnChange(){
value_binNumber = range_binNumber.value;
let colorScheme = state_colorScheme;
if (colorScheme === "compVal"){
document.getElementById("localizationContainer").style.opacity="0.5";
document.getElementById("localMy").disabled = true;
document.getElementById("localSig").disabled = true;
}else{
document.getElementById("localizationContainer").style.opacity="1";
document.getElementById("localMy").disabled = false;
document.getElementById("localSig").disabled = false;
}
if (colorScheme === "histNorm"){
normalizeAllDataIndividually();
normalizeAllDataAsOne();
calculateAndSaveCDFs();
}else{
document.getElementById("binNumberContainer").style.visibility="hidden";
}
if (colorScheme === "boxWhisk"){
normalizeAllDataIndividually();
normalizeAllDataAsOne();
}
document.getElementById("binNumberValue").innerHTML = "# Bins: " + value_binNumber ;
updateColors();
}
/**
* Called if the segmentation-slider changed.
* It saves the state and redraws the maps.
*/
function segSliderOnChange(){
value_seg = range_seg.value;
document.getElementById("segNumberValue").innerHTML = "Segments: " + value_seg ;
updateColors();
}
/**
* normalizes an inputValue and, if user choses a segmentation, maps it to the closest segment value.
* @param inputValue the value to be normalized. Must lay between minVal and maxVal.
*/
function mapSegmentation(inputValue) {
if (state_segmentation === "segmented") {
document.getElementById("segNumberContainer").style.visibility="visible";
let numberOfSegs = value_seg;
if (numberOfSegs === 1){
return 0;
}
else {
return Math.round(inputValue * (numberOfSegs - 1)) / (numberOfSegs - 1.0);
}
} else{
document.getElementById("segNumberContainer").style.visibility="hidden";
return inputValue
}
}
/**
* Maps a inputValue value of the map or the legend (current range: 0-1) to a color using the color scheme selected by the user.
* @param inputValue the value to be mapped. Does not need to be normalized, but must lay between minVal and maxVal.
* @param mapindex the index of the map/dataset that the datapoint that is rendered with this call is part of.
*/
function valueToColor(inputValue, mapindex) {
inputValue = mapSegmentation(inputValue);
var global_scope = (state_scope === "global");
switch (state_colorScheme){
case "linear": return valueToColor_linear(inputValue);
case "boxWhisk": return valueToColor_boxWhisk(inputValue, mapindex, global_scope?normalizedVotesAll:normalizedVotesIndividual[mapindex]);
case "histNorm": return valueToColor_histNorm(inputValue, global_scope?cdfArrayAll:cdfArrayIndividual[mapindex]);
case "compVal": return valueToColor_compare(inputValue);
default: return undefined
}
}
/**
* Maps a value to a HSL value by using the Method proposed by Tominski et al.
* @param inputValue value to be mapped; between 0 and 1.
*/
function valueToColor_compare(inputValue){
let h, s, l;
let i = Math.abs(inputValue);
switch (true){
case (i < 0.2): h=0; s=i*500/3; break;
case (i < 0.4): h=(i-0.2)*900; s=100/3; break;
case (i < 0.6): h=180; s=i*500/3 - 100/3; break;
case (i < 0.8): h=(i-0.6)*900 + 180; s= 200/3; break;
case (i <= 1) : h=360; s=i*500/3 - 200/3; break;
default: h=1; s=1; break;
}
h = Math.round(h);
s = Math.round(s);
l = s;
return w3color("hsl("+ h + "," + s +"%,"+ l+"%)").toRgbString();
}
/**
* Converts a (possibly alreadly mapped) value/brightness in the interval [0,1] to a color.
* This conversion is either linear in HSL, or a hat function if "localization" is selected by the user.
* @param inputValue value/brightness to be mapped to a color
*/
function valueToColor_linear(inputValue){
if(state_identificationMode === "localization"){
let l= hatFunc(inputValue, value_localMy, value_localSig)*100;
return w3color("hsl(0,"+l+"%,"+l+"%)").toRgbString();
}else {
let l = Math.round(inputValue * 100);
return w3color("hsl(0," + l + "%," + l + "%)").toRgbString();
}
}
/**
* function to map values for localization tasks to brightness according to a head function
* @param x value to be mapped to a brightness
* @param m the values that will mapped to the highest brightess
* @param d steepness of the highlight. The smaller the value the further away from m can x be while still being highlited
*/
function hatFunc(x, m, d){
return Math.max(0,-Math.pow(d,2)*Math.abs(x-m)+1);
}
/**
* function to map values for localization tasks to brightness according to a normal distribution
* @param x value to be mapped to a brightness
* @param mean Mean parameter of the ndf
* @param StdDev Standard Deviation parameter of the ndf
*/
function normalDistr(x, mean, StdDev){
let a = x-mean;
return Math.exp( -( a * a ) / ( 2 * StdDev * StdDev ) ) / ( Math.sqrt( 2 * Math.PI ) * StdDev );
}
/**
* maps a value to a color according to a normalized histogram
* @param inputValue value to be mapped to a brightness
* @param cdfInput a pre-calculated cumulative density function of ALL input data
*/
function valueToColor_histNorm(inputValue, cdfInput){
let num_bins = value_binNumber;
inputValue = Math.min(Math.max(inputValue, 0), 1);
let binNumber = Math.floor(inputValue * (num_bins-1));
let outputValue = cdfInput[binNumber];
return valueToColor_linear(outputValue)
}
/**
* maps a value to a color according to a box-whisker plot
* @param inputValue value to be mapped to a brightness
* @param mapIndex the index of the map/dataset that the datapoint that is rendered with this call is part of.
* @param sorted_data_asc the sorted dataset of data for calcuation of the box whisker parameters, normalized to [0,1]
*/
function valueToColor_boxWhisk(inputValue, mapIndex, sorted_data_asc){
//let minVal = sorted_data_asc[0];
//let maxVal = sorted_data_asc[sorted_data_asc.length-1];
//let normalizedInputValue = (inputValue - minVal)/maxVal;
let normalizedInputValue = inputValue;
//Box (25%, 50% and 75% quartiles)
let quart25 = sorted_data_asc[Math.floor(sorted_data_asc.length*0.25)];
let quart50 = sorted_data_asc[Math.floor(sorted_data_asc.length*0.50)];
let quart75 = sorted_data_asc[Math.floor(sorted_data_asc.length*0.75)];
//Whiskers
let lowWhisk = quart25 - (quart50 - quart25) * 1.5;
let highWhisk = quart75 + (quart75 - quart50) * 1.5;
lowWhisk = sorted_data_asc[Math.floor(sorted_data_asc.length*0.10)];
highWhisk = sorted_data_asc[Math.floor(sorted_data_asc.length*0.90)];
//if whiskers are
if (normalizedInputValue < lowWhisk){
return valueToColor_linear(0);
}else if (normalizedInputValue > highWhisk){
return valueToColor_linear(1);
}
//Mapping
let mappingKeys = [lowWhisk, quart25, quart50, quart75, highWhisk];
let mappingValues = [0,0.3,0.5,0.7,1];
//Check in which range the current value to map falls and interpolate between the neighbouring quartiles.
for (let i = 1; i < 5; i++) {
if (normalizedInputValue<mappingKeys[i]){
let x0 = mappingKeys[i-1];
let x1 = mappingKeys[i];
let y0 = mappingValues[i-1];
let y1 = mappingValues[i];
let k = (normalizedInputValue-x0)/(x1-x0);
let outputValue = y1*k + y0*(1-k);
return valueToColor_linear(outputValue)
}
}
}
/**
* returns a histogram of the provided data
* @param {Array} data the dataset.
* @param num_bins number of bins used for building the histogram.
*/
function histogram(data, num_bins) {
let hist = [];
for(let i=0;i<num_bins;++i){hist[i] = 0;} //initialization
for(let i=0; i< data.length; i++) {
// figure out which bin it is in
hist[Math.floor(data[i] * (num_bins-1))]++;
}
return hist;
}
/**
* returns a cumulative distribution function of a histogram.
* @param {Array} hist the histogram as an array.
* @returns Array [0,1] normalized cdf as an array the same size as hist.
*/
// cumulative distribution function for a histogram
function calculateCDF(hist) {
let func_val = [];
//initialization
for(let i=0;i<hist.length;++i){func_val[i] = 0;}
let sumOfAllValues = 0;
//create cdf
for(let i=0; i< hist.length; i++) {
sumOfAllValues = sumOfAllValues + hist[i];
func_val[i] = sumOfAllValues;
}
//normalize cdf
for(let i=0; i< hist.length; i++) {
func_val[i] = func_val[i]/sumOfAllValues;
}
return(func_val);
}