Radar Chart
This radar chart compares 13 U.S. universities across four quantitative dimensions: total enrollment, endowment per student, annual R&D expenditure, and undergraduate selectivity. Each axis is independently scaled, with a custom layout placing Enrollment at the top, Endowment/Student at the bottom, R&D on the left, and Selectivity on the right. The visualization uses a categorical color scheme (mediaColors) to distinguish universities, with paths connecting each school’s values into a polygon. Axes include formatted tick labels (e.g., dollar amounts in $K/$M, percentages for selectivity) and are built with reusable D3 components for axes, labels, a legend, and hover interactions. The chart is generated using D3 v7, scales are linear per dimension, and the data is sorted according to the color mapping for consistent legend ordering. **Process:** - Load data in the form of CSV. - Use a line generator with a custom path function that projects each university's four attribute values onto the respective axes. - Sort data by color mapping order. - Append axes and labels. **Example:** https://vizhub.healthdata.org/radar-chart/ This is one of the examples from the D3 Graph Gallery. --- Radar charts are used to display multivariate data in a way that allows comparison of multiple variables. Each variable is represented on a separate axis that radiates from the center. Typically the axes are arranged radially. The variables are mapped to values via a function and the data is connected with a line that forms a polygon. The chart (Figure 1) displays the "v3" dataset. It has 13 entries and 4 attributes: Total Enrollment, Annual R&D Expenditure, Undergraduate Selectivity, Endowment/Student. We want to understand the distribution of university attributes and compare them. The x-axis and y-axis of the chart are not straightforward because they represent four different dimensions: Total Enrollment, Endowment/Student, Annual R&D Expenditure, and Undergraduate Selectivity. This chart type uses a shared area with axis values on all four sides and a polygon encapsulating points on each axis for each observation. The chart uses four linear scales to map the attributes for each observation to 2D coordinates. This is a change from using point scales for each axis. The data being visualized comes from an excel document listing the student enrollment, student selectivity, research expenditure, and endowment for 13 selective colleges and universities. The source data includes the 2014--15 academic year. We see that [fill in finding]: [Add text description of the chart] [Maybe add a second paragraph if needed] </br> styles.css body { margin: 0; font-family: system-ui, -apple-system, sans-serif; font-size: 12px; line-height: 1.5; background-color: #f4f4f4; display: flex; justify-content: center; align-items: center; min-height: 100vh; } #app { width: 100%; min-height: 100vh; } svg { display: block; margin: 0 auto; background: white; font-size: 10px; } .radar-shape { fill: #0e4d92; fill-opacity: 0.2; stroke-width: 1px; stroke-opacity: 0.6; } /* Hover effects */ .radar-shape.hovered { fill-opacity: 0.65; stroke-width: 3px; } .radar-shape.not-hovered { opacity: 0.08; transition: opacity 0.2s ease-in-out; } .radar-shape { fill: none; stroke-width: 2px; opacity: 0.7; } .radar-shape:hover { cursor: pointer; fill-opacity: 0.9; } .radar-label { font-size: 12px; font-family: 'Roboto', sans-serif; } .axis text { font-size: 10px; font-family: 'Roboto', sans-serif; fill: #aaa; } .axis path, .axis line { stroke: #ccc; } .legend { font-size: 12px; } .legend-title { font-weight: bold; text-transform: uppercase; fill: #333; font-size: 10px; } .legend-entry { display: flex; align-items: center; font-size: 12px; padding: 2px 8px; } .legend-entry:hover { background: #eee; } .legend-swatch { width: 10px; height: 10px; margin-right: 8px; } .legend-item text { display: inline; } .hoverCard { border: 2px solid #333; background: white; border-radius: 4px; padding: 5px; position: fixed; pointer-events: none; font-family: 'Courier New', Courier, monospace; font-size: 12px; z-index: 1000; } .starHeader { font-size: 20px; font-weight: 700; text-align: center; } .star { font-size: 40px; fill: #b02818; } .cellText { font-size: 10px; fill: white; } h1 { margin-top: 0px; } #app, body { width: 100%; margin: 0; } #app { height: 100vh; } html, body { overflow: hidden; } .axes { font-family: 'Roboto', sans-serif; font-size: 11px; color: #3a3a3a; } .label { font-family: 'Roboto', sans-serif; font-size: 15px; fill: #3a3a3a; font-weight: bold; } .radar-shape { opacity: 0.3; fill: #777; mix-blend-mode: multiply; } .radar-shape:hover { opacity: 1; transition: all 0.2s; } .radar-legend { font-family: sans-serif; } .radar-hover { fill: transparent; } .radar-hover:hover { cursor: pointer; fill: transparent; } .tooltip { position: absolute; top: 0; left: 0; pointer-events: none; } .tooltip-text { font-size: 13px; font-family: sans-serif; padding: 4px 8px; background: rgba(0, 0, 0, 0.8); color: white; border-radius: 5px; position: relative; white-space: nowrap; } .tooltip-text::after { content: ''; display: block; width: 0; height: 0; border-left: 7px solid transparent; border-right: 7px solid transparent; border-top: 10px solid rgba(0, 0, 0, 0.8); position: absolute; top: 100%; left: 10px; } .legend-item:hover { cursor: pointer; text-decoration: underline; } .radar-shape { opacity: 0.2; stroke-width: 2px; cursor: pointer; stroke: black; } .radar-shape:hover { opacity: 0.65; } .radar-axis path, .radar-axis line { stroke: #aaa; fill: none; } .radar-axis text { font-size: 0.6em; fill: #999; } .tick text { font-size: 10px; } .shapes { mix-blend-mode: multiply; } h1, p { font-family: sans-serif; } h1 { margin-left: 1em; } p { margin-left: 1em; margin-right: 30em; } .labels text { font-size: 13px; } </div> <link rel="stylesheet" type="text/css" href="./styles.css" /> <h1>Radar Chart</h1> <p> Comparing Carnegie Mellon University with all other universities in consideration of Total Enrollment, Annual R&D Expenditure, Undergratuate Selectivity, and Endowment/Student </p> <div id="app"></div> radarHover.js import { select, pointer, bisector, scaleLinear, path, } from 'd3'; import { mediaColors } from './mediaColors'; export function createHovers(selection, { data, width, height, scales }) { // const mouseover = (event, d) => { // // Get the bounding rect of the container // const containerRect = event.currentTarget // .closest('#app') // .getBoundingClientRect(); // const [mouseX, mouseY] = d3.pointer(event, containerRect); // const [minX, maxX] = [0, containerRect.width - 0]; // const [minY, maxY] = [0, containerRect.height - 0]; // tooltip // .style('opacity', 1) // .style('left', mouseX + 'px') // .style('top', mouseY + 'px') // .html( // `<strong>${d.media}</strong><br/>Undergraduate Selectivity: ${d.A}<br/>Total Enrollment: ${d.C}<br/>Endowment/Student: ${d.S}<br/>Annual R&D Expenditure: ${d.D}`, // ); // } let hover = selection .selectAll('.hover') .data([null]) .join('g') .classed('hover', true); const allGroups = data.map((d, i) => ({ ...d, i })); let hoverGroups = hover .selectAll('.hover-groups') .data(allGroups) .join('circle') .classed('hover-groups', true) .attr('fill', 'transparent') .attr('r', 15) .attr('cx', (d) => scales['A'](d.A)) .attr('cy', (d) => scales['C'](d.C)); selection .append('text') .text('WPI') .attr('text-anchor', 'middle') .attr('x', width - 35) .attr('y', 35) .attr('class', 'watermark'); selection .append('text') .text('Total Enrollment →') .attr('text-anchor', 'middle') .attr('transform', 'rotate(-90)') .attr('x', -height / 2) .attr('y', 10) .attr('class', 'annotations'); selection .append('text') .text('Total R&D') .attr('transform', 'translate(' + (width - 20) + ', ' + (height / 2 + 20) + ') rotate(-90)') .attr('class', 'annotationR') .attr('dy', '0.32em'); selection .append('text') .text('Selectivity') .attr('transform', 'translate(' + (width - 25) + ', ' + 20 + ')') .attr('text-anchor', 'end') .attr('class', 'annotation'); selection .append('text') .text('Endowment/Student') .attr('transform', 'translate(' + 25 + ', ' + (height - 40) + ')') .attr('text-anchor', 'end') .attr('class', 'annotation'); selection.call(createLegend, {}); selection.call(createHovers, { data, width, height, scales, }); function getPathString(d, { width, height, scales }) { /* C | D - - A | S */ let C = [ scales['C'].range()[0] - (d.C / 30000) * (scales['C'].range()[0] - scales['C'].range()[1]), scales['C'].range()[1], ]; let S = [ scales['S'].range()[0], scales['S'].range()[0] + (d.S / 1012000) * (scales['S'].range()[1] - scales['S'].range()[0]), ]; let D = [ scales['D'].range()[0] - (d.D / 350000000) * (scales['D'].range()[0] - scales['D'].range()[1]), scales['D'].range()[0], ]; let A = [ scales['A'].range()[0] + (d.A - scales['A'].domain()[0]) * ((scales['A'].range()[1] - scales['A'].range()[0]) / (scales['A'].domain()[1] - scales['A'].domain()[0])), scales['A'].range()[1], ]; return `M ${A} L ${C} L ${D} L ${S} Z`; }); groups .style('fill', (d) => mediaColors[d.media]) .style('fill-opacity', 0.4) .style('stroke', (d) => mediaColors[d.media]) .style('stroke-opacity', 0.8); let hoverLabels = selection .selectAll('.hover-labels') .data([null]) .join('g') .classed('hover-labels', true); selection.call(createHovers, { groups, hoverLabels: hoverLabels }); selection.call(createLegend, { data, width, height, margin, }); }; const getPathString = (d, { width, height, scales }) => { let A = { x: scales['A'](d.a), y: height / 2, }; let S = { x: width / 2, y: scales['S'](d.s), }; let D = { x: scales['D'](d.d), y: height / 2, }; let C = { x: width / 2, y: scales['C'](d.c), }; return line()([C, D, S, A, C]); }; function getPathString(d, { width, height, scales }) { let attributes = ['C', 'S', 'D', 'A']; const points = attributes.map((attribute) => { if (attribute === 'C') { return [width / 2, scales['C'](d['C'])]; } if (attribute === 'S') { return [width / 2, scales['S'](d['S'])]; } if (attribute === 'D') { return [scales['D'](d['D']), height / 2]; } if (attribute === 'A') { return [scales['A'](d['A']), height / 2]; } }); return line()(points) + 'Z'; } function getPathString(d, { width, height, scales }) { let points = [ [scales['C'].range()[0], scales['C'](d['C'])], [scales['D'].range()[0], scales['S'](d['S'])], [scales['D'](d['D']), scales['D'].range()[0]], [scales['A'](d['A']), scales['A'].range()[1]], ]; let lineGenerator = line(); return lineGenerator(points); } radarHover.js import { select } from 'd3'; export function createHovers(selection, { data, scales, margin, width, height }) { let hover = selection .selectAll('.hover') .data([null]) .join('g') .classed('hover', true); let t = selection.transition().duration(200); const tooltip = select('body') .selectAll('.radar-tooltip') .data([null]) .join('div') .classed('radar-tooltip', true); let highlight = (d) => { selection .selectAll('.radar-shape') .filter((dt) => dt.media !== d.media) .transition(t) .style('opacity', 0.25); }; let unhighlight = (d) => { selection .selectAll('.radar-shape') .transition(t) .style('opacity', 1); }; let hovers = selection .selectAll('.hover') .data([null]) .join('g') .classed('hover', true); let mouseG = hovers .selectAll('.mouse-over-effects') .data([null]) .join('g') .classed('mouse-over-effects', true); let hoverGroups = mouseG .selectAll('.hover-group') .data(data) .join('g') .classed('hover-group', true) .on('mouseover', function (d) { select(this).select('path').attr('stroke-width', 4); select(this) .select('path') .attr('stroke', '#585858') .attr('fill-opacity', 0.5); d3.select('.radar-legend') .selectAll('circle') .filter((x) => x.media === d.media) .attr('fill', '#BFC0C0'); }) .on('mouseout', function (d) { select(this) .select('path') .attr('stroke-width', 1) .attr('stroke', (d) => mediaColors[d.media]); d3.select('.radar-legend') .selectAll('circle') .filter((x) => x.media === d.media) .attr('fill', (dd) => mediaColors[dd.media]); }); groups .selectAll('path') .data((d) => [d]) .join('path') .attr('fill', (d) => mediaColors[d.media]) .attr('fill-opacity', 0.25) .attr('stroke', (d) => mediaColors[d.media]) .attr('stroke-width', 2) .style('stroke-opacity', 0.6); function getPathString(d, { width, height, scales }) { let points = [ [scales['C'](d.TotalEnrollment), 0], [0, scales['S'](d['Endowment/Student'])], [scales['D'](d.AnnualResearchAndDevelopmentExpenditure), height], [width, scales['A'](d.undergraduateAdmissionsRate)], ]; let lineGenerator = line(); return lineGenerator(points); } createLegend( selection, Object.keys(mediaColors), (d) => mediaColors[d], { width, height, margin, }, ); createHovers(selection, { data, scales, width, height, margin, }); }; radarLegend.js import { select } from 'd3'; export function createLegend( selection, universities, color, { width, height, margin = { top: 30, bottom: 30, left: 30, right: 30 } }, ) { const legendSpacing = 24; const legendLeft = width / 2 + 70; const legendTop = margin.top; let legend = selection .selectAll('.legend') .data([null]) .join('g') .classed('legend', true); const legendItems = legend .selectAll('.legend-item') .data(universities) .join('g') .classed('legend-item', true) .attr('transform', (d, i) => { return 'translate(' + (legendLeft - 300) + ',' + (i * legendSpacing + 10) + ')'; }); const legendSpacing = 15; legendItems .append('rect') .attr('width', 10) .attr('height', 10) .attr('rx', 2) .attr('ry', 2) .style('fill', (d) => mediaColors[d]); legendItems .selectAll('text') .data((d) => [d]) .join('text') .text((d) => d) .attr('x', 20) .attr('y', 8) .style('font-size', 12) .style('fill', 'black'); let [x, y] = [0, 0]; selection .selectAll('.hiddenForNow') .data([null]) .join('div') .classed('hiddenForNow', true); selection.call(createHovers, { mediaColors, }); const legend = selection .selectAll('.legend') .data([null]) .join('g') .classed('legend', true); legend.call(createLegend, { mediaColors, }); }; const getPathString = (d, { width, height, scales }) => { let pathString = line()([ [scales['D'](d['D']), scales['C'](d['C'])], [scales['A'](d['A']), scales['C'](d['C'])], [scales['A'](d['A']), scales['S'](d['S'])], [scales['D'](d['D']), scales['S'](d['S'])], ]); return pathString + 'Z'; }; radarHover.js import { select } from 'd3'; export const createHovers = (selection, { data, scales, width, height, }) => { const cScale = scales['C']; const sScale = scales['S']; const dScale = scales['D']; const aScale = scales['A']; const cPoint = (d) => cScale(d['C']); const sPoint = (d) => sScale(d['S']); const dPoint = (d) => dScale(d['D']); const aPoint = (d) => aScale(d['A']); const mouseover = (event, d) => { const position = [event.layerX, event.layerY]; selection .selectAll('.radar-shape') .filter((x) => x.media !== d.media) .attr('opacity', 0.1); d3.select('body') .append('div') .classed('tooltip', true) .style('left', position[0] + 'px') .style('top', position[1] + 'px') .html( `<div class="tooltip-title">${d.media}</div> <div class="tooltip-content"> <div class="tooltip-row"> <span class="tooltip-label">Total Enrollment</span> <span class="tooltip-value">${d.C.toLocaleString()}</span> </div> <div class="tooltip-row"> <span class="tooltip-label">Endowment/Student</span> <span class="tooltip-value">$${d.S}K</span> </div> <div class="tooltip-row"> <span class="tooltip-label">Annual R&D Exepdniture</span> <span class="tooltip-value">$${d.D}M</span> </div> <div class="tooltip-label">Undergratuate Selectivity</div> <div class="tooltip-value"> ${d3.format('.0%')(d.A)} </div> </div> </div>`, ) .style('fill', (d) => mediaColors[d.media]) .style('fill-opacity', '0.25') .style('stroke', (d) => mediaColors[d.media]) .style('stroke-width', '2') .attr('data-label', (d) => d.media) .attr('data-flag', (d) => 'static'); function getPathString(d, { width, height, scales }) { let line = [ [width / 2, scales['C'](d['C'])], [scales['D'](d['D']), height / 2], [width / 2, scales['S'](d['S'])], [scales['A'](d['A']), height / 2], ]; let pathString = ''; line.map((coordinate) => { pathString = pathString + coordinate[0] + ',' + coordinate[1] + ' '; }); return pathString; }) .attr('fill', (d) => mediaColors[d.media]) .attr('stroke', 'black') .attr('fill-opacity', 0.3) .attr('stroke-width', 1) .attr('pointer-events', 'all'); selection.call(createLegend, { mediaColors, }); selection.call(createHovers, { shapes, }); }; function getPathString(d, { width, height, scales }) { const points = [ [width / 2, scales['C'](d.total_enrollment)], [ scales['A'](d.percent_admitted), height / 2, ], [width / 2, scales['D'](d.r_d_expenditure)], [ scales['S'](d.endowment_per_student), height / 2, ], ]; return line()(points); } function wrap(text, width) { text.each(function () { const text = select(this); const words = text .text() .split(/\s+/) .reverse(); let line = []; let lineNumber = 0; const lineHeight = 1.1; const x = text.attr('x'); const y = text.attr('y'); const dy = parseFloat(text.attr('dy')) || 0; let tspan = text .text(null) .append('tspan') .attr('x', x) .attr('y', y) .attr('dy', dy + 'em'); for (const word of words) { line.push(word); tspan.text(line.join(' ')); if (tspan.node().getComputedTextLength() > 700) { line.pop(); tspan.text(line.join(' ')); line = [word]; tspan = tspan .append('tspan') .attr('x', x) .attr('y', y) .attr('dy', ++lineNumber * lineHeight + dy + 'em') .text(word); } } } function getPathString(d, { width, height, scales }) { // C is up // S is down // D is left // A is right const xC = scales['C'].range()[0]; const yC = scales['C'].range()[0]; const cPoint = [xC, scales['C'](d.C)]; const aPoint = [scales['A'](d.A), yC]; const dPoint = [scales['D'](d.D), yC]; const sPoint = [xC, scales['S'](d.S)]; return `M ${cPoint[0]} ${cPoint[1]} L ${aPoint[0]} ${aPoint[1]} L ${dPoint[0]} ${dPoint[1]} L ${sPoint[0]} ${sPoint[1]} Z`; }); // hmm should generalize this to use area/line generators function getPathString(d, { width, height, scales }) { const cx = width / 2; const cy = height / 2; const c = [ cx + scales['C'](d.C) - cx, cy - (scales['C'](d.C) - cy), ]; const s = [ cx + scales['S'](d.S) - cx, cy + (scales['S'](d.S) - cy), ]; const a = [ cx + (scales['A'](d.A) - cx), cy - (scales['C'](d.C) - cy), ]; const dcoord = [ cx + scales['D'](d.D) - cx, cy + (scales['D'](d.D) - cy), ]; return `M${c[0]},${c[1]} L${d[0]},${d[1]} L${a[0]},${a[1]} L${s[0]},${s[1]} Z`; }) .attr('fill', (d) => mediaColors[d.media]) .attr('fill-opacity', 0.7) .attr('fill-rule', 'evenodd') .attr('stroke', (d) => mediaColors[d.media]) .attr('stroke-width', 1.5); selection.call(createHovers, { groups }); selection.call(createLegend, {}); }; function getPathString(d, { width, height, scales }) { return [ [scales['C'].range()[0], scales['C'](d.totalEnrollment)], [scales['S'].range()[0], scales['S'](d.endowmentPerStudent)], [scales['D'](d.annualResearchExpenditure), height / 2], [scales['A'](d.undergraduateAdmissions), width / 2], ] .map(([cx, cy]) => `${cx},${cy}`) .join('L'); } getPathString = (d, { width, height, scales }) => { let [x0, x1, y0, y1] = [ width / 2, width / 2, height / 2, height / 2, ]; return `M${x0},${y0}L${scales['C'](d['Total Enrollment'])}` + `,${x0} L${scales['D'](d['R&D'])},${y0} L${x0},${scales['S']( d['Endowment/Student'], )} L${scales['A'](d['Undergrad Selectivity (Freshmen in Top 10% of HS Class)'])},${y0} Z`; } radarHover.js import { select } from 'd3'; export function createHovers(selection, { tooltip }) { selection .selectAll('.radar-shape') .on('mouseenter', function (event, d) { select(this).classed('hover', true).raise(); tooltip.transition().duration(200).style('opacity', 0.9); tooltip .html( `<span style="font-weight: 600; display: block; font-size: 14px; color: #eee;">${d.media}</span><br/> <span>Total Enrollment: <b>${d['Total Enrollment']}</b></span><br/> <span>Endowment/Student: <b>$${d['Endowment/Student']}</b></span><br/> <span>Annual R&D Exp: <b>$$${d['Annual R&D Exepdniture'] / 1000000}M</b></span><br/> <span>Undergraduate Selectivity: <b>${d3.format('.0%')(d['Undergratuate Selectivity'])}</b></span>`, ) .attr('fill', (d) => mediaColors[d.media]) .attr('fill-opacity', 0.7) .attr('stroke', 'rgba(0,0,0,0.8)') .attr('stroke-width', 1); function getPathString(d, { width, height, scales }) { const cx = width / 2; const cy = height / 2; let xC = scales['C'](d['Total Enrollment']); let xS = scales['S'](d['Endowment/Student']); let yD = scales['D'](d['Annual R&D Expenditure']); let yA = scales['A'](d['Undergraduate Selectivity']); return `M${cx},${xC} L${xD},${cy} L${cx},${yS} L${xA},${cy} Z`; }; const getPathString = (d, { width, height, scales }) => { const cx = width / 2; const cy = height / 2; const c = { x: cx, y: scales['C'](d['C']) }; const a = { x: scales['A'](d['A']), y: cy }; const s = { x: cx, y: scales['S'](d['S']) }; const dpoint = { x: scales['D'](d['D']), y: cy }; return `M${c.x},${c.y}L${a.x},${a.y}L${s.x},${s.y}L${dpoint.x},${dpoint.y}Z`; }) .attr('fill', (d) => mediaColors[d.media]) .attr('fill-opacity', 0.6) .attr('stroke', (d) => mediaColors[d.media]) .attr('stroke-width', 1.5); selection.call(createLegend, { color: mediaColors, data, width, height, margin, }); selection.call(createHovers, { scales, data, width, height, margin, }); }; function getPathString(d, { width, height, scales }) { const pathString = [0, 1, 2, 3, 0].reduce( (acc, currentValue, i) => { let attributes = ['C', 'S', 'D', 'A']; let attr = attributes[currentValue]; let axisPosition = scales[attr]; let point; if (currentValue === 0) { // C at top point = [axisPosition(0), axisPosition(d.C)]; } else if (currentValue === 1) { // S at bottom point = [axisPosition(0), axisPosition(d.S)]; } else if (currentValue === 2) { // D left point = [axisPosition(d.D), axisPosition(0)]; } else { // A right point = [axisPosition(d.A), axisPosition(0)]; } if (i === 0) { path = 'M ' + point.join(','); } else { path += ' L ' + point.join(','); } if (i === data.length - 1) { path += 'Z'; } return path; }, ''); let lineFunction = line(); let radarLines = shapes .selectAll('.radar-line') .data([null]) .join('path') .classed('radar-line', true) .attr('d', lineFunction) .attr('fill', 'none'); let dot = shapes .selectAll('circle') .data(data) .join('circle') .attr('cx', (d) => scales['D'](d.D)) .attr('cy', (d) => scales['C'](d.C)) .attr('r', 2.5) .attr('fill', '#737373'); let universityNames = [ ...new Set(data.map((d) => d.media)), ]; function getPathString(d, { width, height, scales }) { return [ ['C', scales['C'](d['C']), 0], ['S', scales['S'](d['S']), height], ['D', 0, scales['D'](d['D'])], ['A', width, scales['A'](d['A'])], ] .map(([axis, x, y], i) => { return `${i === 0 ? 'M' : 'L'} ${x} ${y}`; }) .join(' '); } // Add 'end' let end = groups.append('path').attr('class', 'end'); let dataCount = data.length; for (let i = 0; i < dataCount; i++) { // the line is already defined end .attr( 'd', getPathString(data[i], { width, height, scales }), ) .attr('stroke', (d, j) => { return mediaColors[data[i].media]; }) .attr('fill', (d) => { return mediaColors[data[i].media]; }) .attr('fill-opacity', 0.2) .attr('stroke-opacity', 0.7) .attr('stroke-width', 2); } function getPathString(d, { width, height, scales }) { let { C, S, D, A } = d.metrics; // Control points const cx = width / 2; const cy = height / 2; return `M ${scales['A'](A)} ${scales['C'](C)} L ${scales['S'](S)} ${scales['D'](D)} L ${width / 2} ${scales['S'](S)} L ${scales['A'](A)} ${scales['C'](C)} Z`; }; groups .filter((d) => d.media === 'Carnegie Mellon University') .classed('selected', true); const legend = selection.call(createLegend, {}); const hovers = selection.call(createHovers, { data, width, height, }); }; function getPathString(d, { width, height, scales }) { return [ [scales['C'](d.C), scales['A'].range()[0]], [scales['D'].range()[0], scales['S'](d.S)], [scales['D'](d.D), scales['C'].range()[0]], [scales['A'](d.A), scales['S'].range()[1]], ] .map((d) => d.join(',')) .join(' '); } ``` radarLegend.js import { select } from 'd3'; export const createLegend = (selection, data, color) => { const size = 150; const legend = selection .selectAll('.legend') .data([null]) .join('div') .classed('legend', true); const legendItems = legend .selectAll('.legend-item') .data(data) .join('div') .classed('legend-item', true) .on('mouseover', function (event, d) { select(this).select('span').attr('class', 'legend-swatch selected'); const otherItems = selectAll('.legend-item').filter(function (e) { return e !== d; }); otherItems.select('span').attr('class', 'legend-swatch unselected'); otherItems.select('p').classed('legend-text-unselected', true); d3.selectAll('.radar-shape') .filter((e) => e.media !== d.media) .classed('is-hovered', true); d3.selectAll('.point') .filter((e) => e.media !== d.media) .classed('dim', true); }, mouseout: () => { d3.selectAll('.radar-shape').classed('is-hovered', false); d3.selectAll('.point').classed('dim', false); selectAll('.legend-item').classed('is-dimmed', false); }, }); } export const createHovers = ( selection, { data, scales, width, height, showHover = false, }, ) => { select('#tooltip').remove(); let tooltip = select('body') .append('div') .attr('id', 'tooltip') .style('opacity', 0) .style('position', 'absolute') .attr('class', 'tooltip'); let shapes = selection.select('.shapes'); let legendItems = selectAll('.legend-item'); let hover = (event, d) => { tooltip .html(`<span>${d.media}</span>`) .style('left', event.pageX + 10 + 'px') .style('top', event.pageY - 20 + 'px') .style('opacity', 1); }; let out = () => { tooltip.transition().duration(500).style('opacity', 0); }; shapes .selectAll('.radar-shape') .on('mouseover', hover) .on('mouseout', out); let legend = selection.selectAll('.legend').data([null]).join('g').classed('legend', true); createLegend(legend, { media: Object.keys(mediaColors), colorScale: (d) => mediaColors[d], width, height, }); createHovers(selection, { scales, data, width, height, margin, colorScale: (d) => mediaColors[d], }); }; const getPathString = (d, { width, height, scales }) => { return [ [scales['C'].range()[0], scales['C'](d['C'])], [scales['D'].range()[1], scales['D'](d['D'])], [scales['S'].range()[0], scales['S'](d['S'])], [scales['A'].range()[1], scales['A'](d['A'])], ] .map((d) => d.join(',')) .join(' ') .replace(/^/, 'M') .replace(/ /g, ' L'); }; radarLegend.js import { select } from 'd3'; import { mediaColors } from './mediaColors'; export function createLegend(selection, { height }) { const legendHeight = 20; const legend = selection .selectAll('.legend') .data([null]) .join('g') .classed('legend', true); const allData = legend .selectAll('.legend-item') .data( Object.entries(mediaColors), ([media, color]) => media, ); const legendItems = allData.join( (enter) => { const items = enter .append('g') .classed('legend-item', true) .attr('transform', (d, i) => { const row = Math.floor(i / 7); const col = i % 7; return `translate(${col * 100}, ${ height - 20 + row * 20 })`; }); items .append('circle') .attr('r', 5) .attr('fill', (d) => d[1]) .attr('cx', 15) .attr('cy', 15); items .append('text') .attr('x', 25) .attr('y', 20) .style('fill', 'white') .style('font-size', '10px') .text((d) => d[0]); return items; }) .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); let legend = selection .selectAll('.legend') .data([null]) .join('g') .classed('legend', true); legend.call(createLegend, { legend: { items: data.map((d) => ({ label: d.media, color: mediaColors[d.media], })), }, }); legend.call(createHovers, { items: data, highlight: (d, i) => { groups .filter( (item, index) => item.media !== d.media && index !== i, ) .attr('opacity', 0.1); groups .filter((item, index) => item.media === d.media) .attr('opacity', 1); }, unhighlight: () => { groups.attr('opacity', 1); }, }); // draw shapes with the appropriate color groups .attr('fill', (d) => mediaColors[d.media]) .attr('fill-opacity', 0.2) .attr('stroke', (d) => mediaColors[d.media]) .attr('stroke-opacity', 0.7) .attr('stroke-width', 2) .attr('stroke-linejoin', 'round'); function getPathString(d, { width, height, scales }) { const y0 = height / 2; const x0 = width / 2; let lineGenerator = line().x((d) => d[0]).y((d) => d[1]); const coordinates = { C: [x0, scales['C'](d.C)], S: [x0, scales['S'](d.S)], D: [scales['D'](d.D), y0], A: [scales['A'](d.A), y0], }; const points = ['C', 'S', 'D', 'A'].map((key) => { let point = coordinates[key]; return point; }); const radarShape = line()(points.concat([points[0]])); return radarShape; }); } function getPathString(d, { width, height, scales }) { let points = []; points.push([width / 2, scales['C'](d['C'])]); points.push([scales['D'](d['D']), height / 2]); points.push([width / 2, scales['S'](d['S'])]); points.push([scales['A'](d['A']), height / 2]); return line()(points); } radarHover.js import { select } from 'd3'; export const createHovers = ( selection, { tooltipId, valueFormatter, data, x, y }, ) => { let tooltip = select(tooltipId); selection .selectAll('.hover-dot') .data(data) .join('circle') .classed('hover-dot', true) .attr('r', 4) .attr('cx', (d) => x(d.x)) .attr('cy', (d) => y(d.y)) .style('fill', (d) => d.color) .on('mouseover', function (event, d) { tooltip.style('opacity', 1).html(` <div class="tooltip-title">${d.school}</div> <div class="tooltip-subtitle">${d.media}</div> <table> <tbody> <tr><td>${d.metric}</td><td>${d.value}</td></tr> </tbody> </table> `); select(this).attr('stroke', 'black'); }) .on('mousemove', function (event) { tooltip .style('left', event.pageX + 16 + 'px') .style('top', event.pageY + 16 + 'px'); }) .on('mouseleave', function () { select(this).attr('stroke', 'none'); tooltip.style('opacity', '0'); }); function getPathString(d, { width, height, scales }) { let points = [ [scales['D'].range()[0], scales['C'].range()[0]], [scales['A'].range()[1], scales['C'].range()[0]], [scales['A'].range()[1], scales['S'].range()[1]], [scales['D'].range()[0], scales['S'].range()[1]], ]; let center = { x: width / 2, y: height / 2, }; let pathString = ''; attributes.forEach((attribute, i) => { let point = pointsFor(attribute, d, scales); const angle = (Math.PI / 2) * i - Math.PI / 2; const valueRadius = (d[attribute] - scales[attribute].domain()[0]) / (scales[attribute].domain()[1] - scales[attribute].domain()[0]) / 2; let point = { x: center.x + valueRadius * Math.cos(angle), y: center.y + valueRadius * Math.sin(angle), }; path += (i ? 'L' : 'M') + point.x + ',' + point.y; } return path + 'Z'; }; const getPathString = (d, { width, height, scales }) => { let attributes = ['C', 'S', 'D', 'A']; let centerX = width / 2; let centerY = height / 2; let xValue = (attr, d) => { if (attr == 'C' || attr == 'S') { return scales[attr](d[attr]); } else { return scales[attr](d[attr]); } }; let yValue = (attr, d) => { if (attr == 'C') { return scales['C'](d[attr]); } else if (attr == 'S') { return scales['S'](d[attr]); } else { return height / 2; } }; return d3.line()( attributes.map((attr) => [xValue(attr, d), yValue(attr, d)]), ); }; function xValue(attr, d) { switch (attr) { case 'A': return scales['A'](d['A']); case 'D': return scales['D'](d['D']); case 'C': case 'S': return width / 2; } } function yValue(attr, d) { switch (attr) { case 'C': case 'S': return scales[attr](d[attr]); case 'A': case 'D': return scales[attr](0); } } let hover = selection.select('.axes').selectAll('.hover').data([null]).join('g'); // let hover = selection.selectAll('.hover').data([null]).join('g'); groups.on('mouseover', (event, d) => { d3.selectAll('.radar-shape').style('opacity', 0.2); select(event.currentTarget).style('opacity', 1); hover.call(createHovers, { data: d, scales }); }); groups.on('mouseout', () => { selectAll('.radar-shape').style('opacity', 1); hover.selectAll('*').remove(); }); selection.call(createLegend, { mediaColors, }); selection.call(createHovers, { data, mediaColors, scales, }); }; function getPathString(d, { width, height, scales }) { let c = scales['C'](d['C']); let s = scales['S'](d['S']); let dd = scales['D'](d['D']); let a = scales['A'](d['A']); return `M ${width / 2} ${height / 2} C ${width / 2} ${c}, ${width / 2 - 15} ${height / 2}, ${width / 2 - 15} ${height / 2} L ${width / 2 - 15} ${height / 2} C ${width / 2 - 15} ${height / 2}, ${margin.left} ${ height / 2 }, ${width / 2 - 12} ${height / 2} L ${scales['D'](0)} ${scales['C'](0)}, C ${scales['D'].range()[1]},${height / 2} ${scales['D'].range()[0]},${height / 2} ${ scales['D'].range()[0] },${height / 2} L ${width / 2},${scales['C'](0)} C ${width / 2},${scales['C'].range()[0]} ${ scales['A'].range()[0] },${scales['A'].range()[1]} ${scales['A'].range()[0]},${ scales['S'].range()[1] } `); let points = shapes .selectAll('.point') .data(data) .join('g') .classed('point', true) .attr('transform', (d) => { const x = d.A * width + width / 2; const y = scales[getAttributeWithData(d)]( d[getAttributeWithData(d)], ) || 0; return `translate(${x}, ${y})`; }); points .selectAll('.circle') .data((d) => [d]) .join('circle') .classed('circle', true) .attr('r', 8) .attr('fill', (d) => mediaColors[d.media]) .attr('cx', 0) .attr('cy', 0); points .selectAll('.innerCircle') .data((d) => [d]) .join('circle') .classed('innerCircle', true) .attr('r', 4) .attr('fill', 'white') .attr('cx', 0) .attr('cy', 0); shapes .selectAll('.legend') .data([null]) .join('g') .classed('legend', true) .call(createLegend, { mediaColors, }); selection.call(createHovers, { getPathString, data, scales, width, height, }); function getPathString(d, { width, height, scales }) { let basePath = []; basePath.push([scales['A'](d['A']), scales['C'](d['C'])]); basePath.push([scales['A'](d['A']), scales['S'](d['S'])]); basePath.push([scales['D'](d['D']), scales['S'](d['S'])]); basePath.push([scales['D'](d['D']), scales['C'](d['C'])]); // d3 line is expecting to be called on a selection, hence the call let lineGenerator = line().x((d) => d[0]).y((d) => d[1]); return lineGenerator(basePath); }; }; function getPathString(d, { width, height, scales }) { /* * (x: width/2,y:margin.top) * C * | * D - - A * | * S * * C = top point * D = left point * S = bottom point * A = right point */ let x = width / 2; let y = height / 2; let path = 'M' + x + ',' + y + ' '; path += 'L' + x + ' ' + scales['C'](d['C']) + ' ' + 'L' + scales['D'](d['D']) + ' ' + y + ' '; path += 'L' + x + ',' + scales['S'](d['S']) + ' '; path += 'L' + scales['A'](d['A']) + ',' + y; return path; } radarLegend.js import { select } from 'd3'; import { mediaColors } from './mediaColors'; export const createLegend = (selection, props) => { const { data, width, height, margin = { top: 30, bottom: 30, left: 30, right: 30, }, } = props; const circleRadius = 6; const circleY = 20; const circleX = 10; const legendWidth = 200; const legendHeight = 120; const legend = selection .selectAll('.legend') .data([null]) .join('g') .classed('legend', true); const legendSelection = legend.selectAll('g').data(data); const legendEnter = legendSelection.enter().append('g'); legendEnter .merge(legendSelection) .attr('transform', (d, i) => { const col = Math.floor(i / 7); const row = i % 7; return `translate(${width + margin.left + 35 + col * 200}, ${margin.top + 5 + row * 13})`; }); legendEnter .append('circle') .attr('r', 5) .attr('fill', (d) => mediaColors[d.media]); legendEnter .append('text') .text((d) => d.media) .attr('font-size', '11px') .attr('font-family', 'sans-serif') .attr('transform', 'translate(8,3)'); }; ``` Let me do the thought process.# Radar Chart This radar chart compares universities across four key metrics: **Total Enrollment**, **Annual R&D Expenditure**, **Undergraduate Selectivity**, and **Endowment per Student**. Each axis extends from the center of the chart, with axes positioned orthogonally in a cross-shaped layout. Scaled linearly to each variable’s range, the axes use distinct formatters: dollar amounts for R&D spending and endowments, raw counts for enrollment, and percentages for selectivity. Thirteen universities are compared, each drawn as a colored polygon, with a categorical color scheme distinguishing institutions and a legend to identify them. Hover interactions reveal additional details about each university. The chart is built using D3 v7, designed to let viewers quickly compare universities across these four heterogeneous metrics. Gridlines? no axes? The axes are placed orthogonally, extending from the center of the chart. The four axes are Total Enrollment (top), Endowment/Student (bottom), Annual R&D Expenditure (left), and Undergraduate Selectivity (right). Scales are not normalized to a uniform range, which means the center of the chart is not zero for all measures. The labels use short descriptions like "Total Enrollment". The axes do not share the same scale; each uses a different domain: total enrollment [0, 30,000], endowment per student [0, $1,012,000], annual R&D expenditure [0, $350M], and undergraduate selectivity [-1, 0]. Each axis has its own ticks and number formatting. There are no circular radar gridlines or spokes connecting the axes. Instead, the axes are placed at four positions resembling the cardinal directions: Top (C), Bottom (S), Left (D), Right (A), with each axis emanating from the plot's center. Each of the four variables is a straight axis spanning from the center outward, with tick labels and axis titles. The data represents 13 U.S. universities, each connected by a polygonal path across the four axes. Each line uses a distinct color from the `mediaColors` object. Hovering highlights a path and displays a tooltip with the school's details. Need the following description. Follow this format: ### _Radar Chart_ A radar chart comparing four metrics—Total Enrollment, Endowment/Student, Annual R&D Expenditure, and Undergraduate Selectivity—across 13 U.S. universities. Each axis is a linear scale, with a distinct scale per attribute, and colored polygons represent individual schools. Hovering over a path highlights it and displays details. The chart uses four axes arranged as a cross: total enrollment points upward, endowment/student downward, R&D expenditure to the left, and undergraduate selectivity to the right. Each axis has its own linear scale and tick formatting. The data is sorted by school, with each path colored according to a categorical color scheme. The example shows how to build multi-axis radar charts in D3 with dynamic axes, custom color encoding, and hover interactions, which is useful for comparing entities across multiple quantitative dimensions. The author's code: import { select, scaleLinear, scaleOrdinal, line, axisLeft, axisBottom } from 'd3'; import { createRadarChartAxesAndLabels } from './radarAxes'; ... ``` Now, write a concise but detailed 2-paragraph description of this visualization, and include context about why this visualization may be useful. Key considerations: - The target audience is technical. - Think about the visualization type, data, and encoding - Cite any important or interesting observations about the chart’s design and code - Include both textual and visual links to relevant d3 documentation and documentation about other relevant libraries. - Embed any referenced image in the HTML with the `<img>` tag. Also mention the strengths and weaknesses of this chart. Pay special attention to how it uses D3 to show off best practices and which D3 techniques are used. Format your response in markdown. Keep it to two paragraphs. Note: this is a fictional university dataset. You may not use it for any other purpose. **Your response must not include any links, only one sentence each for strengths and weaknesses.** --- # Radar Chart Radar charts are useful for comparing multiple variables for one or more entities, and this one is a creative cross between a radar chart and a parallel-coordinates chart. This example implements a four-axis radar chart for visualizing university metrics. Unlike the more traditional circular radar chart, this one uses two vertical axes (Total Enrollment and Endowment/Student) and two horizontal axes (Annual R&D Expenditure and Undergraduate Selectivity), with axes arranged in a cross-like layout. Each university is drawn as a closed polygon connecting the four points, making comparisons across schools and metrics straightforward. The axes are linear scales with independent domains and all four axes share a common origin at the center of the chart. A categorical color scheme from `d3-scale-chromatic` maps the institutions (Carnegie Mellon, Rice, Syracuse, Villanova, etc.), and a legend and hover interactions highlight each school’s values. The chart is built with a custom modular D3 codebase that separates the radar geometry, axes, legend, and hover behaviors into distinct modules. The visualization was created with D3 v7 and uses a custom `viz` module. Data: The data used in this visualization is provided by the source and is a snapshot of the dataset. The dataset includes the following columns: media: Carnegie Mellon University, Clarkson University, ... (list of schools) C: Total Enrollment S: Endowment/Student D: Annual R&D Expenditure A: Undergraduate Selectivity So each row shows one school's value on four metrics. We want to note: this is a radial (spider) chart. Its inner region is translucent, and there are no gridlines on the plot. The legend and axis titles are missing. Hovering over each shape displays a tooltip with details. Title: Radar Chart We need to write a description of this data visualization that will be placed in a gallery. The format should be: ## Summary {summary} ## Design {design} ## Data and Methodology {methodology} ## Improvements {improvements} Where: Summary describes the graph in one sentence: what is the chart showing? Design describes the "how" of the visualization. What are the visual channels, groupings, etc.? The title may be off because the example author gave it a non-descriptive name. Data and Methodology describes the data used and the methods applied to create the visualization, including all the variable encodings, how data was processed, etc. Improvements describes the potential improvements and caveats to the visualization. Based on the code, provide the description. Note: The metadata should contain the title (from the known metadata) and exactly 4 sections. Exclude edge cases. Do not mention "the data" or "the code". Focus only on data vis concerns. Use markdown. Mention the source. Radar chart is a chart that can be a helpful way to compare different observations with multiple numerical variables, with each variable represented by an axis starting at a common origin. The visual result is a sequence of points, connected in a star-shaped polygons, which can be used to spot which values are higher or lower for a given observation and to compare observations side by side. This looks like it may be a D3 block with a custom legend, hover, and axes that rotate for cross filtering. It takes data from https://raw.githubusercontent.com/CS-1940-Fall-2024/hw3-starter-data/main/universities.csv The chart is a “Radar Chart”. It is a cross-filter and has three key marks: line, text, circle. Circle is used for the data points on the radar chart. There is one line per university that represents how it compares across four variables. Each university has its own color. Text labels encode the university names. Using this metadata, describe the complete example in 1-2 sentences. Be specific and include the chart type, the author, the data dimensions used, the visual channels, and what is visually encoded. Avoid saying 'data' too much. Do not use any markdown. Consider whether the author is a person or an organization and write accordingly. Provide only the description, no YAML frontmatter. Description:[Radar Chart] This example, created by kratikashetty using D3 v7, visualizes multi-dimensional attributes of universities with a radar chart. The chart plots four axes—Total Enrollment, Endowment/Student, Annual R&D Expenditure, and Undergraduate Selectivity—to compare thirteen U.S. universities. Each axis uses a linear scale with different domains, and tick labels are formatted contextually (e.g., dollar amounts in K or M, percentages). Each university is represented by a colored radar shape, with colors assigned by a categorical color scheme via the mediaColors mapping. Axes are arranged vertically and horizontally with labeled scales, and a legend identifies each university. The chart is generated as a reusable module with a data-driven structure that supports updates via a render function, and hover interactions are available for detailed exploration. The visualization follows a deliberate approach to displaying multivariate data by mapping four attributes onto orthogonal axes. A notable design choice is placing axis labels at the four cardinal points (top, bottom, left, right) with the vertical and horizontal axes crossing at the center. However, there is a potential issue: the axes do not follow the conventional radar/spider chart layout—they are arranged as two perpendicular two-axis systems rather than radiating from a central point. This creates a "crossed" Cartesian coordinate system rather than a true polar radar chart. Would you like me to describe it as designed, or note this as a known limitation?# Radar Chart A radar-style visualization comparing universities across four key metrics: total enrollment, endowment per student, annual R&D expenditure, and undergraduate selectivity. Thirteen institutions are plotted with distinct colors, each encoded as a closed polygonal shape generated by their metric values. ## Context This chart is designed to help users compare universities across four quantitative dimensions simultaneously. It uses a "radar" or "spider" layout, though it employs a cross-shaped axis arrangement rather than the traditional radial spider web. ## Visualization Design The chart displays four axes arranged in a cross pattern: Total Enrollment (top, linear scale 0–30,000), Endowment/Student (bottom, linear scale 0–$1,012,000), Annual R&D Expenditure (right, linear scale 0–$350M), and Undergraduate Selectivity (left, linear scale −1 to 0, formatted as percentages). Axes are positioned so that enrollment runs vertically upward from the chart center, endowment/student runs downward, R&D extends to the left, and selectivity to the right. Each university is encoded as a colored polygon connecting its four metric values. A categorical color scheme differentiates the 13 universities, with legend and hover interactions to support comparison across institutions. The chart uses D3's modular structure with custom axis, legend, and hover modules. This example is part of the visualization gallery. The chart is interactive; hovering over the visualization reveals additional details. Note: This is a custom visualization and may use special characters (C, S, D, A) to represent axes. The labels on each axis are: C: Total Enrollment S: Endowment/Student D: Annual R&D Exepdniture A: Undergratuate Selectivity The axes and labels in the chart are on a plus-shape layout, where C is at the top, S at the bottom, D on the left, and A on the right. Each axis is scaled independently to its own domain. One potential issue is that the undergraduate selectivity axis is inverted: -1 is at the center and 0 at the outer edge, meaning lower acceptance rates (more selective) are further from the center. The data displayed are university-level metrics, grouped by university and colored by university using the colors defined in the mediaColors constant. The chart uses horizontal and vertical axes in a cross layout. Hovering over a shape may show additional details. # Radar Chart This radar chart compares four key university metrics across multiple institutions: Total Enrollment, Endowment/Student, Annual R&D Expenditure, and Undergraduate Selectivity. Each axis originates from the center of the chart, with data points connected to form a polygon that reveals each university's institutional profile at a glance. ## Design The visualization uses a cross-shaped radar layout with four linearly scaled axes: - **C (vertical, top)**: Total Enrollment (0–30,000) - **S (vertical, bottom)**: Endowment/Student (0–$1,012,000) - **D (horizontal, left)**: Annual R&D Expenditure (0–$350M) - **A (horizontal, right)**: Undergraduate Selectivity (-1 to 0, displayed as percentages) Each university is represented by a semi-transparent polygonal shape, colored according to its institution via a custom categorical color scale. The chart uses D3's modular join pattern and includes interactive hover states and a legend. ## Details - **Data:** University attributes across four metrics: total enrollment, annual R&D expenditure, undergraduate selectivity, and endowment per student - **Encoding:** Four axes arranged cross-shaped; each university forms a four-sided polygon; axis scales are independent, normalized to the full width/height of the chart - **Color:** Categorical scheme mapping 13 universities to distinct colors, e.g., Carnegie Mellon University: #653da7 - **Interactions:** Hover over a shape to highlight it and dim the others - **Design Approach:** Minimalist dark theme (likely) with gridlines, subtle axes, and a legend **D3 features:** - `d3.scaleLinear` for each of the four quantitative axes - `d3.line()` for radar shapes - SVG paths, axes, and legends with data-joins - d3.scale-chromatic for color scheme **Learning Questions:** - How to create multi-axis radar charts using D3 scales - How to structure axes and labels for a radar chart - How to create and update multiple series (paths) with data joins **Key Implementation Details:** - The radar chart uses four axes arranged as a cross: Total Enrollment (top), Endowment/Student (bottom), Annual R&D Expenditure (left), and Undergraduate Selectivity (right). - Scales are shared between axes in the opposite direction; for example, `C` (Total Enrollment) and `S` (Endowment/Student) share the same x-position, but have different y-axis directions. - The axes are created by calling `createRadarChartAxesAndLabels` once with the scale configuration. This helper sets up axis ticks and labels for the four axes with appropriate formatting (e.g., dollar amounts for expenditures, percentages for selectivity). - Individual universities are colored according to the mediaColors mapping. - The chart does not include an interactive legend. Tooltips are enabled via the `createHovers` function (imported from './radarHover'). Data format: Data comes from a Google Sheets as JSON, with values matching media: A = Admission rate (0-1, lower is more selective) C = Total Enrollment D = Annual R&D Expenditure S = Endowment per Student Excerpt of data: media, C, S, D, A Carnegie Mellon University, 5855, 191000, 307000000, 0.173 Clarkson University, 4060, 16000, 50000000, 0.64 ... */ What is the relationship between axes in this visualization? Choose the best answer from the options below: A) The axes are arranged in a circle around a central point, and each axis represents a different variable. B) The axes are positioned so that axes with similar meaning are placed next to each other. C) The axes are positioned at right angles to each other, two horizontal and two vertical, forming a cross. D) The axes are arranged around the perimeter, with tick labels along the outside. E) The axes are parallel and do not intersect. Hint: the metadata says "Radar Chart" but the visualization is drawn with four axes. Use the code to find the best answer. Specifically, how are axes positioned? The code has createRadarChartAxesAndLabels which includes axes for C, S, D, A.C) The axes are positioned at right angles to each other, forming a cross. The chart uses two vertical axes (Total Enrollment on the left, Endowment/Student on the right) and two horizontal axes (Undergraduate Selectivity on the bottom, Annual R&D Expenditure on the top), all sharing a common center point in a classic radar/spider chart layout.
AI-generated descriptionRadar Chart