Skip to content

Instantly share code, notes, and snippets.

@karen-izuka
Last active January 21, 2020 22:37
Show Gist options
  • Save karen-izuka/cc2015b0354e8b06daed5a9e8aada980 to your computer and use it in GitHub Desktop.
Save karen-izuka/cc2015b0354e8b06daed5a9e8aada980 to your computer and use it in GitHub Desktop.
Apartment Hunting
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Apartments</title>
<link rel="stylesheet" href="style.css">
<link href="https://fonts.googleapis.com/css?family=Open+Sans|Raleway|Sofia&display=swap" rel="stylesheet"><body>
<h1>Apartment Comparison Chart</h1>
<div id="wrapper">
<div id="chart"></div>
<div id="control">
<br>
<br>
<br>
<select multiple size=6 id="selector">
<optgroup label="Choose Apartments">
<option selected value="Crescent Village">Crescent Village</option>
<option value="North Park">North Park</option>
<option value="Santana Heights">Santana Heights</option>
<option value="Vista">Vista</option>
<option value="Verdant">Verdant</option>
<option value="Estancia">Estancia</option>
</optgroup>
</select>
<p id="note">*to select multiple items type control+c (command+c on mac)*</p>
<button id="submit" onclick="refresh()">Submit</button>
<br>
<br>
<br>
<p id="fancy">Color Legend</p>
<div id="legend"></div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.4.1.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="index.js"></script>
</body>
</html>
//input data
const data = [
{name: 'Crescent Village', axis: 'price', value: 0.80},
{name: 'Crescent Village', axis: 'size', value: 0.32},
{name: 'Crescent Village', axis: 'commute', value: 0.63},
{name: 'Crescent Village', axis: 'location', value: 0.60},
{name: 'Crescent Village', axis: 'coffee', value: 0.73},
{name: 'Crescent Village', axis: 'amenities', value: 0.70},
{name: 'North Park', axis: 'price', value: 0.79},
{name: 'North Park', axis: 'size', value: 0.20},
{name: 'North Park', axis: 'commute', value: 0.71},
{name: 'North Park', axis: 'location', value: 0.60},
{name: 'North Park', axis: 'coffee', value: 0.73},
{name: 'North Park', axis: 'amenities', value: 0.30},
{name: 'Santana Heights', axis: 'price', value: 0.20},
{name: 'Santana Heights', axis: 'size', value: 0.80},
{name: 'Santana Heights', axis: 'commute', value: 0.20},
{name: 'Santana Heights', axis: 'location', value: 0.90},
{name: 'Santana Heights', axis: 'coffee', value: 0.80},
{name: 'Santana Heights', axis: 'amenities', value: 0.40},
{name: 'Vista', axis: 'price', value: 0.49},
{name: 'Vista', axis: 'size', value: 0.62},
{name: 'Vista', axis: 'commute', value: 0.80},
{name: 'Vista', axis: 'location', value: 0.70},
{name: 'Vista', axis: 'coffee', value: 0.50},
{name: 'Vista', axis: 'amenities', value: 0.80},
{name: 'Verdant', axis: 'price', value: 0.65},
{name: 'Verdant', axis: 'size', value: 0.49},
{name: 'Verdant', axis: 'commute', value: 0.80},
{name: 'Verdant', axis: 'location', value: 0.20},
{name: 'Verdant', axis: 'coffee', value: 0.35},
{name: 'Verdant', axis: 'amenities', value: 0.50},
{name: 'Estancia', axis: 'price', value: 0.52},
{name: 'Estancia', axis: 'size', value: 0.74},
{name: 'Estancia', axis: 'commute', value: 0.54},
{name: 'Estancia', axis: 'location', value: 0.80},
{name: 'Estancia', axis: 'coffee', value: 0.20},
{name: 'Estancia', axis: 'amenities', value: 0.20}
] //data
//select color scheme
const color = d3.scaleOrdinal()
.domain(data.map(d => d.name))
.range(['#D99CA5','#EDCB9C','#EFEFA8','#C2E0AE','#8FC2DF','#A19AD1']);
//set chart configurations
const config = {
width: 450,
height: 450,
margin: {top: 100, right: 100, bottom: 100, left: 100},
maxValue: 1,
levels: 5,
labelFactor: 1.2,
wrapWidth: 75,
opacityArea: 0.25,
dotRadius: 3,
opacityCircles: 0.1,
strokeWidth: 2,
color: color
}
//text wrap helper function - forked from mike bostock
const wrap = (text, width) => {
text.each(function() {
let text = d3.select(this);
let words = text.text().split(/\s+/).reverse();
let word;
let line = [];
let lineNumber = 0;
let lineHeight = 1.4;
let x = text.attr('x');
let y = text.attr('y');
let dy = parseFloat(text.attr('dy'));
let tspan = text.text(null).append('tspan').attr('x', x).attr('y', y).attr('dy', `${dy}em`);
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(' '));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(' '));
line = [word];
tspan = text.append('tspan')
.attr('x', x)
.attr('y', y)
.attr('dy', `${++lineNumber * lineHeight + dy}em`)
.text(word)
}
}
});
} //wrap
//create legend
const legend = () => {
const svg = d3.select('#legend')
.append('svg')
.attr('width', 150)
.attr('height', 150);
const g = svg.selectAll('.groups')
.data(color.domain())
.join('g')
.attr('class', 'groups');
g.append('circle')
.attr('cx', 5)
.attr('cy', (d,i) => i * 20 + 5)
.attr('r', 5)
.attr('fill', d => color(d));
g.append('text')
.text(d => d)
.attr('x', 15)
.attr('y', (d,i) => i * 20 + 5)
.attr('dy', '0.35em');
}
legend();
//main function - forked by nadieh bremer
const radarChart = (id, data, nestedData, config) => {
const maxValue = Math.max(config.maxValue, d3.max(data, d => d.value));
const allAxis = Array.from(new Set(data.map(d => d.axis)));
const total = allAxis.length;
const radius = Math.min(config.width/2, config.height/2);
const format = d3.format('.0%');
const angleSlice = Math.PI * 2 / total;
const rScale = d3.scaleLinear().domain([0, maxValue]).range([0, radius]);
//setup
d3.select(id).select('svg').remove();
const svg = d3.select(id)
.append('svg')
.attr('width', config.width + config.margin.left + config.margin.right)
.attr('height', config.height + config.margin.top + config.margin.bottom)
.attr('class', 'radar');
const g = svg.append('g')
.attr('transform', `translate(${config.width/2 + config.margin.left}, ${config.height/2 + config.margin.top})`);
const filter = g.append('defs')
.append('filter')
.attr('id', 'glow');
const feGaussianBlur = filter.append('feGaussianBlur')
.attr('stdDeviation', 2.5)
.attr('result', 'coloredBlur');
const feMerge = filter.append('feMerge');
const feMergeNodeA = feMerge.append('feMergeNode')
.attr('in', 'coloredBlur');
const feMergeNodeB = feMerge.append('feMergeNode')
.attr('in', 'SourceGraphic');
//draw the axis circles
const axisGrid = g.append('g')
.attr('class', 'axisWrapper');
axisGrid.selectAll('.levels')
.data(d3.range(1, (config.levels+1)).reverse())
.join('circle')
.attr('class', 'gridCircle')
.attr('r', d => radius/config.levels*d)
.attr('fill', '#CDCDCD')
.attr('stroke', '#CDCDCD')
.attr('stroke-width', '1.5px')
.attr('fill-opacity', config.opacityCircles)
.attr('filter', 'url(#glow)');
axisGrid.selectAll('.axisLabel')
.data(d3.range(1, (config.levels+1)).reverse())
.join('text')
.attr('class', 'axisLabel')
.attr('x', 4)
.attr('y', d => -d*radius/config.levels)
.attr('dy', '0.35em')
.attr('fill', '#737373')
.text(d => format(maxValue * d/config.levels));
//draw the lines
const axis = axisGrid.selectAll('.axis')
.data(allAxis)
.join('g')
.attr('class', 'axis');
axis.append('line')
.attr('x1', 0)
.attr('y1', 0)
.attr('x2', (d,i) => rScale(maxValue*1.1) * Math.cos(angleSlice*i - Math.PI/2))
.attr('y2', (d,i) => rScale(maxValue*1.1) * Math.sin(angleSlice*i - Math.PI/2))
.attr('class', 'line')
.attr('stroke', 'white')
.attr('stroke-width', '2px');
axis.append('text')
.attr('class', 'legend')
.attr('text-anchor', 'middle')
.attr('x', (d,i) => rScale(maxValue * config.labelFactor) * Math.cos(angleSlice*i - Math.PI/2))
.attr('y', (d,i) => rScale(maxValue * config.labelFactor) * Math.sin(angleSlice*i - Math.PI/2))
.attr('dy', '0.35em')
.text(d => d)
.call(wrap, config.wrapWidth);
//draw the blobs
const radarLine = d3.lineRadial()
.curve(d3.curveCardinalClosed)
.radius(d => rScale(d.value))
.angle((d,i) => i*angleSlice);
const blobWrapper = g.selectAll('.radarWrapper')
.data(nestedData)
.join('g')
.attr('class', 'radarWrapper');
blobWrapper.append('path')
.attr('class', 'radarArea')
.attr('d', d => radarLine(d.values))
.attr('fill', d => config.color(d.key))
.attr('fill-opacity', config.opacityArea)
.attr('stroke', d => config.color(d.key))
.attr('stroke-width', `${config.strokeWidth}px`)
.attr('filter', 'url(#glow)');
}
//refresh data
const refresh = () => {
let values = $('#selector').val();
let filteredData = data.filter(d => values.indexOf(d.name) !== -1);
let filteredNest = d3.nest()
.key(d => d.name)
.entries(filteredData);
radarChart('#chart', filteredData, filteredNest, config);
}
//onload
radarChart('#chart', data.filter(d => d.name === 'Crescent Village'), d3.nest().key(d => d.name).entries(data.filter(d => d.name === 'Crescent Village')), config);
h1 {
font-family: 'Sofia', cursive;
font-size: 48px;
text-align: center;
}
optgroup {
font-family: 'Sofia', cursive;
font-size: 14px;
}
option {
font-family: 'Open Sans', sans-serif;
font-size: 12px;
}
p {
font-family: 'Open Sans', sans-serif;
font-size: 14px;
font-weight: bold;
}
svg {
display: block;
margin: auto;
}
.axisLabel {
font-family: 'Open Sans', sans-serif;
font-size: 12px;
}
.groups {
font-family: 'Open Sans', sans-serif;
font-size: 12px;
}
.legend {
font-family: 'Sofia', cursive;
font-size: 16px;
}
#chart {
width: 1000px;
float: left;
}
#control {
width: 150px;
float: left;
}
#fancy {
font-family: 'Sofia', cursive;
font-size: 16px;
}
#note {
font-family: 'Open Sans', sans-serif;
font-size: 10px;
}
#selector {
border: #ffffff;
}
#wrapper {
width: 1250px;
overflow: hidden;
margin: auto;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment