Source: src.js

//id column
var uniqueKey = 'ID'

// color handling for radarchart
var color = ['#ff0000', '#ffff00', '#00ff00', '#00ffff'];
var color_used = {}

var radars = new Array()
var dominateMax = -1

var margin = {top: 20, right: 60, bottom: 20, left: 60}
var height = screen.height/2
var width = screen.width /2;

//Histogramdefine width and height
const width_hist = 400 - margin.left - margin.right
const height_hist = 100 - margin.top - margin.bottom

//saves parameter names which contain numerical values
var num_params = [];

//max value per parameter - for further normalization
var maxVal2 = {}

/**
 * Manage the radar charts within the comparison View. Zero to 4 radar charts can be created.
 *
 * @param data Object of the displayed data row
 */
var comparisionView = function(data){
    var dataKey = data[uniqueKey].replace(/ /g, '')
    if(radars.includes(dataKey)){
        //delete radar chart from selected row
        d3.select('.radarChart').select('.'+dataKey).remove()
        radars.splice(radars.indexOf(dataKey),1)
        color.push(color_used[dataKey])
        delete color_used[dataKey]
        return;
    }else{
        if(radars.length==4){
            //do nothing - only 4 radar charts allowed
            return;
        }

        color_used[dataKey] = color.pop()

        var svgRad = d3.select('.radarChart')
            .append('svg')
            .attr('class', dataKey)

        radars.push(dataKey)

        var radarChartOptions = {
            w: width/12,
            h: width/12,
            margin: margin,
            maxValue: 1,
            dominateScore: Math.max(0.05,data.dominate/dominateMax * 1.1), // if dominate score too small => default 0.1
            color: d3.scaleOrdinal().range([color_used[dataKey],"#CC333F"])
        };

        var rad_data = {name: [dataKey],
            axes: [],
            color: '#2a2fd4'}

        //create data for radial chart
        for(param in num_params){
            rad_data.axes.push({axis: num_params[param], value: data[num_params[param]]/maxVal2[num_params[param]]})
        }

        // Call function to draw the Radar chart
        RadarChart('.' + dataKey, [rad_data], radarChartOptions);
    }
}

/**
 * Draws the attribute table which summarizes the data.
 *
 * @param data Object of Column names and range of values as string
 * @param columns header for the table
 * @returns {*} generated table
 */
var tabulate = function (data,columns) {
    var table = d3.select('#attrTable').append('table')
        .attr('class', 'table table-striped table-bordered table-sm')
        //.attr('scrollX', true)
        .attr('scrollY', '200')
        .attr('scrollCollapse', true)
    var thead = table.append('thead')
    var tbody = table.append('tbody')

    thead.append('tr')
        .selectAll('th')
        .data(columns)
        .enter()
        .append('th')
        .text(function (d) { return d })

    var rows = tbody.selectAll('tr')
        .data(data)
        .enter()
        .append('tr')

    var cells = rows.selectAll('td')
        .data(function(row) {
            return columns.map(function (column) {
                return { column: column, value: row[column] }
            })
        })
        .enter()
        .append('td')
        .text(function (d) { return d.value })

    return table;
}

/**
 * Create on glyph chart for each skyline point
 *
 * @param domPoints Array of all skyline items
 */
var createdGlyphs = function (domPoints){
    var rose = Chart.rose()

    // Where the maximum value gives us the maximum radius:
    var maxRadius = Math.sqrt(100 / Math.PI);

    let widthGlyph = Math.max(width / 4 / domPoints.length, 100)
    let heightGlyph = Math.max(height / 4 / domPoints.length, 100)

    let centerX = (widthGlyph / 5)
    let centerY = (heightGlyph / 5)

    domPoints.forEach(function (d, i) {
        //calculate dominante color score
        let c = d3.interpolateOranges(d.dominate/dominateMax)

        rose.legend(['params', 'domCircle'])
            .margin(margin)
            .width(widthGlyph)
            .height(heightGlyph)
            .centerX(centerX)
            .centerY(centerY)
            .domain([0, maxRadius])
            .angle(function (d, i) {
                return i;
            })
            .area(function (d) {
                return [d['params'], 10];
            });

        glyphData = new Array()

        for (var n in num_params) {
            glyphData.push({'label': d[uniqueKey], 'score': d.dominate, 'c': c, 'params': d[num_params[n]] / maxVal2[num_params[n]] * 100})
        }

        // Append a new figure to the DOM:
        figure = d3.select('.glyphs')
            .append('figure' + i)
            .datum(glyphData)
            .call(rose);
    });
}

