Interactive Periodic Table in JavaScript version 1.0

The periodic table is a chart found pinned to the wall of every school chemistry lab showing various pieces of information on each element, or type of atom. The information is tightly packed and often difficult to read and understand so I decided to develop an interactive web-based version in JavaScript which is easier to use and comprehend than a static paper version.

My original intention was to simply produce a filterable version of the table, and in this post I have done just that. However, while researching the topic I found that it was far more complex than I originally realised and that there is huge scope for expanding the topic to show more infomation and also to show the existing information in different formats. This is therefore an ongoing project which I will enhance in the future. Watch this space...

The Periodic Table

I don't want to spend a huge amount of time to explaining the principles behind the periodic table - while researching this project I found several large books devoted to the topic, so I will just give a brief overview, including details of the data I have included in this, the first version.

If you look at the atom in the banner above you will notice that it has three red spheres at the centre. To use the correct terminology it has three protons in its nucleus, which means that it is a lithium atom. It also has four blue spheres or neutrons but the same number of electrons, the grey spheres orbiting the nucleus. For any given atom of a particular element the number of neutrons and electrons can vary but the number of protons is what defines it as being of a particular element. The number of protons can only be changed with using a large amount of energy: nuclear fission or "splitting the atom" gives two or more smaller atoms of different elements, and fusion or joining nuclei combines two or more atoms into one of a different element.

The periodic table shows each element in a coloured square with, amongst other things, the atomic number or number of protons in the nucleus.

These are the properties of each element shown on my version of the periodic table.

  • Name - the full name of the element. Some are familiar like gold while others are obscure like oganesson, but as only five or six oganesson atoms have ever been detected it is hardly a household name.

  • Atomic number - As I mentioned above this is the number of protons in the nucleus, for example the atomic number of lithium is 3, as shown by the red spheres in the heading.

  • Symbol - a short one or two letter symbol. Some are dervived from the name such as Co for cobalt, others from another language such as Pb for lead, from plumbum, the Latin for lead.

  • Atomic weight - a complex concept that could form a chapter of a book, or even an entire book! In short it is the mass of a sample of the element relative to a sample of carbon-12 (carbon atoms with the same number of neutrons as protons). This is weighted to allow for different atoms of an element to have different numbers of neutrons. Even carbon itself does not have an atomic weight of 12; it is actually 12.011. This is because heavier isotopes of carbon also exist, and you may have heard of carbon-14 used to date organic matter.

  • Category - elements are categorised as metals, metalloids and nonmetals. These are split into nine subcategories (plus unknown) which are shown as different colours on the periodic table.

  • Group - there are 18 groups, represented on the periodic table by numbered columns, although a few also have a name. Elements in each group have similar chemical properties.

  • Period - generally these are represented by the 7 rows although in the conventional table a large number of elements are pushed out of their proper place into two extra rows at the bottom. The electrons orbiting nucleii do so in "shells" or layers; all elements in a particular period have the same number of shells.

  • Block - in a piece of text borrowed from Wikipedia
    "Specific regions of the periodic table can be referred to as blocks in recognition of the sequence in which the electron shells of the elements are filled. Each block is named according to the subshell in which the "last" electron notionally resides."
    As with categories, blocks are shown by colour and it is therefore not possible to show both at once. My implementation defaults to showing category colours but with the option to show block colours.

Enough words for the moment: let's look at the interactive periodic table.

The "big bit in the middle" is of course the periodic table itself. To the left are the filter controls and to the right are the radio buttons and keys for the colour coding: these are what make it interactive.

Now let's take a closer look at some of the elements.

Taking hydrogen in the top left as an example, we see the following:

  • The name Hydrogen

  • The atomic number 1

  • The chemical symbol H

  • The atomic weight 1.008

  • Group (column) 1 - Alkali metals

  • Period (row) 1

  • Category: reactive nonmetal (indicated by colour)

  • If we chose to show block colours we would see that hydrogen is in s-block

The Project

The project consists of the following files plus an HTML page, a CSS file and a graphic. They can be downloaded as a zip from the Downloads page, or you can clone/download the Github repository if you prefer.

  • data.js

  • periodictable.js

  • periodictabledisplay.js

  • periodictablepage.js

Source Code Links

ZIP File
GitHub

The data is hard coded into an array of objects in a separate file. It is sort of fixed but if somebody manages to smash some atoms together with sufficient energy to make more than 118 protons stick together (if only for a tiny fraction of a second) I will need to edit this file.

data.js

