Skip to content

Commit 76b509c

Browse files
convert graph into d3
1 parent 9003cf6 commit 76b509c

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

styles/styles.css

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -684,15 +684,14 @@ img {
684684
margin: 0;
685685
width: 100%;
686686
}
687-
.graph-img {
687+
#d3-graph {
688688
width: 100%;
689+
display: flex;
690+
justify-content: center;
691+
}
692+
#d3-graph svg {
693+
max-width: 100%;
689694
height: auto;
690-
max-height: 480px;
691-
object-fit: contain;
692-
display: block;
693-
border-radius: 8px;
694-
box-shadow: 0 6px 18px rgba(0,0,0,0.08);
695-
background: #fff;
696695
}
697696
.graph-figure figcaption {
698697
margin-top: 8px;

0 commit comments

Comments
 (0)