Phase Diagrams

Quick Start

Phase diagrams of alloys seem easy till you really ask what's going on. Here you can really find out.


The app is inspired by the excellent tutorial series from Prof Dr-Ing Martin Bonnet of TU Köln.

Alloy Phase Diagrams

Eutectic* %
Solid Sol %
Cooling Curve
Probe %
Probe T
//One universal basic required here to get things going once loaded
window.onload = function () {
    //restoreDefaultValues(); //Un-comment this if you want to start with defaults

//Main() is hard wired as THE place to start calculating when inputs change
//It does no calculations itself, it merely sets them up, sends off variables, gets results and, if necessary, plots them.
function Main() {
    //Save settings every time you calculate, so they're always ready on a reload

    //Send all the inputs as a structured object
    //If you need to convert to, say, SI units, do it here!
    const inputs = {
        Tm1: sliders.SlideTm1.value,
        Tm2: sliders.SlideTm2.value,
        Eu: sliders.SlideEuPerc.value / 100, //convert % to fractions
        SolSol: sliders.SlideSolSol.value / 100,
        NI: sliders.SlideNI.value / 100,
        ProbeT: sliders.SlideProbeT.value,
        ProbePerc: sliders.SlideProbePerc.value,
        showCooling: document.getElementById('showCooling').checked,

    //Send inputs off to CalcIt where the names are instantly available
    //Get all the resonses as an object, result
    const result = CalcIt(inputs);

    //Set all the text box outputs
    document.getElementById('Melt').value = result.lInfo;
    document.getElementById('Solid').value = result.sInfo;
    //Do all relevant plots by calling plotIt - if there's no plot, nothing happens
    //plotIt is part of the app infrastructure in
    if (result.plots) {
        for (let i = 0; i < result.plots.length; i++) {
            plotIt(result.plots[i], result.canvas[i]);

    //You might have some other stuff to do here, but for most apps that's it for Main!

//Here's the app calculation
//The inputs are just the names provided - their order in the curly brackets is unimportant!
//By convention the input values are provided with the correct units within Main
function CalcIt({ Tm1, Tm2, Eu, SolSol, NI, ProbeT, ProbePerc, showCooling }) {
    if (Tm1 > Tm2) { //By convention we force Tm1 to be lower than Tm2
        Tm2 = Tm1 + 10
        if (Tm2 > 95) {
            Tm2 = 95; Tm1 = 85; sliders.SlideTm1.value = Tm1
        sliders.SlideTm2.value = Tm2
    const Tmin = Math.min(Tm1, Tm2)
    // const EuT=Math.max(0,Tmin*2*Math.abs(Eu-0.5))
    const EuT = Math.max(0, Tmin * SolSol)
    const NIEu = NI * SolSol, PBot = 1 + NI * 3, PTop = 1 + NI * 2.5
    const Eu1 = Eu * (1 - SolSol),Eu100=Eu1*100
    const xLo = Eu1 * SolSol, xHi = Eu1 + (1 - Eu1) * (1 - SolSol)
    let bCurve = [], tCurve = [], euCurve = [], aeCurve = [], beCurve = [], plotData = [], labelData = [], dottedLine = [], borderWidth = [], myColors = [], isStraight = []
    let ProbeLo = 0, ProbeHi = 100
    let crossT = [], tbT = [], nT = 0
    crossT.push(-1), tbT.push("-") //Dummy point
    crossT.push(0), tbT.push("B") //Catch sharp or no end curve
    for (let x = 0; x <= 1.01; x += 0.001) {
        //Bottom curve
        if (x < xLo) {
            bT = EuT + (Tm1 - EuT) * Math.pow((xLo - x) / xLo, PBot)
        } else {
            if (x < xHi) {
                bT = EuT
            } else {
                bT = EuT + (Tm2 - EuT) * Math.pow((x - xHi) / (1 - xHi), PBot)
        //Top curve
        if (x < Eu1) {
            tT = EuT + (Tm1 - EuT) * (1 - Math.pow(x / Eu1, PTop))
        } else {
            tT = EuT + (Tm2 - EuT) * (1 - Math.pow(Math.max(0, (1 - x)) / (1 - Eu1), PTop))
        bCurve.push({ x: 100 * x, y: bT })
        tCurve.push({ x: 100 * x, y: tT })
        //Set the temperatures at probe %
        if (Math.abs(x - ProbePerc / 100) < 0.01) { ProbeLo = bT; ProbeHi = tT }
        //Set the % at probe T
        if (Math.abs(bT - ProbeT) < 1.0 && (x * 100 - crossT[nT]) > 2) { crossT.push(x * 100); tbT.push("B"); nT++ }
        if (Math.abs(tT - ProbeT) < 1.0 && (x * 100 - crossT[nT]) > 2) { crossT.push(x * 100); tbT.push("T"); nT++ }
    crossT.push(100), tbT.push("B") //Catch sharp or no end curve
    //To allow the Cooling Curve to show in front, these need to be pushed on near the end
    // plotData.push(tCurve, bCurve)
    // labelData.push("Liquidus", "Solidus")
    // dottedLine.push(false, false)
    // borderWidth.push(null, null)
    // myColors.push('#afd8f8', '#edc240')
    // isStraight.push(false, false)
    let gotEu=false,gotAlphaBeta=false
    if (SolSol<0.95) {
        euCurve.push({ x: Eu100, y: EuT }, { x: Eu100, y: -10 })
        if (SolSol>0.03) {
            aeCurve.push({ x: xLo * 100, y: EuT }, { x: xLo * 100, y: -10 })
            beCurve.push({ x: xHi * 100, y: EuT }, { x: xHi * 100, y: -10 })
            plotData.push(aeCurve, beCurve)
            labelData.push("α:Eu", "Eu:β")
            dottedLine.push(null, null)
            borderWidth.push(1, 1)
            myColors.push('#4da74d', '#9440ed')
    let cCurve = []
    if (showCooling) {
        const xWidth = ProbePerc < 50 ? 6 : -6
        let xVal = ProbePerc - xWidth / 2
        if (xVal < 0) xVal = 0
        if (xVal > 100) xVal = 100
        let flatPart=(Eu100-ProbePerc)/Eu100
        if (flatPart<0) flatPart=(ProbePerc-Eu100)/(100-Eu100)
        cCurve.push({ x: xVal, y: 100 })
        cCurve.push({ x: xVal + xWidth / 4, y: ProbeHi })
        const tmp=2 * xWidth / 4+flatPart*xWidth/4
        cCurve.push({ x: xVal + tmp, y: ProbeLo })
        cCurve.push({ x: xVal + 3 * xWidth / 4, y: ProbeLo })
        cCurve.push({ x: xVal + xWidth, y: -10 })

    plotData.push(tCurve, bCurve)
    labelData.push("Liquidus", "Solidus")
    dottedLine.push(false, false)
    borderWidth.push(null, null)
    myColors.push('#afd8f8', '#edc240')
    isStraight.push(false, false)

    let hCurve = [], vCurve = [], xT1 = 0; xT2 = 100 //Defaults to full width line
    let lInfo = "-", sInfo = "-"
    //But find if we're in a curve:
    if (crossT.length > 2) {//Otherwise we've no pair of Ts
        crossT.push(-1); tbT.push("-") //Another dummy
        for (let i = 1; i < crossT.length; i++) {
            if (crossT[i] <= ProbePerc && crossT[i + 1] >= ProbePerc && tbT[i] != tbT[i + 1] && (Math.sign(Eu100 - crossT[i]) == Math.sign(Eu100 - crossT[i + 1]))) {
                xT1 = crossT[i]; xT2 = crossT[i + 1]
                //Lever rule
                P1 = 100 * (ProbePerc - crossT[i]) / (crossT[i + 1] - crossT[i])
                P2 = 100 * (crossT[i + 1] - ProbePerc) / (crossT[i + 1] - crossT[i])
                let liqu = "Melt: ", sol1 = "α: ", sol2 = "β: "
                if (Eu1 < 0.01 || SolSol < 0.02) { sol1 = "A: "; sol2 = "B: " }
                if (ProbePerc > Eu100) {
                    // lInfo=liqu+" Composition:" + crossT[i].toFixed(0) +"%, @" +P2.toFixed(0)+"%"
                    // sInfo=sol2+" Composition:" +crossT[i+1].toFixed(0)+"%, @:"+P1.toFixed(0)+"%"
                    lInfo = P2.toFixed(0) + "% of " + crossT[i].toFixed(0) + "% B"
                    sInfo = sol2 + P1.toFixed(0) + "% of " + crossT[i + 1].toFixed(0) + "% B"
                } else {
                    lInfo = P1.toFixed(0) + "% of " + crossT[i + 1].toFixed(0) + "% B"
                    sInfo = sol2 + P2.toFixed(0) + "% of " + crossT[i].toFixed(0) + "% B"
    hCurve.push({ x: xT1, y: ProbeT }, { x: xT2, y: ProbeT })
    vCurve.push({ x: ProbePerc, y: 100 }, { x: ProbePerc, y: -10 })
    //Need some labels
    let inText = []
    if (!gotEu){
        inText.push({txt:"Solid Solution",xp:50,yp:(Tm1-10)/2,fontSize:20})
    } else {
        if (gotAlphaBeta){
            if (xLo>0.01)inText.push({txt:"α",xp:100*xLo/2,yp:-5,fontSize:15}) 
            inText.push({txt:"α + Eu(α+β)",xp:Math.max(1,100*(Eu1+xLo)/2),yp:-5,fontSize:15})
            inText.push({txt:"L + α",xp:Math.max(1,100*(Eu1+xLo)/2),yp:EuT+5,fontSize:15})
            inText.push({txt:"β + Eu(α+β)",xp:Math.max(1,100*(Eu1+xHi)/2),yp:-5,fontSize:15})
            inText.push({txt:"L + β",xp:Math.max(1,100*(Eu1+xHi)/2),yp:EuT+5,fontSize:15})
            if (xHi<0.99)inText.push({txt:"β",xp:100*(1+xHi)/2,yp:-5,fontSize:15}) 
        } else {
            inText.push({txt:"A + Eu(Α+Β)",xp:Math.max(3,100*Eu1/2),yp:-5,fontSize:15}) 
            inText.push({txt:"L +A ",xp:Math.max(3,100*Eu1/2),yp:EuT+5,fontSize:15}) 
            inText.push({txt:"B + Eu(Α+Β)",xp:Math.min(97,100*(1+Eu1)/2),yp:-5,fontSize:15}) 
            inText.push({txt:"L + B",xp:Math.min(97,100*(1+Eu1)/2),yp:EuT+5,fontSize:15}) 
    //Now set up all the graphing data detail by detail.
    const prmap = {
        plotData: plotData, //An array of 1 or more datasets
        lineLabels: labelData, //An array of labels for each dataset
        dottedLine: dottedLine,
        borderWidth: borderWidth,
        colors: myColors,
        xLabel: 'B&%', //Label for the x axis, with an & to separate the units
        yLabel: 'T&rel', //Label for the y axis, with an & to separate the units
        y2Label: null, //Label for the y2 axis, null if not needed
        yAxisL1R2: [], //Array to say which axis each dataset goes on. Blank=Left=1
        logX: false, //Is the x-axis in log form?
        xTicks: undefined, //We can define a tick function if we're being fancy
        logY: false, //Is the y-axis in log form?
        yTicks: undefined, //We can define a tick function if we're being fancy
        legendPosition: 'top', //Where we want the legend - top, bottom, left, right
        hideLegend: true,
        xMinMax: [0, 100], //Set min and max, e.g. [-10,100], leave one or both blank for auto
        yMinMax: [-10, 100], //Set min and max, e.g. [-10,100], leave one or both blank for auto
        y2MinMax: [,], //Set min and max, e.g. [-10,100], leave one or both blank for auto
        xSigFigs: 'F0', //These are the sig figs for the Tooltip readout. A ,wide choice!
        ySigFigs: 'F0', //F for Fixed, P for Precision, E for exponential
        inText: inText,

    //Now we return everything - text boxes, plot and the name of the canvas, which is 'canvas' for a single plot
    return {
        lInfo: lInfo,
        sInfo: sInfo,
        plots: [prmap],
        canvas: ['canvas'],


Most of us find alloy phase diagrams confusing and difficult to interpret. This app creates some plausible-looking diagrams with some pseudo-physics that captures the essence without pretending to be real. Note that for programming convenience, the melting point Tm1 is always forced to be lower than Tm2. Also temperatures are arbitrarily fixed from 0 (pure eutectic) to 100 - we are not trying to mimic any specific alloy system.

The effects we are trying to capture, when we have two materials with different melting points, are the solubility of the solids within each other and the non-ideality of the interactions in the melt.

Solid solubility

With 100% solid solubility you have two smooth curves. The top defines the Liquidus - where you have pure liquid, and the bottom defines the Solidus, the solid solution. In between there is a mixture of melt and solids, which we'll explore later.

With 0% solid solubility, i.e. complete immiscibility of the solids, there are now two sets of liquidus/solidus curves that meet at a point, the Eutectic, with a melting point lower than Tm1. The % of 2 in the Eutectic is defined by the Eutectic* input value.

For intermediate values of solid solubility, things are more complex. The eutectic temperature is higher than the 0% case and, for programming convenience, the eutectic/α/β point (explained later) shifts smoothly towards 1. This will make visual sense when you go between 0% and 100% with the solid solubility slider. It means that to set the "real" eutectic temperature for a given solid solubility, you need to move Eutectic*.


The curvature of the liquidus and solidus curves depends on how (un)comfortable the materials are in each other's presence. With 0 non-ideality (i.e. an ideal solution), everything is linear. With high values, there is strong curvature.

Cooling curves

If you turn on the Cooling curves option you see how, in principle, the phase diagram is determined. You start in the melt and let it cool at a constant rate of heat loss. In the liquid phase the temperature falls with a reasonable slope but as soon as it reaches the liquidus, we start having solids forming. Crystallization gives out heat, so the rate of cooling decreases. At the solidus we revert to a stable cooling curve (with a different slope because of a different heat capacity. If the liquidus and solidus are at the same point (MPt of a pure material or, as we'll see, the eutectic) then we get a horizontal line. In other places we get two "break points" in the cooling curve. If we also have solid phase changes we get additional curvature effects.

Knowing what you've got

Down any verticle line of T at a given B% we know the average content of A and B; nothing can change that! However, that average is made up from a confusing mix of liquids and solids in various proportions. The real purpose of the app is to make sense out of that confusion. So first we need some labels of the various phases; they at least are a basic guide.

For more detail when we choose a melt composition via Probe % we can see what we would expect to find at our chosen Probe T. If we are outside an interesting zone, nothing is reported and we just have a T line crossing the whole graph. When we are inside a zone, the T line detects the intersections with the phase boundaries and we can then work out what to expect.

The first thing is that we have a melt liquid with a %B shown by the intersection with the liquidus, and some solid with a % B shown by intersection with the solidus. At this temperature, if we slide the Probe %, while remaining in the zone, those intersections do not (cannot!) change. What is changing is the relative proportions of that liquid and that solid. Although most of us know that the proportions are calculated via the lever rule, our intuitions are the wrong way round. If the Probe % is rather close to the liquidus, the lever arm towards the liquidus is short but that means we have a lot of liquid. Similarly, a short solid arm means a lot of solid. This makes sense after a while, but get used to reading the values in the boxes below.

Inspired by Prof Bonnet

Although the app makes it look as though all phase diagrams go from 0 to 100% B, real-world phase diagrams are often much more complex. If you look at the iron/carbon phase diagram, you will find a partial solid solubility phase diagram of carbon in iron, with the α's, β's and Eu(α+β), but covering the range from 0 to 6.66% carbon. This means that "0-100% B" actually means not 0-100% carbon but 0 to 100% cementite, Fe3C. How do I know this?

The whole of this app is inspired by, and tries to mimic as much as possible, the superb set of phase diagram tutorials by Prof Dr-Ing Martin Bonnet of TU Köln, for example Phase diagram, binary system - Part 2. It is very instructive to watch a tutorial and to pause and check things live with the app. Any mismatch between the app and the tutorials is my error.