added pricing chart

This commit is contained in:
Chris Zhu
2025-03-18 13:05:22 -07:00
parent 027cd23a07
commit fc9216307f
3 changed files with 85 additions and 12 deletions
@@ -1,12 +1,12 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["chart"]
static values = {
prices: Object
}
connect() {
console.log(this.pricesValue)
this.computed = {
team: {
'team-size': 1,
@@ -22,17 +22,77 @@ export default class extends Controller {
'worker-instances': 0,
},
}
// Initialize sliders and pricing on page load
this.calculateAll();
}
renderChart(breakdowns) {
const data = breakdowns.map(b => {
const { service, breakdown } = b;
if (breakdown.error) {
return null;
}
const serviceName = this.pricesValue[service].name;
const color = this.pricesValue[service].color;
return {
breakdown,
serviceName,
color,
}
}).filter(b => b !== null);
let chartStatus = Chart.getChart("price-chart");
if (chartStatus != undefined) {
chartStatus.destroy();
}
new Chart(this.chartTarget, {
type: 'bar',
data: {
labels: data.map(b => b.serviceName),
datasets: [{
label: 'Cost',
backgroundColor: data.map(b => b.color),
borderColor: data.map(b => b.color),
borderWidth: 1,
data: data.map(b => this.cost(b.breakdown)),
}]
},
options: {
scales: {
y: {
beginAtZero: true,
ticks: {
callback: function (value) {
return '$' + value;
}
}
}
},
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: function (context) {
return '$' + context.formattedValue;
}
}
}
}
}
});
}
instanceSize(tiers, cpuNeeded, memoryNeeded) {
for (let i = 0; i < tiers.length; i++) {
if (cpuNeeded <= tiers[i].cpu && memoryNeeded <= tiers[i].memory) {
return {instanceNeeded: tiers[i], i}
return { instanceNeeded: tiers[i], i }
}
}
return {instanceNeeded: null, i: -1}
return { instanceNeeded: null, i: -1 }
}
calculateBreakdown(service) {
@@ -42,7 +102,7 @@ export default class extends Controller {
const webCpuNeeded = this.computed.web['web-cpu']
const webMemoryNeeded = this.computed.web['web-memory']
const {instanceNeeded: webInstanceNeeded, i: webInstanceIndex} = this.instanceSize(prices.tiers, webCpuNeeded, webMemoryNeeded)
const { instanceNeeded: webInstanceNeeded, i: webInstanceIndex } = this.instanceSize(prices.tiers, webCpuNeeded, webMemoryNeeded)
const webReplicas = this.computed.web['web-instances']
if (webInstanceNeeded) {
@@ -58,7 +118,7 @@ export default class extends Controller {
const workerCpuNeeded = this.computed.worker['worker-cpu']
const workerMemoryNeeded = this.computed.worker['worker-memory']
const {instanceNeeded: workerInstanceNeeded, i: workerInstanceIndex} = this.instanceSize(prices.tiers, workerCpuNeeded, workerMemoryNeeded)
const { instanceNeeded: workerInstanceNeeded, i: workerInstanceIndex } = this.instanceSize(prices.tiers, workerCpuNeeded, workerMemoryNeeded)
const workerReplicas = this.computed.worker['worker-instances']
if (workerInstanceNeeded) {
@@ -76,10 +136,13 @@ export default class extends Controller {
}
calculateAll() {
const services = ["render", "heroku", "digitalocean", "hetzner"];
services.forEach(service => {
this.place(this.render(service, this.calculateBreakdown(service)), `${service.toLowerCase()}-breakdown`);
const services = ["heroku", "render", "digitalocean", "hetzner"];
const breakdowns = services.map(service => {
const breakdown = this.calculateBreakdown(service);
this.place(this.render(service, breakdown), `${service.toLowerCase()}-breakdown`);
return { breakdown, service }
});
this.renderChart(breakdowns);
}
place(html, id) {
@@ -91,8 +154,10 @@ export default class extends Controller {
container.appendChild(el);
}
cost(breakdown) {
return breakdown.reduce((sum, b) => sum + (typeof b.cost === 'number' ? b.cost : 0), 0);
}
render(service, breakdown) {
console.log(breakdown);
const serviceName = this.pricesValue[service].name
if (breakdown.error) {
return `
@@ -104,7 +169,7 @@ export default class extends Controller {
</div>
`
}
const total = breakdown.reduce((sum, b) => sum + (typeof b.cost === 'number' ? b.cost : 0), 0);
const total = this.cost(breakdown);
const header = `
<div class="flex items-center justify-between mb-2">
<div class="flex items-center">
@@ -140,7 +205,7 @@ export default class extends Controller {
if (!obj[k]) obj[k] = {};
return obj[k];
}, object);
// Set the final value
lastObj[lastKey] = value;
return object;
+5 -1
View File
@@ -1,4 +1,5 @@
<!-- Inspired by https://judoscale.com/tools/paas-pricing-calculator -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js" integrity="sha512-ZwR1/gSZM3ai6vCdI+LVF1zSq/5HznD3ZSTk7kajkaj4D292NLuduDCO1c/NT8Id+jE58KYLKT7hXnbtryGmMg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<div class="bg-slate-900 min-h-screen text-white" data-controller="pricing" data-pricing-prices-value="<%= @prices.to_json %>">
<!-- Header -->
@@ -198,7 +199,10 @@
<div id="digitalocean-breakdown"></div>
<div id="hetzner-breakdown"></div>
<div class="mt-4 text-center">
<h4 class="mb-2 font-semibold text-lg">Pricing Chart (monthly)</h4>
<canvas id="price-chart" data-pricing-target="chart"></canvas>
</div>
</div>
</div>
</div>
+4
View File
@@ -1,6 +1,7 @@
{
"heroku": {
"name": "Heroku",
"color": "#430098",
"seat": 0,
"canine": true,
"tiers": [
@@ -50,6 +51,7 @@
},
"digitalocean": {
"name": "DigitalOcean",
"color": "#0080FF",
"seat": 0,
"canine": true,
"tiers": [
@@ -111,6 +113,7 @@
},
"hetzner": {
"name": "Hetzner",
"color": "#c32d36",
"seat": 0,
"canine": true,
"tiers": [
@@ -142,6 +145,7 @@
},
"render": {
"name": "Render",
"color": "#46e3b7",
"canine": false,
"seat": 19,
"tiers": [