/**
 * Calculate the attribute differences for each parameter based on the paper.
 *
 * @param data whole csv data
 * @param currentPoint data row for which the difference should be calculated
 * @param sortedby parameter to sort values
 * @param divisors pre-calculated divisors for each param
 * @returns {{avgDiv: any[], currentPoint: number}}
 */
function calcAvgDivergence(data, currentPoint, sortedby, divisors){
    var newIndex = 0
    var result = new Array()

    // sort column by parameter
    var sortedData = data.slice().sort(function (a, b) {
        return d3.ascending(a[sortedby], b[sortedby])
    })

    // calculate attribute divergence from current point
    //var attrData = data.forEach(function (d, i) {
    for(var i in sortedData) {
        if (!isNaN(i)) {
            if (sortedData[i][uniqueKey] == currentPoint[uniqueKey]) {
                newIndex = i
            }

            var row = new Array()

            for(var n in divisors) {
                row.push((sortedData[i][n] - currentPoint[n])/divisors[n])
            }

            result.push(d3.sum(row, function(l){ return l}))
        }
    }
    return {currentPoint: newIndex, avgDiv: result}
}

/**
 * Proof if one item is dominated by another data item.
 * @param point1 point which is could be a dominater
 * @param point2 point which could be dominated by point1
 * @returns {boolean} True if point1 dominates point2, otherwise False
 */
function proofIfDominated(point1, point2){
    var dominater = new Boolean(true)
    var equalPoint = new Boolean(true)
    for(let i=0; i<num_params.length; i++){
        if(point1[num_params[i]]<point2[num_params[i]]){
            dominater = false
            break;
        }
        if(point1[num_params[i]]>point2[num_params[i]]){
            equalPoint = false
        }
    }

    if(dominater && !equalPoint){
        return true
    }

    return false
}

/**
 * Create DOM objects and start program
 */
