{
// --- 1. CONFIGURATION & DESIGN ---
const config = {
width: 600,
height: 500,
thermoWidth: 25,
fluidWidth: 15,
fontStack: "system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
colors: {
background: "#f8f9fa", border: "#e9ecef", label: "#212529",
fluid: "#e63946", fluidGradient: "#d00000",
indicatorLine: "#adb5bd",
indicatorText: "#023e8a", tick: "#6c757d", rule: "#0077b6"
}
};
const keyTemps = [
{ name: "Boiling Point of Water", K: 373.15 },
{ name: "Ambient Temperature", K: 298.15 },
{ name: "Freezing Point of Water", K: 273.15 },
];
// --- 2. CONVERSION & SCALE LOGIC ---
const KtoC = k => k - 273.15;
const KtoF = k => (k - 273.15) * 1.8 + 32;
const yDomain = [240, 390];
const yScale = d3.scaleLinear().domain(yDomain).range([config.height - 50, 50]);
// --- 3. SVG RENDERING ---
const svg = d3.create("svg")
.attr("width", config.width).attr("height", config.height)
.attr("viewBox", [0, 0, config.width, config.height])
.attr("style", `font-family: ${config.fontStack}; background-color: ${config.colors.background}; border-radius: 8px;`);
const defs = svg.append("defs");
const gradient = defs.append("linearGradient").attr("id", "fluidGradient")
.attr("x1", "0%").attr("y1", "0%").attr("x2", "100%").attr("y2", "0%");
gradient.append("stop").attr("offset", "0%").attr("stop-color", config.colors.fluid);
gradient.append("stop").attr("offset", "100%").attr("stop-color", config.colors.fluidGradient);
// --- LAYOUT GROUPS ---
const indicators = svg.append("g");
const allThermos = svg.append("g").attr("transform", `translate(260, 0)`);
const thermoData = [
{ unit: "K", pos: 0, tickStep: 20, domain: yDomain },
{ unit: "°C", pos: 140, tickStep: 20, domain: yDomain.map(KtoC), ruleLabel: "100°" },
{ unit: "°F", pos: 280, tickStep: 50, domain: yDomain.map(KtoF), ruleLabel: "180°" }
];
thermoData.forEach(t => {
const g = allThermos.append("g").attr("transform", `translate(${t.pos}, 0)`);
const scale = d3.scaleLinear().domain(t.domain).range(yScale.range());
g.append("rect").attr("x", -config.thermoWidth/2).attr("y", yScale.range()[1]).attr("width", config.thermoWidth).attr("height", yScale.range()[0] - yScale.range()[1]).attr("rx", config.thermoWidth/2).attr("ry", config.thermoWidth/2).attr("fill", "#fff").attr("stroke", config.colors.border);
g.append("rect").attr("x", -config.fluidWidth/2).attr("y", yScale(298.15)).attr("width", config.fluidWidth).attr("height", yScale.range()[0] - yScale(298.15)).attr("rx", config.fluidWidth/2).attr("ry", config.fluidWidth/2).attr("fill", "url(#fluidGradient)");
g.append("text").attr("y", 30).attr("text-anchor", "middle").attr("font-size", "16px").attr("font-weight", "600").attr("fill", config.colors.label).text(t.unit);
const axisGroup = g.append("g").attr("transform", `translate(${config.thermoWidth / 2 + 5}, 0)`);
const minorTickStep = 5;
const minorStartTick = Math.ceil(t.domain[0] / minorTickStep) * minorTickStep;
for (let val = minorStartTick; val <= t.domain[1]; val += minorTickStep) {
if (val % t.tickStep !== 0) {
const y = scale(val);
axisGroup.append("line").attr("x1", 0).attr("x2", 4).attr("y1", y).attr("y2", y).attr("stroke", config.colors.tick);
}
}
const startTick = Math.ceil(t.domain[0] / t.tickStep) * t.tickStep;
for (let val = startTick; val <= t.domain[1]; val += t.tickStep) {
const y = scale(val);
axisGroup.append("line").attr("x1", 0).attr("x2", 8).attr("y1", y).attr("y2", y).attr("stroke", config.colors.tick);
axisGroup.append("text").attr("x", 12).attr("y", y).attr("dy", "0.32em").attr("fill", config.colors.label).attr("font-size", "12px").text(d3.format(".0f")(val));
}
if (t.ruleLabel) {
const ruleX = -config.thermoWidth / 2 - 30;
const yFP = yScale(273.15);
const yBP = yScale(373.15);
const ruleGroup = g.append("g");
const textCenterY = (yBP + yFP) / 2;
const textLabel = ruleGroup.append("text")
.attr("x", ruleX).attr("y", textCenterY)
.attr("text-anchor", "middle").attr("dominant-baseline", "middle")
.attr("fill", config.colors.rule).attr("font-size", "12px").attr("font-weight", "500")
.html(t.ruleLabel);
const bbox = textLabel.node().getBBox();
// Asymmetrical padding to create a visually balanced gap
const paddingTop = 12; // Increased top padding
const paddingBottom = 8; // Decreased bottom padding
const gapTop = textCenterY - (bbox.height / 2) - paddingTop;
const gapBottom = textCenterY + (bbox.height / 2) + paddingBottom;
const lineGroup = ruleGroup.insert("g", "text")
.attr("stroke", config.colors.rule).attr("stroke-width", 1.5);
lineGroup.append("line").attr("x1", ruleX).attr("y1", yBP).attr("x2", ruleX).attr("y2", gapTop);
lineGroup.append("line").attr("x1", ruleX).attr("y1", gapBottom).attr("x2", ruleX).attr("y2", yFP);
lineGroup.append("line").attr("x1", ruleX - 4).attr("y1", yBP).attr("x2", ruleX + 4).attr("y2", yBP);
lineGroup.append("line").attr("x1", ruleX - 4).attr("y1", yFP).attr("x2", ruleX + 4).attr("y2", yFP);
}
});
// Draw main indicator lines and labels on the far left
keyTemps.forEach(temp => {
const yPos = yScale(temp.K);
indicators.append("line").attr("x1", 20).attr("x2", config.width - 20).attr("y1", yPos).attr("y2", yPos).attr("stroke", config.colors.indicatorLine).attr("stroke-width", 1).attr("stroke-dasharray", "4 3");
const textGroup = indicators.append("g");
const mainLabel = textGroup.append("text").attr("x", 25).attr("y", yPos - 10).attr("font-size", "14px").attr("font-weight", "bold").attr("fill", config.colors.indicatorText).text(temp.name);
const bbox = mainLabel.node().getBBox();
textGroup.insert("rect", "text").attr("x", bbox.x - 3).attr("y", bbox.y - 2).attr("width", bbox.width + 6).attr("height", bbox.height + 4).attr("fill", "rgba(248, 249, 250, 0.85)");
indicators.append("text").attr("x", 25).attr("y", yPos + 14).attr("font-size", "12px").attr("fill", "#6c757d").text(`${temp.K.toFixed(2)} K | ${KtoC(temp.K).toFixed(1)} °C | ${KtoF(temp.K).toFixed(1)} °F`);
});
// Return the SVG wrapped in a centering div
return html`<div style="display: flex; justify-content: center;">${svg.node()}</div>`;
}
More Than a Formula: Understanding Temperature Scales
In science, we deal with three common temperature scales: Celsius (°C), Fahrenheit (°F), and Kelvin (K). You have likely been told to memorize a set of equations to convert between them:
\[ t/^\circ\text{C} = (t/^\circ\text{F} - 32) / 1.8 \] \[ t/^\circ\text{F} = (t/^\circ\text{C} \times 1.8) + 32 \] \[ T/\text{K} = t/^\circ\text{C} + 273.15 \]
Memorizing these is one thing; understanding them is another. These formulas are not arbitrary rules. They are the logical consequence of how each temperature scale was designed. To truly understand them, we must first see how they relate to each other visually.
Visualizing the Scales: Water as a Universal Reference
The Celsius and Fahrenheit scales were both built around the most important substance for early science: water. They define their values based on its freezing and boiling points, but they do so in very different ways.
- The Celsius scale sets the freezing point of water to 0 °C and the boiling point to 100 °C.
- The Fahrenheit scale sets the freezing point to 32 °F and the boiling point to 212 °F.
The Kelvin scale, as we will see, is based on a more fundamental principle, but its step size is conveniently the same as Celsius.
These different design choices create the two major challenges in temperature conversion: a different starting point (offset) and a different step size (scaling). The illustration below summarizes these key differences and will serve as our guide for understanding the conversion formulas.
Pay close attention to three features in the diagram:
- How the freezing and boiling points of water align across the three scales.
- The different numbers assigned to “Ambient Temperature” in each system.
- The visual comparison between the “100 Degree Span” for Celsius and the “180 Degree Span” for Fahrenheit. This difference is the key to the famous “1.8” factor in the conversion equations.
A Tale of Two Problems: Offset and Scaling
With the visual model from our illustration in mind, we can now deconstruct the conversion formulas into two logical steps.
1. The Offset Problem (Fixing the Starting Point)
As the thermometers show, the Celsius scale starts its count at the freezing point of water (0 °C), but the Fahrenheit scale starts its count 32 degrees below that point. To convert between them, we must first align their starting points by subtracting 32 from the Fahrenheit temperature. This action effectively “re-zeros” the Fahrenheit scale so that a value of 0 now corresponds to the freezing point of water, just like the Celsius scale.
2. The Scaling Problem (Fixing the Step Size)
Now that our scales are aligned, we must address the size of the degrees themselves. The illustration clearly shows that the space between the freezing and boiling points is covered by 100 Celsius degrees but 180 Fahrenheit degrees. This means that Celsius degrees are “larger” than Fahrenheit degrees. The ratio of their step sizes is:
\[ \frac{\text{Fahrenheit degrees}}{\text{Celsius degrees}} = \frac{180}{100} = 1.8 \]
For every 1.8 degrees that pass on the Fahrenheit scale, only 1 degree passes on the Celsius scale. This is why, after subtracting the offset, we divide by 1.8 to convert from Fahrenheit to Celsius, and multiply by 1.8 to go the other way.
A New Kind of Zero: The Absolute Scale
Celsius and Fahrenheit are relative scales because their zero points are set relative to an arbitrary substance. For many scientific laws, however, we need an absolute scale, where zero means true zero.
The ultimate zero is absolute zero, the theoretical temperature at which all particulate motion ceases. The Kelvin scale is an absolute scale because it sets its zero point at this fundamental limit. It was brilliantly designed to make the size of a kelvin identical to the size of a Celsius degree. This means there is no scaling factor; the only difference is their starting point.
Why Kelvin is the Scientific Standard
The shift to an absolute zero point is essential for modern science. The first clue to its special status is in the notation: we write °C and °F, but simply K. A kelvin is not a “degree” relative to something; it is an absolute unit of temperature.
This matters because many fundamental scientific equations, particularly in thermodynamics and the study of gases, describe a direct proportionality to temperature. This means that if you double the absolute temperature, you double a related property like pressure. This mathematical relationship only works if the temperature scale starts at true zero.
A perfect example is the Ideal Gas Law (\(pV=nRT\)).
This law states that the pressure of a gas in a rigid container is directly proportional to its absolute temperature (\(P \propto T\)). Let’s see what happens when we double the temperature using both Kelvin and Celsius.
- Using Kelvin: If we heat a gas from 100 K to 200 K, we have truly doubled its absolute temperature. As a result, its pressure doubles. The math works.
- Using Celsius: If we heat the same gas from 10 °C to 20 °C, it seems like we doubled the temperature. But have we? In absolute terms, the temperature changed from 283.15 K to 293.15 K. This is only a small fractional increase, not a doubling. As a result, the pressure will only increase slightly, not double.
Using a relative scale like Celsius would break the mathematical foundation of these laws and give incorrect results. Therefore, whenever a calculation involves a temperature ratio or a direct proportionality, using Kelvin is not just recommended; it is required.