function rocChart(id, data, options) { // set default configuration const cfg = { 'margin': {top: 30, right: 20, bottom: 70, left: 61}, 'width': 470, 'height': 450, 'interpolationMode': 'basis', 'ticks': undefined, 'tickValues': [0, .1, .25, .5, .75, .9, 1], 'fpr': 'fpr', 'tprVariables': [{ 'name': 'tpr0', }], 'animate': true }; //Put all of the options into a variable called cfg if('undefined' !== typeof options){ Object.keys(options).forEach(key => { if('undefined' !== typeof options[key]){ cfg[key] = options[key]; } }) } const tprVariables = cfg['tprVariables']; // if values for labels are not specified // set the default values for the labels to the corresponding // true positive rate variable name tprVariables.forEach((d, i) => { if('undefined' === typeof d.label){ d.label = d.name; } }) console.log('tprVariables', tprVariables); const interpolationMode = cfg['interpolationMode']; const fpr = cfg['fpr']; const width = cfg['width']; const height = cfg['height']; const animate = cfg['animate']; const format = d3.format('.2'); const aucFormat = d3.format('.4r'); const x = d3.scaleLinear().range([0, width]); const y = d3.scaleLinear().range([height, 0]); const color = d3.scaleOrdinal() .range(d3.schemeCategory10); // d3.scaleOrdinal().range(['steelblue', 'red', 'green', 'purple']); const xAxis = d3.axisTop() .scale(x) .tickSizeOuter(0); const yAxis = d3.axisRight() .scale(y) .tickSizeOuter(0); // set the axis ticks based on input parameters, // if ticks or tickValues are specified if('undefined' !== typeof cfg['ticks']) { xAxis.ticks(cfg['ticks']); yAxis.ticks(cfg['ticks']); } else if ('undefined' !== typeof cfg['tickValues']) { xAxis.tickValues(cfg['tickValues']); yAxis.tickValues(cfg['tickValues']); } else { xAxis.ticks(5); yAxis.ticks(5); } // apply the format to the ticks we chose xAxis.tickFormat(format); yAxis.tickFormat(format); // a function that returns a line generator function curve(data, tpr) { const lineGenerator = d3.line() .curve(d3.curveBasis) .x(d => x(d[fpr])) .y(d => y(d[tpr])); return lineGenerator(data); } // a function that returns an area generator function areaUnderCurve(data, tpr) { const areaGenerator = d3.area() .x(d => x(d[fpr])) .y0(height) .y1(d => y(d[tpr])); return areaGenerator(data); } const svg = d3.select('#roc') .append('svg') .attr('width', width + margin.left + margin.right) .attr('height', height + margin.top + margin.bottom) .append('g') .attr('transform', `translate(${margin.left},${margin.top})`); x.domain([0, 1]); y.domain([0, 1]); svg.append('g') .attr('class', 'x axis') .attr('transform', `translate(0,${height})`) .call(xAxis) .append('text') .attr('x', width / 2) .attr('y', 40 ) .style('text-anchor', 'middle') .text('False Positive Rate') const xAxisG = svg.select('g.x.axis'); // draw the top boundary line xAxisG.append('line') .attr({ 'x1': -1, 'x2': width + 1, 'y1': -height, 'y2': -height }); // draw a bottom boundary line over the existing // x-axis domain path to make even corners xAxisG.append('line') .attr({ 'x1': -1, 'x2': width + 1, 'y1': 0, 'y2': 0 }); // position the axis tick labels below the x-axis xAxisG.selectAll('.tick text') .attr('transform', `translate(0,${25})`); // hide the y-axis ticks for 0 and 1 xAxisG.selectAll('g.tick line') .style('opacity', d => // if d is an integer (d % 1 === 0 ? 0 : 1)); svg.append('g') .attr('class', 'y axis') .call(yAxis) .append('text') .attr('transform', 'rotate(-90)') .attr('y', -35) // manually configured so that the label is centered vertically .attr('x', 0 - height/1.56) .style('font-size','12px') .style('text-anchor', 'left') .text('True Positive Rate'); yAxisG = svg.select('g.y.axis'); // add the right boundary line yAxisG.append('line') .attr({ 'x1': width, 'x2': width, 'y1': 0, 'y2': height }) // position the axis tick labels to the right of // the y-axis and // translate the first and the last tick labels // so that they are right aligned // or even with the 2nd digit of the decimal number // tick labels yAxisG.selectAll('g.tick text') .attr('transform', d => { if(d % 1 === 0) { // if d is an integer return `translate(${-22},0)`; } else if((d*10) % 1 === 0) { // if d is a 1 place decimal return `translate(${-32},0)`; } else { return `translate(${-42},0)`; } }) // hide the y-axis ticks for 0 and 1 yAxisG.selectAll('g.tick line') .style('opacity', d => // if d is an integer (d % 1 === 0 ? 0 : 1)); // draw the random guess line svg.append('line') .attr('class', 'curve') .style('stroke', 'black') .attr('x1', 0) .attr('x2', width) .attr('y1', height) .attr('y2', 0) .style('stroke-width', 2) .style('stroke-dasharray', '8') .style('opacity', 0.4); // draw the ROC curves function drawCurve(data, tpr, stroke){ svg.append('path') .attr('class', 'curve') .style('stroke', stroke) .attr('d', curve(data, tpr)) .on('mouseover', d => { const areaID = `#${tpr}Area`; svg.select(areaID) .style('opacity', .4) const aucText = `.${tpr}text`; svg.selectAll(aucText) .style('opacity', .9) }) .on('mouseout', () => { const areaID = `#${tpr}Area`; svg.select(areaID) .style('opacity', 0) const aucText = `.${tpr}text`; svg.selectAll(aucText) .style('opacity', 0) }); } // draw the area under the ROC curves function drawArea(data, tpr, fill) { svg.append('path') .attr('class', 'area') .attr('id', `${tpr}Area`) .style('fill', fill) .style('opacity', 0) .attr('d', areaUnderCurve(data, tpr)) } function drawAUCText(auc, tpr, label) { svg.append('g') .attr('class', `${tpr}text`) .style('opacity', 0) .attr('transform', `translate(${.5*width},${.79*height})`) .append('text') .text(label) .style({ 'fill': 'white', 'font-size': 18 }); svg.append('g') .attr('class', `${tpr}text`) .style('opacity', 0) .attr('transform', `translate(${.5*width},${.84*height})`) .append('text') .text(`AUC = ${aucFormat(auc)}`) .style({ 'fill': 'white', 'font-size': 18 }); } // calculate the area under each curve tprVariables.forEach(d => { const tpr = d.name; const points = generatePoints(data, fpr, tpr); const auc = calculateArea(points); d['auc'] = auc; }) console.log('tprVariables', tprVariables); // draw curves, areas, and text for each // true-positive rate in the data tprVariables.forEach((d, i) => { console.log('drawing the curve for', d.label) console.log('color(', i, ')', color(i)); const tpr = d.name; drawArea(data, tpr, color(i)) drawCurve(data, tpr, color(i)); drawAUCText(d.auc, tpr, d.label); }) /////////////////////////////////////////////////// ////// animate through areas for each curve /////// /////////////////////////////////////////////////// if(animate && animate !== 'false') { //sort tprVariables ascending by AUC const tprVariablesAscByAUC = tprVariables.sort((a, b) => a.auc - b.auc); console.log('tprVariablesAscByAUC', tprVariablesAscByAUC); for(let i = 0; i < tprVariablesAscByAUC.length; i++) { areaID = `#${tprVariablesAscByAUC[i]['name']}Area`; svg.select(areaID) .transition() .delay(2000 * (i+1)) .duration(250) .style('opacity', .4) .transition() .delay(2000 * (i+2)) .duration(250) .style('opacity', 0) textClass = `.${tprVariablesAscByAUC[i]['name']}text`; svg.selectAll(textClass) .transition() .delay(2000 * (i+1)) .duration(250) .style('opacity', .9) .transition() .delay(2000 * (i+2)) .duration(250) .style('opacity', 0) } } /////////////////////////////////////////////////// /////////////////////////////////////////////////// /////////////////////////////////////////////////// function generatePoints(data, x, y) { const points = []; data.forEach(d => { points.push([ Number(d[x]), Number(d[y]) ]) }) return points; } // numerical integration function calculateArea(points) { let area = 0.0; const length = points.length; if (length <= 2) { return area; } points.forEach((d, i) => { const x = 0; const y = 1; if('undefined' !== typeof points[i-1]){ area += (points[i][x] - points[i-1][x]) * (points[i-1][y] + points[i][y]) / 2; } }); return area; } } // rocChart