const periodictabledata =
[
    {
        name: "Hydrogen",
        atomicnumber: 1,
        symbol: "H",
        category: "Reactive nonmetal",
        atomicweight: "1.008",
        occurrence: "",
        stateofmatter: "",
        group: 1,
        period: 1,
        block: "s",
        row: 1,
        column: 1,
        visible: true
    },

Obviously I won't show the whole 118 elements; one is sufficient for illustration. Most of the properties are familiar from my earlier description but a few need comment:

  • occurrence: for future use

  • stateofmatter: for future use

  • row and column: these refer to the rows and columns in the HTML table, not the periods and groups although they mostly coincide

  • visible: indicates whether or not the element has been filtered out and should be displayed in the UI

This is the source code for the PeriodicTable class.

periodictable.js

class PeriodicTable
{
    constructor()
    {
        this._rowcount = 10;
        this._columncount = 19;
        this._data = periodictabledata;
        this._FilterChangedEventHandlers = [];
    }

    //---------------------------------------------------
    // PROPERTIES
    //---------------------------------------------------

    get columncount() { return this._columncount };
    get rowcount() { return this._rowcount; };
    get data() { return this._data; };

    //-----------------------------------------------------------------
    // METHODS
    //-------------------------------------------------------------------

    AddFilterChangedEventHandler(handler)
    {
        this._FilterChangedEventHandlers.push(handler);
    }


    ApplyFilter(filtercriteria)
    {
        const changed = [];

        for(let element of this._data)
        {
            const filterresults = [];

            filterresults.push(this._Match(element.name, filtercriteria.name, true));
            filterresults.push(this._Match(element.atomicnumber, filtercriteria.atomicnumber, false));
            filterresults.push(this._Match(element.symbol, filtercriteria.symbol, true));
            filterresults.push(this._Match(element.category, filtercriteria.category, false));
            filterresults.push(this._Match(element.group, filtercriteria.group, false));
            filterresults.push(this._Match(element.period, filtercriteria.period, false));
            filterresults.push(this._Match(element.block, filtercriteria.block, false));

            const show = filterresults.every( b => b === true );

            if(show === true)
            {
                if(element.visible === false)
                {
                    element.visible = true;
                    changed.push(element);
                }
            }
            else
            {
                if(element.visible === true)
                {
                    element.visible = false;
                    changed.push(element);
                }
            }
        }

        this._FireFilterChangedEvent(changed);
    }


    ClearFilter()
    {
        const changed = [];

        for(let element of this._data)
        {
            if(element.visible === false)
            {
                element.visible = true;
                changed.push(element);
            }
        }

        this._FireFilterChangedEvent(changed);
    }

    //-----------------------------------------------------------------
    // FUNCTIONS
    //-------------------------------------------------------------------

    _FireFilterChangedEvent(changed)
    {
        this._FilterChangedEventHandlers.forEach(function (Handler) { Handler(changed); });
    }


    _Match(value, criteria, wildcard)
    {
        let match = true;

        if(criteria.length > 0)
        {
            if(wildcard)
            {
                match = value.toString().toLowerCase().includes(criteria.toString().toLowerCase());
            }
            else
            {
                match = (value.toString().toLowerCase() === criteria.toString().toLowerCase());
            }
        }

        return match;
    }
}

The PeriodicTable class represents the underlying data on the periodic table, independently of the data itself in data.js and the visual representation which is handled by the PeriodicTableDisplay class which we will see in a moment.

Let's look at the PeriodicTable class in detail.

Properties

The rowcount and columncount properties refer to the number of rows and columns in the HTML table representing the periodic table, not the number periods or groups. The data is set to the array in data.js, and we also have an array of functions to be called when the table's filtering is changed.

AddFilterChangedEventHandler

This method adds the supplied function to the event handler array. Any UI displaying the periodic table can add event handlers so it knows when to update the display.

ApplyFilter

Applying more than one filter criteria to data, especially when some may or may not be set, is always a bit fiddly and the solution I have come up with here is to create an array of Boolean values for each element. These are set using the _Match function which I will describe in a moment, and the element's show property is set to true only if all of the items in the array are true. If the element's visible property has changed it is added to an array which forms the argument of the filter changed event handler. This ensures the UI need only show/hide the elements which have actually changed.

ClearFilter

This method iterates all elements, changing any which are not visible to visible and adding them to an array to pass to the filter changed event handlers.

_FireFilterChangedEvent

Here we call all the functions in _FilterChangedEventHandlers with the changed array. (At the moment there is only one function but as this project expands we may need more, hence the use of an array.)

_Match

This function returns a Boolean value depending on whether the value argument (ie item of data) matches the criteria argument. The third argument, wildcard, specifies whether to look for an exact match or whether the data just needs to contain the filter criteria. Currently only the element name and chemical symbol use wildcard matches.

Now let's look at the PeriodicTableDisplay class.

periodictabledisplay.js

class PeriodicTableDisplay
{
    constructor(periodictable, tableid)
    {
        this._periodictable = periodictable;
        this._tableid = tableid;

        this._periodictable.AddFilterChangedEventHandler(this._onFilterChanged);

        this._categoryClassMappings =
        {
            "Alkali metal": "alkalimetal",
            "Alkaline earth metal": "alkalineearthmetal",
            "Lanthanide": "lanthanide",
            "Actinide": "actinide",
            "Transition metal": "transitionmetal",
            "Post-transition metal": "posttransitionmetal",
            "Metalloid": "metalloid",
            "Reactive nonmetal": "reactivenonmetal",
            "Noble gas": "noblegas",
            "Unknown": "unknown"
        }

        this._blockClassMappings =
        {
            "s": "sblock",
            "d": "dblock",
            "f": "fblock",
            "p": "pblock"
        }

        this._groupNames =
        {
            1: "Alkali metals",
            2: "Alkaline earth metals ",
            15: "Pnictogens",
            16: "Chalcogens",
            17: "Halogens",
            18: "Noble gases"
        };

        this._createCells();

        this._createColumnHeadings();

        this._createRowHeadings();

        this._populate();
    }


    _onFilterChanged(changed)
    {
        let currentcell = null;

        for(let element of changed)
        {
            currentcell = document.querySelector(`[data-row='${element.row}'][data-column='${element.column}']`);

            currentcell.classList.toggle("elementcellfaded");
        }
    }


    _createCells()
    {
        let table = document.getElementById(this._tableid);

        let currentcell;

        for(let row = 0; row < this._periodictable.rowcount; row++)
        {
            let newrow = document.createElement('tr');
            table.appendChild(newrow);

            for(let column = 0; column < this._periodictable.columncount; column++)
            {
                let cell = document.createElement('td');

                cell.setAttribute("data-row", row);
                cell.setAttribute("data-column", column);

                newrow.appendChild(cell);

                currentcell = document.querySelector(`[data-row='${row}'][data-column='${column}']`);
                currentcell.classList.add("cell");
            }
        }
    }


    _createColumnHeadings()
    {
        for(let column = 1; column <= 18; column++)
        {
            let currentcell = document.querySelector(`[data-row='0'][data-column='${column}']`);
            currentcell.innerHTML = `${column}<br /><span class="groupname">${this._groupNames[column] || " "}</span>`;
            currentcell.classList.add("headingcell");
        }
    }


    _createRowHeadings()
    {
        for(let row = 1; row <= 7; row++)
        {
            let currentcell = document.querySelector(`[data-row='${row}'][data-column='0']`);
            currentcell.innerHTML = row;
            currentcell.classList.add("headingcell");
        }
    }


    _populate()
    {
        for(let element of this._periodictable.data)
        {
            let currentcell = document.querySelector(`[data-row='${element.row}'][data-column='${element.column}']`);

            currentcell.innerHTML = `
                ${element.name}<br />
                ${element.atomicnumber}<br />
                <span class="chemicalsymbol">${element.symbol}</span><br />
                ${element.atomicweight}`;

            currentcell.classList.add("elementcell");
        }

        this.ColorByCategory();
    }


    ColorByCategory()
    {
        for(let element of this._periodictable.data)
        {
            let currentcell = document.querySelector(`[data-row='${element.row}'][data-column='${element.column}']`);

            for(let v of Object.values(this._blockClassMappings))
            {
                currentcell.classList.remove(v);
            }

            currentcell.classList.add(this._categoryClassMappings[element.category]);
        }
    }


    ColorByBlock()
    {
        for(let element of this._periodictable.data)
        {
            let currentcell = document.querySelector(`[data-row='${element.row}'][data-column='${element.column}']`);

            for(let v of Object.values(this._categoryClassMappings))
            {
                currentcell.classList.remove(v);
            }

            currentcell.classList.add(this._blockClassMappings[element.block]);
        }
    }
}

constructor

Firstly we save the periodictable and tableid arguments as properties. These tell the class the periodic table it is displaying and the HTML table to display it in. We also need to add a function to the periodic table's filter changed event array and we will look at the actual function shortly.

Next are a couple of objects providing mappings between data values and their corresponding CSS classes. These are used to colour-code the individual elements.

Finally we call four functions which together create and populate the HTML table.

_onFilterChanged

This is used as an event handler for when the periodic table is filtered. It's argument is an array of elements which have had their visible property changed so we just need to iterate it and toggle the relevant CSS class.

I used a variable for the cells (actually <td> elements) simply to avoid having a very long line of code.

The class for non-visible elements is called elementcellfaded and has an opacity of 0.1 which means they are still just visible which I think looks better than hiding them completely.

_createCells

The periodictable.htm file contains an empty HTML table and this function creates and adds <tr> and <td> elements to it. Note that each <td> has data attributes to identify its row and column.

These could of course be hardcoded into the HTML but having them dynamically generated makes enhancements and additions easier to implement.

_createColumnHeadings and _createRowHeadings

Here we simply set the row and column heading text, and apply the relevant CSS class.

_populate

This function iterates all the elements and picks up the <td> for each with this bit of code:

document.querySelector(`[data-row='${element.row}'][data-column='${element.column}']`)

which is a rather obscure but useful bit of syntax for using with data attributes.

Then we set the innerHTML to the required values before applying the elementcell class.

After the loop we call ColorByCategory to set the colours.

ColorByCategory

This function iterates all elements, picks up the corresponding cell, removes any block classes, and applies the relevant category class.

ColorByBlock

Here we mirror the previous function but remove any category classes and add the correct block class.

Finally we have the periodictablepage.js file which ties all the previous code together with the HTML page.

periodictablepage.js

const APP = {};

window.onload = function()
{
    SetEventHandlers();

    APP.periodictable = new PeriodicTable();

    APP.display = new PeriodicTableDisplay(APP.periodictable, "periodictable");

    APP.filterinputs =
    {
        name: document.getElementById("namefilter"),
        atomicnumber: document.getElementById("atomicnumberfilter"),
        symbol: document.getElementById("symbolfilter"),
        category: document.getElementById("categoryfilter"),
        group: document.getElementById("groupfilter"),
        period: document.getElementById("periodfilter"),
        block: document.getElementById("blockfilter")
    };
}


function SetEventHandlers()
{
    document.getElementById("btnApplyFilter").onclick = ApplyFilter;
    document.getElementById("btnClearFilter").onclick = ClearFilter;

    document.getElementById("colorblock").onclick = ColorByBlock;
    document.getElementById("colorcategory").onclick = ColorByCategory;
}


function ColorByBlock()
{
    this.blur();

    document.getElementById("categorykey").style.display = "none";
    document.getElementById("blockkey").style.display = "inline";

    APP.display.ColorByBlock();
}


function ColorByCategory()
{
    this.blur();

    document.getElementById("blockkey").style.display = "none";
    document.getElementById("categorykey").style.display = "inline";

    APP.display.ColorByCategory();
}


function ApplyFilter()
{
    filtercriteria =
    {
        name: APP.filterinputs.name.value,
        atomicnumber: APP.filterinputs.atomicnumber.value,
        symbol: APP.filterinputs.symbol.value,
        category: APP.filterinputs.category.value,
        group: APP.filterinputs.group.value,
        period: APP.filterinputs.period.value,
        block: APP.filterinputs.block.value
    };

    APP.periodictable.ApplyFilter(filtercriteria)
}


function ClearFilter()
{
    APP.filterinputs.name.value = "";
    APP.filterinputs.atomicnumber.value = "";
    APP.filterinputs.symbol.value = "";
    APP.filterinputs.category.value = "";
    APP.filterinputs.group.value = "";
    APP.filterinputs.period.value = "";
    APP.filterinputs.block.value = "";

    APP.periodictable.ClearFilter();
}

window.onload

Firstly we call a function to set a few onclick event handlers before creating a PeriodicTable and a PeriodicTableDisplay object. Finally I have created an object containing the filter inputs. This isn't strictly necessary but it saves repeating the document.getElementById calls.

SetEventHandlers

Here we just set a few onclick handlers.

ColorByBlock

This function hides and shows the category and block colour key respectively before calling the PeriodicTableDisplay's ColorByBlock method.

ColorByCategory

This is the equivalent of the previous function but for category.

ApplyFilter

This function creates an object containing the various filter criteria set by the user which is then passed to the ApplyFilter methd.

ClearFilter

Here we set all the filter inputs to empty strings and call ClearFilter.

The Finished Project

That's the coding finished so open periodictable.htm in your browser. You should see the page from the screenshot above. You can play around with the filters and radio buttons; shown below is the table with the Name filter set to "s", so we see only elements with the letter "s" in their name, the others being shown faded to 0.1 opacity.

Looking for a VPN?

StrongVPN is running a promotion where customers can get a permanent discount on the monthly plan for just $5 for life! Ends June 30, 2020.