function init() {
    const buttons = d3.selectAll('input');

    async function extract_data(){
        const dataset = await d3.csv('/zhao18_vis2/data/bstl.csv')

        let dimensions = d3.keys(dataset[0]);

        var attrTableData = new Array()

        // proof which params should be plotted (only numeric ones)
        for(var i in dimensions){
            if (isNaN(dataset[0][dimensions[i]])) {
                attrTableData.push({'Attribute Name': dimensions[i], 'Attribute Type': 'nominal'})
            } else {

                num_params.push(dimensions[i])

                // extract maximum value per column for further charts
                maxVal2[dimensions[i]] = d3.max(dataset, function (d) {
                    return +d[dimensions[i]]
                });
                attrTableData.push({'Attribute Name': dimensions[i], 'Attribute Type': '0 ~' + maxVal2[dimensions[i]]})
            }
        }

        // calculate attribute values
        var divisorObj = new Object()

        num_params.forEach(function(d){
            // - calc mean of each dimension (=parameter)
            var mean = d3.mean(dataset, function(l){return l[d]})

            // - calc divisor for each dimension
            var divisor = 0

            dataset.forEach(function(dat, i){
                divisor += Math.pow(dat[d]-mean, 2)
            })

            divisorObj[d] = Math.sqrt(divisor/dataset.length)
        })

        var tabulate_v2 = function (data,columns,domObject) {
            var table = d3.select(domObject).append('table')
                .attr('class', 'table table-bordered table-hover table-sm')
                //.attr('scrollX', true)
                .attr('scrollY', '200')
                .attr('scrollCollapse', true)
            var thead = table.append('thead')
            var tbody = table.append('tbody')

            thead.append('tr')
                .selectAll('th')
                .data(columns)
                .enter()
                .append('th')
                .text(function (d) { return d })

            //set up rows and the different columns which will be dealt with separately
            var rows = tbody.selectAll('tr')
                .data(data)
                .enter()
                .append('tr')
                .attr('id', function(d, i){return i+1;});

            dimensions.forEach(function (d, i) {
                if(!isNaN(data[0][d])) {
                    var chartColumn = rows.append('td')
                        .attr('class', 'number')

                    if (domObject == '.histTable') {

                        // //create chart for each row, enclosed in a <td>
                        const svg = chartColumn.append('svg')
                            .attr('width', width_hist)
                            .attr('height', height_hist);

                        //calculate min and max value of parameter
                        const xMax = Math.round(d3.max(dataset, n => n[d]) * 100) / 100;

                        const xScale = d3.scaleLinear()
                            .domain([0, xMax])
                            .rangeRound([0, width_hist])

                        const binsGenerator = d3.histogram()
                            .domain([0, xMax])
                            .thresholds(xScale.ticks(dataset.length / 2))
                            .value(n => n[d])

                        const yMax = d3.max(binsGenerator(dataset), d => d.length)

                        const yScale = d3.scaleLinear()
                            .domain([0, yMax])
                            .range([height_hist, 0])

                        svg.selectAll("rect")
                            .data(binsGenerator(dataset))
                            .enter()
                            .append("rect")
                            .attr("x", d => xScale(d.x0))
                            .attr("y", d => yScale(d.length))
                            .attr("width", d => xScale(d.x1) - xScale(d.x0))
                            .attr("height", d => yScale(0) - yScale(d.length))
                            .attr("fill", "blue")
                            .attr("opacity", .5)
                    }
                }else{
                    //prepare and create the nominal columns
                    if(data.length==1){
                        rows.append('td')
                            .attr('class', 'nominal')
                            .data(data)
                    }else{
                        rows.append('td')
                            .attr('class', 'nominal')
                            .data(data)
                            .text(function (f){return f[d]})
                    }
                }
            })

            if (domObject != '.histTable') {
                rows.on("dblclick", function(d){comparisionView(d)})

                rows.selectAll('td.number').each(function (d, n) {

                    var avgDiverg = calcAvgDivergence(dataset, d, num_params[n], divisorObj)
                    var attrData = avgDiverg.avgDiv

                    var mouseover = function(d) {
                        Tooltip
                            .style("opacity", 1)
                        d3.select(this)
                            .style("stroke", "black")
                            .style("opacity", 1)
                    }

                    var mousemove = function(d) {
                        Tooltip
                            .html(num_params[n] + ': ' + d[num_params[n]])
                            .style("left", (n*width_hist) + "px")
                            .style("top", (d3.select(this.parentNode.parentNode).attr('id')*height_hist + height_hist/2) + "px")
                    }

                    var mouseleave = function(d) {
                        Tooltip
                            .style("opacity", 0)
                        d3.select(this)
                            .style("stroke", "none")
                            .style("opacity", 0.8)
                    }

                    // //create Attribute chart for each cell, enclosed in a <td.number>
                    const svg = d3.select(this).append('svg')
                        .attr('width', width_hist)
                        .attr('height', height_hist)
                        .on('mouseover', mouseover)
                        .on('mousemove', mousemove)
                        .on('mouseleave', mouseleave);

                    var yMax = Math.max(Math.abs(d3.min(attrData)), Math.abs(d3.max(attrData)));

                    //set y scale
                    var yScale = d3.scaleLinear()
                        .range([0,height_hist])
                        .domain([yMax,-yMax]);//show negative

                    //add x axis
                    var xScale = d3.scaleBand()
                        .range([0,width_hist])
                        .paddingInner(0.1);//scaleBand is used for  bar chart

                    xScale.domain(attrData.map(function(d, i) {return i; }));
                    //value in this array must be unique
                    /*if domain is specified, sets the domain to the specified array of values. The first element in domain will be mapped to the first band, the second domain value to the second band, and so on. Domain values are stored internally in a map from stringified value to index; the resulting index is then used to determine the band. Thus, a band scale’s values must be coercible to a string, and the stringified version of the domain value uniquely identifies the corresponding band. If domain is not specified, this method returns the current domain.*/

                    var bars = svg.selectAll("rect")
                        .data(attrData)
                        .enter()
                        .append("rect");

                    bars
                        .attr("x", function(d, i) { return xScale(i); })
                        .attr("y",function(d, i){
                            if(i == avgDiverg.currentPoint){
                                return yScale(yMax)
                            }else {
                                if (+d < 0) {
                                    return height_hist / 2;
                                } else {
                                    return yScale(+d);
                                }
                            }
                        })//for bottom to top
                        .attr("width", Math.max(xScale.bandwidth(),1)/*width/dataset.length-barpadding*/)
                        .attr("yMax",  function(d) {return Math.max(Math.abs(d3.min(d)), Math.abs(d3.max(d)))})
                        .attr("height", function(d, i){
                            if(i==avgDiverg.currentPoint){
                                return height_hist
                            }else {
                                return height_hist / 2 - yScale(Math.abs(+d));
                            }
                        });

                    if(d.dominatedBy > 0){
                        bars.attr("class", 'dominatedBy')
                    }else{
                        bars.attr("class", function(d, i) { return i==avgDiverg.currentPoint ? "bar current" : (d < 0 ? "bar negative" : "bar positive"); })
                    }

                    //draw middle line
                    svg.append("g")
                        .attr("class", "y axis")
                        .append("line")
                        .attr("y1", yScale(0))
                        .attr("y2", yScale(0))
                        .attr("x1", 0)
                        .attr("x2", width_hist);

                    // create a tooltip
                    var Tooltip = d3.select("#table")
                        .append("div")
                        .style("opacity", 0)
                        .attr("class", "tooltip")
                })
            }

            return table;
        }

        //add 2 columns for skyline point detection
        dataset.columns.push('dominatedBy')
        dataset.columns.push('dominate')

        $.each(dataset, function(key1,value1) {
            value1["dominatedBy"] = 0
            value1["dominate"] = 0
        })

        //calculate the dominated by and dominate scores
        $.each(dataset, function(key1,value1){
            $.each(dataset, function(key2,value2){
                if(proofIfDominated(value1, value2)){
                    value1["dominate"] += 1
                    value2["dominatedBy"] += 1
                }
            })
        })

        dominateMax = d3.max(dataset, function (d) {
            return d.dominate
        })

        var dataset_header = new Array()
        dataset_header.columns = dataset.columns
        dataset_header.push(dataset[0])

        // histogram table
        tabulate_v2(dataset_header, dimensions, '.histTable')

        //create dominant points
        var dominater = []
        var dominated = []
        dataset.forEach(function(row, index){
            if(row.dominatedBy==0){
                dominater.push(row)
            }else[
                dominated.push(row)
            ]
        });

        d3.select('#dataEntries').append('text')
           .text('Tabular View - ' + dominater.length + ' items')

        // attribute table
        tabulate_v2(dominater, dimensions, '#table')

        createdGlyphs(dominater)

        // parameter description table
        tabulate(attrTableData, ['Attribute Name', 'Attribute Type'])

        buttons.on('change', function(d){
            console.log('button changed to ' + this.value);
            d3.select('#table').select('table').remove()
            if(this.value=='dominated'){
                d3.select('#dataEntries').select('text')
                    .text('Tabular View - ' + dominated.length + ' items')

                tabulate_v2(dominated, dimensions, '#table')
            }else if(this.value=='skyline'){
                d3.select('#dataEntries').select('text')
                    .text('Tabular View - ' + dominater.length + ' items')

                tabulate_v2(dominater, dimensions, '#table')
            }else{
                d3.select('#dataEntries').select('text')
                    .text('Tabular View - ' + dataset.length + ' items')

                tabulate_v2(dataset, dimensions, '#table')
            }
        })
    }

    extract_data()
}