/* globals d3 */ const radius = 5; const bigRadius = 4 * radius; const bigPadding = 3 * bigRadius; const jitter = 2 * radius; const padding = 3 * radius; const scales = { 'Usability Threshold': d3.scaleLinear() .domain([0, 5]) .range([588 - padding, 490 + padding]), 'Expressiveness Ceiling': d3.scaleLinear() .domain([0, 5]) .range([490 - padding, 392 + padding]), 'Number of Tasks Supported': d3.scaleLinear() .domain([0, 2]) .range([392 - padding, 294 + padding]), 'Editing vs Starting From Scratch': d3.scaleLinear() .domain([0, 2]) .range([294 - padding, 196 + padding]), 'Separation From the Graphics': d3.scaleLinear() .domain([0, 2]) .range([196 - padding, 98 + padding]), 'Optimized for Algorithmic vs Immediate Tasks': d3.scaleLinear() .domain([0, 2]) .range([98 - padding, 0 + padding]) }; const pcaScales = { 'x': d3.scaleLinear() .range([bigPadding, 832 - bigPadding]), 'y': d3.scaleLinear() .range([bigPadding, 832 - bigPadding]) }; const reverseLabels = { 'drawing tools': true, 'direct manipulation-based tools': true, 'GUI-based tools': false, 'grammars': false, 'libraries': false, 'programming languages': false }; d3.text('template.svg', template => { d3.select('#container').html(template); const background = d3.select('#Background'); const scatterFrame = d3.select('#ScatterFrame'); const scatterGroup = d3.select('svg').append('g'); d3.csv('data.csv', data => { // Clean the data data = data.map(row => { Object.keys(row).forEach(key => { let floatValue = parseFloat(row[key]); if (!isNaN(floatValue)) { row[key] = floatValue; } }); return row; }); // Figure out the PCA domains pcaScales.x.domain(d3.extent(data, d => d['PCA 1'])); pcaScales.y.domain(d3.extent(data, d => d['PCA 2'])); // Generate a plot for each possible pairing let pairwisePlots = []; Object.keys(scales).forEach(xKey => { Object.keys(scales).forEach(yKey => { pairwisePlots.push({xKey, yKey}); }); }); let smallMultiples = scatterGroup.selectAll('.smallMultiple').data(pairwisePlots); smallMultiples.exit().remove(); let smallMultiplesEnter = smallMultiples.enter().append('g') .classed('smallMultiple', true); smallMultiples = smallMultiplesEnter.merge(smallMultiples); // Generate the points in those plots let modalities = smallMultiples.selectAll('.modality').data((plot, index) => { // Derive the location of each point in its plot let locationHashTable = {}; data.forEach(d => { let derivedPoint = { Modality: d.Modality, includeLabel: index === 0, x: scales[plot.xKey](d[plot.xKey]), y: scales[plot.yKey](d[plot.yKey]), pcaX: pcaScales.x(d['PCA 1']), pcaY: pcaScales.y(d['PCA 2']) }; if (isNaN(derivedPoint.pcaX) || isNaN(derivedPoint.pcaY)) { throw new Error(); } let hash = derivedPoint.x + ',' + derivedPoint.y; derivedPoint.hash = hash; if (!locationHashTable[hash]) { locationHashTable[hash] = []; } locationHashTable[hash].push(derivedPoint); }); // Where there are hash collisions, jitter the points Object.keys(locationHashTable).forEach(hash => { let pointsAtLocation = locationHashTable[hash]; if (pointsAtLocation.length > 1) { let center = hash.split(',').map(d => parseFloat(d)); pointsAtLocation.forEach((point, index) => { let offset = ((index + 0.5) / pointsAtLocation.length) - 0.5; point.x = center[0] + offset * jitter; point.y = center[1] - offset * jitter; }); } }); // Finally convert the locationHashTable to an array of points return Object.keys(locationHashTable).reduce((acc, hash) => { return acc.concat(locationHashTable[hash]); }, []); }, d => d.Modality); modalities.exit().remove(); let modalitiesEnter = modalities.enter().append('g') .attr('class', d => 'modality ' + d.Modality.replace(/[ -]/g, '')); modalities = modalitiesEnter.merge(modalities); modalitiesEnter.append('circle'); modalitiesEnter.append('text') .attr('x', d => reverseLabels[d.Modality] ? -1.5 * bigRadius : 1.5 * bigRadius) .attr('text-anchor', d => reverseLabels[d.Modality] ? 'end' : 'start') .attr('y', '0.35em') .text(d => d.includeLabel ? d.Modality : ''); modalities.attr('transform', function (d) { return 'translate(' + d.x + ',' + d.y + ')'; }); // Okay, now let's deal with animation. First, get the background to match // the badge let t = d3.transition() .delay(10000) .duration(2500); background // .attr('fill', '#000000') // .transition(t) .attr('fill', '#333333'); // Show the ScatterFrame scatterFrame .attr('opacity', 0) .transition(t) .attr('opacity', 1); // Rotate the scatterGroup scatterGroup .attr('transform', 'translate(109,472) rotate(0)') .transition(t) .attr('transform', 'translate(525,472) rotate(45)'); // Move all the points into their scatterplot positions modalities .attr('transform', d => 'translate(' + d.pcaX + ',' + d.pcaY + ')') .transition(t) .attr('transform', d => 'translate(' + d.x + ',' + d.y + ')'); // Make the points smaller modalities.select('circle') .attr('r', bigRadius) .transition(t) .attr('r', radius); // Hide the labels modalities.select('text') .attr('opacity', 1) .transition(t) .attr('opacity', 0); }); });