Source: colorMapper.js

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);
}