Skip to content

Commit 77b2549

Browse files
authored
Merge pull request #80 from code4policy/graph
convert graph into d3
2 parents 36642c9 + 76b509c commit 77b2549

2 files changed

Lines changed: 201 additions & 8 deletions

File tree

index.html

Lines changed: 195 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
<title>Demo Website</title>
55
<!-- linking css file-->
66
<link href="styles/styles.css" rel="stylesheet" />
7+
<!-- D3.js library -->
8+
<script src="https://d3js.org/d3.v7.min.js"></script>
79

810
</head>
911
<body>
@@ -135,7 +137,7 @@ <h3>
135137

136138
<div class="graph-container">
137139
<figure class="graph-figure">
138-
<img class="graph-img" src="data/payt_panels.png" alt="Pre-post effect of PAYT">
140+
<div id="d3-graph"></div>
139141
<figcaption>Data Sourced: Massachusetts Department of Environmental Protection's 2024 Municipal Solid Waste and Recycling Survey, Massachusetts Department of Revenue, & Massachusetts Municipal PAYT Programs June 2025 </figcaption>
140142
</figure>
141143
</div>
@@ -232,6 +234,198 @@ <h3>
232234
</div>
233235
<script>
234236
document.addEventListener('DOMContentLoaded', function () {
237+
// D3 Interactive Graph - Pre-Post PAYT Effect
238+
const graphData = [
239+
// Chicopee (PAYT 2017, years_payt: -4 to 5)
240+
{ municipality: 'Chicopee', year: -4, tonnage: 0.7393617, type: 'pre' },
241+
{ municipality: 'Chicopee', year: -3, tonnage: 0.7267905, type: 'pre' },
242+
{ municipality: 'Chicopee', year: -2, tonnage: 0.7201172, type: 'pre' },
243+
{ municipality: 'Chicopee', year: -1, tonnage: 0.7240520, type: 'pre' },
244+
{ municipality: 'Chicopee', year: 0, tonnage: 0.5886695, type: 'post' },
245+
{ municipality: 'Chicopee', year: 1, tonnage: 0.6204813, type: 'post' },
246+
{ municipality: 'Chicopee', year: 2, tonnage: 0.5576872, type: 'post' },
247+
{ municipality: 'Chicopee', year: 3, tonnage: 0.6272738, type: 'post' },
248+
{ municipality: 'Chicopee', year: 4, tonnage: 0.6468433, type: 'post' },
249+
{ municipality: 'Chicopee', year: 5, tonnage: 0.6151788, type: 'post' },
250+
// Harvard (PAYT 2019, years_payt: -5 to 5)
251+
{ municipality: 'Harvard', year: -5, tonnage: 0.8311209, type: 'pre' },
252+
{ municipality: 'Harvard', year: -4, tonnage: 0.8260870, type: 'pre' },
253+
{ municipality: 'Harvard', year: -3, tonnage: 0.9753580, type: 'pre' },
254+
{ municipality: 'Harvard', year: -2, tonnage: 0.8177323, type: 'pre' },
255+
{ municipality: 'Harvard', year: -1, tonnage: 0.8061971, type: 'pre' },
256+
{ municipality: 'Harvard', year: 0, tonnage: 0.6302390, type: 'post' },
257+
{ municipality: 'Harvard', year: 1, tonnage: 0.5599200, type: 'post' },
258+
{ municipality: 'Harvard', year: 2, tonnage: 0.5020960, type: 'post' },
259+
{ municipality: 'Harvard', year: 3, tonnage: 0.4425517, type: 'post' },
260+
{ municipality: 'Harvard', year: 4, tonnage: 0.4455224, type: 'post' },
261+
{ municipality: 'Harvard', year: 5, tonnage: 0.4975635, type: 'post' },
262+
// Heath (PAYT 2017, years_payt: -5 to 5)
263+
{ municipality: 'Heath', year: -5, tonnage: 0.9145299, type: 'pre' },
264+
{ municipality: 'Heath', year: -4, tonnage: 0.7803922, type: 'pre' },
265+
{ municipality: 'Heath', year: -3, tonnage: 0.7142857, type: 'pre' },
266+
{ municipality: 'Heath', year: -2, tonnage: 0.7811877, type: 'pre' },
267+
{ municipality: 'Heath', year: -1, tonnage: 0.6587726, type: 'pre' },
268+
{ municipality: 'Heath', year: 0, tonnage: 0.5249527, type: 'post' },
269+
{ municipality: 'Heath', year: 1, tonnage: 0.3610714, type: 'post' },
270+
{ municipality: 'Heath', year: 2, tonnage: 0.3556857, type: 'post' },
271+
{ municipality: 'Heath', year: 3, tonnage: 0.4209174, type: 'post' },
272+
{ municipality: 'Heath', year: 4, tonnage: 0.4009621, type: 'post' },
273+
{ municipality: 'Heath', year: 5, tonnage: 0.3579228, type: 'post' },
274+
// Hinsdale (PAYT 2017, years_payt: -4 to 5)
275+
{ municipality: 'Hinsdale', year: -4, tonnage: 0.8728448, type: 'pre' },
276+
{ municipality: 'Hinsdale', year: -3, tonnage: 0.9290323, type: 'pre' },
277+
{ municipality: 'Hinsdale', year: -2, tonnage: 0.9652903, type: 'pre' },
278+
{ municipality: 'Hinsdale', year: -1, tonnage: 0.9324731, type: 'pre' },
279+
{ municipality: 'Hinsdale', year: 0, tonnage: 0.6558077, type: 'post' },
280+
{ municipality: 'Hinsdale', year: 1, tonnage: 0.4879636, type: 'post' },
281+
{ municipality: 'Hinsdale', year: 2, tonnage: 0.4927273, type: 'post' },
282+
{ municipality: 'Hinsdale', year: 3, tonnage: 0.5397254, type: 'post' },
283+
{ municipality: 'Hinsdale', year: 4, tonnage: 0.5259774, type: 'post' },
284+
{ municipality: 'Hinsdale', year: 5, tonnage: 0.4343286, type: 'post' },
285+
// Peru (PAYT 2017, years_payt: -5 to 4)
286+
{ municipality: 'Peru', year: -5, tonnage: 0.6625000, type: 'pre' },
287+
{ municipality: 'Peru', year: -4, tonnage: 0.6781250, type: 'pre' },
288+
{ municipality: 'Peru', year: -3, tonnage: 0.6375000, type: 'pre' },
289+
{ municipality: 'Peru', year: -2, tonnage: 0.6742813, type: 'pre' },
290+
{ municipality: 'Peru', year: -1, tonnage: 0.5378307, type: 'pre' },
291+
{ municipality: 'Peru', year: 0, tonnage: 0.7028621, type: 'post' },
292+
{ municipality: 'Peru', year: 1, tonnage: 0.6156156, type: 'post' },
293+
{ municipality: 'Peru', year: 2, tonnage: 0.5544776, type: 'post' },
294+
{ municipality: 'Peru', year: 3, tonnage: 0.5881493, type: 'post' },
295+
{ municipality: 'Peru', year: 4, tonnage: 0.5265373, type: 'post' }
296+
];
297+
298+
const margin = { top: 20, right: 30, bottom: 30, left: 60 };
299+
const width = 900 - margin.left - margin.right;
300+
const height = 500 - margin.top - margin.bottom;
301+
302+
const svg = d3.select('#d3-graph')
303+
.append('svg')
304+
.attr('width', width + margin.left + margin.right)
305+
.attr('height', height + margin.top + margin.bottom)
306+
.attr('viewBox', `0 0 ${width + margin.left + margin.right} ${height + margin.top + margin.bottom}`)
307+
.attr('preserveAspectRatio', 'xMidYMid meet')
308+
.style('max-width', '100%')
309+
.style('height', 'auto')
310+
.append('g')
311+
.attr('transform', `translate(${margin.left},${margin.top})`);
312+
313+
// Scales
314+
const xScale = d3.scaleLinear()
315+
.domain([-5, 5])
316+
.range([0, width]);
317+
318+
const yScale = d3.scaleLinear()
319+
.domain([0, 1.5])
320+
.range([height, 0]);
321+
322+
const colorScale = d3.scaleOrdinal()
323+
.domain(['Hinsdale', 'Peru', 'Heath', 'Chicopee', 'Harvard'])
324+
.range(['#091F2F', '#1871BD', '#FB4D42', '#51ACFF', '#45789C']);
325+
326+
// Line generator
327+
const line = d3.line()
328+
.x(d => xScale(d.year))
329+
.y(d => yScale(d.tonnage));
330+
331+
// Group data by municipality
332+
const municipalities = d3.group(graphData, d => d.municipality);
333+
334+
// Draw lines for each municipality
335+
municipalities.forEach((data, municipality) => {
336+
svg.append('path')
337+
.datum(data)
338+
.attr('fill', 'none')
339+
.attr('stroke', colorScale(municipality))
340+
.attr('stroke-width', 2.5)
341+
.attr('d', line)
342+
.attr('class', 'graph-line')
343+
.style('opacity', 0.7)
344+
.on('mouseover', function() {
345+
d3.select(this).style('opacity', 1).attr('stroke-width', 3.5);
346+
})
347+
.on('mouseout', function() {
348+
d3.select(this).style('opacity', 0.7).attr('stroke-width', 2.5);
349+
});
350+
351+
// Draw circles for data points
352+
svg.selectAll(`.dots-${municipality}`)
353+
.data(data)
354+
.enter()
355+
.append('circle')
356+
.attr('cx', d => xScale(d.year))
357+
.attr('cy', d => yScale(d.tonnage))
358+
.attr('r', 4)
359+
.attr('fill', colorScale(municipality))
360+
.attr('class', `dots-${municipality}`)
361+
.on('mouseover', function(event, d) {
362+
d3.select(this).attr('r', 6);
363+
svg.append('text')
364+
.attr('class', 'tooltip')
365+
.attr('x', xScale(d.year))
366+
.attr('y', yScale(d.tonnage) - 10)
367+
.attr('text-anchor', 'middle')
368+
.attr('font-size', '12px')
369+
.attr('fill', '#333')
370+
.text(d.tonnage.toFixed(2) + ' tons');
371+
})
372+
.on('mouseout', function() {
373+
d3.select(this).attr('r', 4);
374+
svg.selectAll('.tooltip').remove();
375+
});
376+
});
377+
378+
// X-axis
379+
svg.append('g')
380+
.attr('transform', `translate(0,${height})`)
381+
.call(d3.axisBottom(xScale).tickValues([-5, -3, 0, 3, 5]).tickFormat(d => {
382+
if (d === 0) return 'Implementation';
383+
if (d < 0) return `${Math.abs(d)}yr Pre`;
384+
return `${d}yr Post`;
385+
}))
386+
.append('text')
387+
.attr('x', width / 2)
388+
.attr('y', 40)
389+
.attr('fill', '#333')
390+
.attr('font-size', '14px')
391+
.attr('text-anchor', 'middle')
392+
.text('Timeline');
393+
394+
// Y-axis
395+
svg.append('g')
396+
.call(d3.axisLeft(yScale))
397+
.append('text')
398+
.attr('transform', 'rotate(-90)')
399+
.attr('y', 0 - margin.left)
400+
.attr('x', 0 - (height / 2))
401+
.attr('dy', '1em')
402+
.attr('fill', '#333')
403+
.attr('font-size', '14px')
404+
.attr('text-anchor', 'middle')
405+
.text('Tons per Household');
406+
407+
// Legend
408+
const legend = svg.selectAll('.legend')
409+
.data(['Hinsdale', 'Peru', 'Heath', 'Chicopee', 'Harvard'])
410+
.enter()
411+
.append('g')
412+
.attr('class', 'legend')
413+
.attr('transform', (d, i) => `translate(0,${-height - 10 - i * 20})`);
414+
415+
legend.append('line')
416+
.attr('x1', 0)
417+
.attr('x2', 20)
418+
.attr('y1', 0)
419+
.attr('y2', 0)
420+
.attr('stroke', d => colorScale(d))
421+
.attr('stroke-width', 2.5);
422+
423+
legend.append('text')
424+
.attr('x', 25)
425+
.attr('y', 4)
426+
.attr('font-size', '12px')
427+
.text(d => d);
428+
235429
document.querySelectorAll('.method-toggle').forEach(function (btn) {
236430
var content = btn.parentElement.querySelector('.method-content');
237431
btn.addEventListener('click', function () {

styles/styles.css

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -676,15 +676,14 @@ img {
676676
margin: 0;
677677
width: 100%;
678678
}
679-
.graph-img {
679+
#d3-graph {
680680
width: 100%;
681+
display: flex;
682+
justify-content: center;
683+
}
684+
#d3-graph svg {
685+
max-width: 100%;
681686
height: auto;
682-
max-height: 480px;
683-
object-fit: contain;
684-
display: block;
685-
border-radius: 8px;
686-
box-shadow: 0 6px 18px rgba(0,0,0,0.08);
687-
background: #fff;
688687
}
689688
.graph-figure figcaption {
690689
margin-top: 8px;

0 commit comments

Comments
 (0)