Skip to content

Commit b6d5273

Browse files
committed
changes
1 parent a000afd commit b6d5273

File tree

1 file changed

+221
-42
lines changed

1 file changed

+221
-42
lines changed

index.html

Lines changed: 221 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -306,11 +306,11 @@
306306

307307
React.useEffect(() => {
308308
const container = containerRef.current;
309-
if (!container || !chartData.length) return;
309+
if (!container || !amortizationSchedule.length) return;
310310

311311
const width = container.offsetWidth;
312312
const height = container.offsetHeight;
313-
const margin = { top: 50, right: 30, bottom: 80, left: 60 }; // Increased bottom margin for rotated labels
313+
const margin = { top: 30, right: 120, bottom: 120, left: 80 };
314314
const innerWidth = width - margin.left - margin.right;
315315
const innerHeight = height - margin.top - margin.bottom;
316316

@@ -323,81 +323,260 @@
323323
.append("g")
324324
.attr("transform", `translate(${margin.left},${margin.top})`);
325325

326+
// Format numbers to be more concise (e.g., $120k instead of $120,000)
327+
const formatCurrency = (value) => {
328+
if (Math.abs(value) >= 1000000) {
329+
return `$${(value / 1000000).toFixed(1)}M`;
330+
} else if (Math.abs(value) >= 1000) {
331+
return `$${(value / 1000).toFixed(0)}k`;
332+
} else {
333+
return `$${value.toFixed(0)}`;
334+
}
335+
};
336+
337+
// Create scales
326338
const x = d3
327339
.scaleBand()
328-
.domain(chartData.map((_, index) => `Year ${index + 1}`))
340+
.domain(amortizationSchedule.map((d) => d.year))
329341
.range([0, innerWidth])
330-
.padding(0.1);
342+
.padding(0.2);
331343

344+
// Left y-axis scale for stacked bars (principal and interest paid)
332345
const y = d3
333346
.scaleLinear()
334-
.domain([0, d3.max(chartData, (d) => d.interest + d.principal)])
347+
.domain([
348+
0,
349+
d3.max(
350+
amortizationSchedule,
351+
(d) => d.principalPaid + d.interestPaid
352+
) * 1.05,
353+
])
335354
.range([innerHeight, 0]);
336355

337-
const xAxis = d3.axisBottom(x);
338-
const yAxis = d3.axisLeft(y).tickFormat(d3.format(".2s"));
356+
// Right y-axis scale for remaining principal line
357+
const y2 = d3
358+
.scaleLinear()
359+
.domain([
360+
0,
361+
d3.max(amortizationSchedule, (d) => d.endOfYearPrincipal) *
362+
1.05,
363+
])
364+
.range([innerHeight, 0]);
339365

366+
// Create axes
367+
const xAxis = d3
368+
.axisBottom(x)
369+
.tickValues(x.domain().filter((d, i) => i % 5 === 0)); // Show every 5th year
370+
371+
const yAxis = d3
372+
.axisLeft(y)
373+
.tickFormat(formatCurrency)
374+
.ticks(
375+
Math.min(
376+
10,
377+
Math.floor(
378+
d3.max(
379+
amortizationSchedule,
380+
(d) => d.principalPaid + d.interestPaid
381+
) / 10000
382+
)
383+
)
384+
);
385+
386+
const y2Axis = d3.axisRight(y2).tickFormat(formatCurrency);
387+
388+
// Add axes to chart
340389
chartSvg
341390
.append("g")
342391
.attr("transform", `translate(0,${innerHeight})`)
343392
.call(xAxis)
344393
.selectAll("text")
345-
.style("font-size", "0.8rem")
346-
.style("text-anchor", "end") // Rotate labels
347-
.attr("dx", "-0.8em")
348-
.attr("dy", "0.15em")
349-
.attr("transform", "rotate(-65)");
394+
.style("font-size", "0.8rem");
350395

351396
chartSvg
352397
.append("g")
353398
.call(yAxis)
354399
.selectAll("text")
355400
.style("font-size", "0.8rem");
356401

357-
const stack = d3.stack().keys(["interest", "principal"])(chartData);
402+
chartSvg
403+
.append("g")
404+
.attr("transform", `translate(${innerWidth},0)`)
405+
.call(y2Axis)
406+
.selectAll("text")
407+
.style("font-size", "0.8rem");
408+
409+
// Define colors for the stacked bars
410+
const colors = {
411+
principal: "#38a169", // Green
412+
interest: "#e53e3e", // Red
413+
remainingPrincipal: "#000000", // Black instead of blue
414+
};
415+
416+
// Create tooltip div
417+
const tooltip = d3
418+
.select("body")
419+
.append("div")
420+
.attr("class", "tooltip")
421+
.style("opacity", 0)
422+
.style("position", "absolute")
423+
.style("background", "white")
424+
.style("border", "1px solid #ddd")
425+
.style("border-radius", "4px")
426+
.style("padding", "8px")
427+
.style("pointer-events", "none")
428+
.style("z-index", 1000);
429+
430+
// Add hover functionality to bars
431+
amortizationSchedule.forEach((d) => {
432+
// Group for both bars
433+
const barGroup = chartSvg.append("g");
434+
435+
// Interest bar (bottom of stack)
436+
barGroup
437+
.append("rect")
438+
.attr("x", x(d.year))
439+
.attr("y", y(d.interestPaid))
440+
.attr("width", x.bandwidth())
441+
.attr("height", innerHeight - y(d.interestPaid))
442+
.attr("fill", colors.interest)
443+
.on("mouseover", function (event) {
444+
tooltip.transition().duration(200).style("opacity", 0.9);
445+
tooltip
446+
.html(
447+
`
448+
<strong>Year ${d.year}</strong><br/>
449+
Interest: ${formatCurrency(d.interestPaid)}<br/>
450+
Principal: ${formatCurrency(d.principalPaid)}<br/>
451+
Remaining: ${formatCurrency(d.endOfYearPrincipal)}
452+
`
453+
)
454+
.style("left", event.clientX + 10 + "px")
455+
.style("top", event.clientY - 28 + "px");
456+
})
457+
.on("mouseout", function () {
458+
tooltip.transition().duration(500).style("opacity", 0);
459+
});
460+
461+
// Principal bar (top of stack)
462+
barGroup
463+
.append("rect")
464+
.attr("x", x(d.year))
465+
.attr("y", y(d.principalPaid + d.interestPaid))
466+
.attr("width", x.bandwidth())
467+
.attr(
468+
"height",
469+
y(d.interestPaid) - y(d.principalPaid + d.interestPaid)
470+
)
471+
.attr("fill", colors.principal)
472+
.on("mouseover", function (event) {
473+
tooltip.transition().duration(200).style("opacity", 0.9);
474+
tooltip
475+
.html(
476+
`
477+
<strong>Year ${d.year}</strong><br/>
478+
Interest: ${formatCurrency(d.interestPaid)}<br/>
479+
Principal: ${formatCurrency(d.principalPaid)}<br/>
480+
Remaining: ${formatCurrency(d.endOfYearPrincipal)}
481+
`
482+
)
483+
.style("left", event.clientX + 10 + "px")
484+
.style("top", event.clientY - 28 + "px");
485+
})
486+
.on("mouseout", function () {
487+
tooltip.transition().duration(500).style("opacity", 0);
488+
});
489+
});
358490

359-
const colorScale = d3
360-
.scaleOrdinal()
361-
.domain(["interest", "principal"])
362-
.range(["#e53e3e", "#38a169"]);
491+
// Add legend below x-axis
492+
const legendWidth = 300;
493+
const legendHeight = 30;
363494

364-
chartSvg
365-
.selectAll(".bar")
366-
.data(stack)
367-
.enter()
495+
const legend = chartSvg
368496
.append("g")
369-
.attr("fill", (d) => colorScale(d.key))
370-
.selectAll("rect")
371-
.data((d) => d)
372-
.enter()
373-
.append("rect")
374-
.attr("x", (d, i) => x(`Year ${i + 1}`))
375-
.attr("y", (d) => y(d[1]))
376-
.attr("height", (d) => y(d[0]) - y(d[1]))
377-
.attr("width", x.bandwidth());
378-
379-
// Add title to the chart
497+
.attr(
498+
"transform",
499+
`translate(${(innerWidth - legendWidth) / 2}, ${
500+
innerHeight + 70
501+
})`
502+
);
503+
504+
const legendItems = [
505+
{ key: "Principal", color: colors.principal },
506+
{ key: "Interest", color: colors.interest },
507+
{ key: "Remaining Principal", color: colors.remainingPrincipal },
508+
];
509+
510+
const legendSpacing = legendWidth / legendItems.length;
511+
512+
legendItems.forEach((item, i) => {
513+
const legendItem = legend
514+
.append("g")
515+
.attr("transform", `translate(${i * legendSpacing}, 0)`);
516+
517+
legendItem
518+
.append("rect")
519+
.attr("width", 12)
520+
.attr("height", 12)
521+
.attr("fill", item.color);
522+
523+
legendItem
524+
.append("text")
525+
.attr("x", 18)
526+
.attr("y", 10)
527+
.attr("text-anchor", "start")
528+
.style("font-size", "0.8rem")
529+
.text(item.key);
530+
});
531+
532+
// Add left y-axis label
380533
chartSvg
381534
.append("text")
382-
.attr("x", innerWidth / 2)
383-
.attr("y", -30)
384-
.attr("text-anchor", "middle")
385-
.style("font-size", "1rem")
386-
.style("font-weight", "bold")
387-
.text("Amortization Chart");
535+
.attr("transform", "rotate(-90)")
536+
.attr("y", -60)
537+
.attr("x", -innerHeight / 2)
538+
.attr("dy", "1em")
539+
.style("text-anchor", "middle")
540+
.style("font-size", "0.9rem")
541+
.text("Annual Payment Amount");
388542

389-
// Add y-axis label
543+
// Add right y-axis label
390544
chartSvg
391545
.append("text")
392546
.attr("transform", "rotate(-90)")
393-
.attr("y", -50)
547+
.attr("y", innerWidth + 60)
394548
.attr("x", -innerHeight / 2)
395549
.attr("dy", "1em")
396550
.style("text-anchor", "middle")
397551
.style("font-size", "0.9rem")
398-
.text("Amount ($)");
552+
.text("Remaining Principal");
553+
554+
// Add x-axis label
555+
chartSvg
556+
.append("text")
557+
.attr("x", innerWidth / 2)
558+
.attr("y", innerHeight + 50)
559+
.attr("text-anchor", "middle")
560+
.style("font-size", "0.9rem")
561+
.text("Year");
562+
563+
// Create line for remaining principal - moved to be drawn last so it appears on top
564+
const remainingPrincipalLine = d3
565+
.line()
566+
.x((d) => x(d.year) + x.bandwidth() / 2)
567+
.y((d) => y2(d.endOfYearPrincipal))
568+
.curve(d3.curveMonotoneX);
569+
570+
// Add remaining principal line
571+
chartSvg
572+
.append("path")
573+
.datum(amortizationSchedule)
574+
.attr("fill", "none")
575+
.attr("stroke", colors.remainingPrincipal)
576+
.attr("stroke-width", 3)
577+
.attr("d", remainingPrincipalLine);
399578
}, [
400-
chartData,
579+
amortizationSchedule,
401580
monthlyPayment,
402581
purchasePrice,
403582
downPayment,

0 commit comments

Comments
 (0)