steelbreeze/landscape

View on GitHub
docs/index.html

Summary

Maintainability
Test Coverage
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <meta name="description"
        content="Landscape map viewpoint visualisation generation tool for JavaScript written in TypeScript." />
    <meta name="robots" content="index, follow" />
    <meta property="og:image" content="./images/landscape-map.png">
    <meta property="og:image:type" content="image/png">
    <title>Landscape map viewpoint</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
    <link rel="stylesheet" href="https://steelbreeze.net/style/steelbreeze.css">
    <link rel="stylesheet" href="./style/landscape.css">
    <link rel="stylesheet" href="./style/controls.css">
</head>

<body>
    <header>
        <div class="fixedWidth">
            <nav>
                <a href="/">
                    <h1>steelbreeze</h1>
                </a>
            </nav>
            <nav>
                <a href="../pivot">pivot</a>
                <a href="../landscape" class="selected">landscape</a>
                <a href="../state">state</a>
            </nav>
        </div>
    </header>
    <main>
        <div class="mainSection">
            <div class="fixedWidth">
                <article>
                    <h1>Landscape map viewpoint</h1>
                    <p>The landscape map viewpoint as a visualisation good for articulating a portfolio using three
                        dimensions on the x-axis, y-axis and colour-coding. They can show the health of the portfolio
                        clearly: if there is a high concentration of elements within a particular cell, then it’s
                        typically a sign that there is redundancy; conversely if an element covers too much of the table
                        it could be over-extended. The colour-coding speaks for itself. Knowing your domain, you will
                        know shapes and patterns are acceptable or not.</p>
                </article>
                <article class="widget" id="landscapeTarget">
                    <table id="tablan" class="landscape"></table>
                </article>
                <article class="widget">
                    <table class="timeline">
                        <tbody>
                            <tr>
                                <td colspan="3">
                                    <label for="xAxisSelector">x axis:</label>
                                    <select id="xAxisSelector"></select>
                                </td>
                            </tr>
                            <tr>
                                <td colspan="3">
                                    <label for="yAxisSelector">y axis:</label>
                                    <select id="yAxisSelector"></select>
                                </td>
                            </tr>
                            <tr>
                                <td colspan="3">
                                    <label>Split by axis</label>
                                    <label for="splitByY">y:</label>
                                    <input type="radio" name="splitAxis" id="splitByY" />
                                    <label for="splitByX">x:</label>
                                    <input type="radio" name="splitAxis" id="splitByX" checked />
                                </td>
                            </tr>
                            <tr>
                                <td colspan="3">
                                    <label>Merge by axis</label>
                                    <label for="mergeByY">y:</label>
                                    <input type="checkbox" id="mergeByY" checked />
                                    <label for="mergeByX">x:</label>
                                    <input type="checkbox" id="mergeByX" checked />
                                </td>
                            </tr>
                            <tr>
                                <td colspan="3">View as of: <span id="dateOfInterest"></span></td>
                            </tr>
                            <tr>
                                <td colspan="3">
                                    <input type="range" min="-1095" max="1096" value="0" class="slider" id="slider" />
                                </td>
                            </tr>
                            <tr>
                                <td id="earliestDate">Earliest</td>
                                <td id="todaysDate">Today</td>
                                <td id="latestDate">Latest</td>
                            </tr>
                        </tbody>
                    </table>
                </article>
                <article>
                    <p>Move the slider above left/right to change the view in time, or change the dimensions used on
                        each axis.</p>
                    <p>Links: <a href="/landscape">Project</a>; <a
                            href="https://github.com/steelbreeze/landscape">GitHub</a>; <a
                            href="https://pubs.opengroup.org/architecture/archimate2-doc/chap08.html#_Toc371945248">Archimate</a>.
                    </p>
                </article>
            </div>
        </div>
        <div class="mainSection">
            <div class="fixedWidth">
                <article>
                    <h1>Notes</h1>
                    <p>The application landscape map viewpoint above is auto-generated from a <a
                            href="./data/mythicalBank.csv" target="_top">CSV file</a> containing information about
                        applications and the context in which they are used. The landscape maps viewpoint conveys a
                        great deal of information in a compact form, showing the health of a domain clearly and
                        consisely. For more information see the <a
                            href="https://pubs.opengroup.org/architecture/archimate2-doc/chap08.html#_Toc371945248"
                            target="_blank">Archimate architecture viewpoints</a> documentation. They resemble a pivot
                        table, but with content from the underlying data displayed rather than aggregates.</p>
                    <p>Two libraries are used: <a href="https://github.com/steelbreeze/pivot">@steelbreeze/pivot</a> and
                        <a href="https://github.com/steelbreeze/landscape">@steelbreeze/landscape</a>.
                    </p>
                    <p>The first is used to pivot the source data into a cube based on the selected x and y axes. The
                        axes and cube is the landscape library, but they could equally be used for numerical reporting.
                    </p>
                    <p>The second is used to render the results. As a cell in a cube may contain multiple values, we
                        first create duplicate rows or columns to provide only single values in any table cell, we then
                        merge adjacent cells with the same value to provide the resultant table.</p>
                </article>
            </div>
        </div>
        <div class="mainSection">
            <div class="fixedWidth">
                <article>
                    <h1>Browser support</h1>
                    <p>The layout engine is writtin in TypeScript and transpiled into JavaScript ES6; it should
                        therefore work in any modern browser. The json data in this example is loaded with
                        <code>window.fetch</code> therefore a polyfil is required for older browsers.
                    </p>
                </article>
            </div>
        </div>
        <div class="mainSection">
            <div class="fixedWidth">
                <article>
                    <h1>Outstanding work</h1>
                    <p>Create a seperate standalone tool to create optimal axes where this is too time consuming to do
                        in real time.</p>
                    <p>Further optimisations to the axes optimisation algorithm.</p>
                    <p>Split cells with multiple application horizontally and vertically for optimal layout.</p>
                    <p>Smarter ordering of split cells to expliot affinity with neighbouring cells.</p>
                    <p>Base the date slider max and min values based on the underlying data.</p>
                </article>
            </div>
        </div>
    </main>
    <html-include href="https://steelbreeze.net/components/footer.html"></html-include>
    <script src = "https://steelbreeze.net/components/html-include.js"></script>
    <script src="./dist/landscape.min.js"></script>
    <script src="./dist/render.min.js"></script>
    <script type="module">
        import * as pivot from 'https://cdn.skypack.dev/pin/@steelbreeze/pivot@v4.3.0-JN707WjcEaD97F4i6JD7/mode=imports,min/optimized/@steelbreeze/pivot.js';

        // common controls used
        const xAxisSelector = document.getElementById("xAxisSelector");
        const yAxisSelector = document.getElementById("yAxisSelector");
        const splitByX = document.getElementById("splitByX");
        const dateOfInterest = document.getElementById("dateOfInterest");

        xAxisSelector.addEventListener('change', createCube);
        yAxisSelector.addEventListener('change', createCube);
        splitByX.addEventListener('change', renderCube);
        document.getElementById("slider").addEventListener('input', filterCube);
        document.getElementById("splitByY").addEventListener('change', renderCube);
        document.getElementById("mergeByX").addEventListener('change', renderCube);
        document.getElementById("mergeByY").addEventListener('change', renderCube);

        // defaults
        const defaults = {
            dimensions: {
                "Capability": ["Market gateway", "Order execution", "Order management", "Confirmations"],
                "Product": ["Rates", "FX", "MM", "Credit", "Equities"],
                "Status": ["green", "amber", "red"],
                "Location": ["United States", "United Kingdom", "Germany", "Italy"],
                "Supplier": ["Finastra", "ION", "Kx Systems", "Microsoft", "Murex", "Thunderhead", "Wall Street Systems"],
                "Status": ["green", "amber", "red"]
            },
            xAxisColumn: "Product",
            yAxisColumn: "Capability"
        };

        // context used
        const context = {
            json: [
                { "Name": "ION Markets", "Supplier": "ION", "Capability": "Market gateway", "Product": "Credit", "Location": "United Kingdom", "Status": "red", "To": "2022-05-01", "From": "" },
                { "Name": "Azure API GW", "Supplier": "Microsoft", "Capability": "Market gateway", "Product": "Credit", "Location": "Italy", "Status": "green", "To": "", "From": "2022-05-01" },
                { "Name": "Thunderhead ONE", "Supplier": "Thunderhead", "Capability": "Confirmations", "Product": "FX", "Location": "United Kingdom", "Status": "green", "To": "", "From": "" },
                { "Name": "Thunderhead ONE", "Supplier": "Thunderhead", "Capability": "Confirmations", "Product": "MM", "Location": "United Kingdom", "Status": "green", "To": "", "From": "" },
                { "Name": "Thunderhead ONE", "Supplier": "Thunderhead", "Capability": "Confirmations", "Product": "Rates", "Location": "United Kingdom", "Status": "green", "To": "", "From": "" },
                { "Name": "Thunderhead ONE", "Supplier": "Thunderhead", "Capability": "Confirmations", "Product": "Credit", "Location": "United Kingdom", "Status": "green", "To": "", "From": "" },
                { "Name": "Thunderhead ONE", "Supplier": "Thunderhead", "Capability": "Market gateway", "Product": "Credit", "Location": "United Kingdom", "Status": "red", "To": "2022-10-01", "From": "" },
                { "Name": "Thunderhead ONE", "Supplier": "Thunderhead", "Capability": "Confirmations", "Product": "Equities", "Location": "United Kingdom", "Status": "green", "To": "", "From": "2024-06-06" },
                { "Name": "Azure API GW", "Supplier": "Microsoft", "Capability": "Market gateway", "Product": "FX", "Location": "Italy", "Status": "green", "To": "", "From": "2023-02-20" },
                { "Name": "Fusion Summit", "Supplier": "Finastra", "Capability": "Market gateway", "Product": "FX", "Location": "United Kingdom", "Status": "green", "To": "2023-04-15", "From": "" },
                { "Name": "Fusion Summit", "Supplier": "Finastra", "Capability": "Order execution", "Product": "FX", "Location": "United Kingdom", "Status": "green", "To": "2023-01-15", "From": "" },
                { "Name": "Fusion Summit", "Supplier": "Finastra", "Capability": "Market gateway", "Product": "Rates", "Location": "United Kingdom", "Status": "green", "To": "2023-04-15", "From": "" },
                { "Name": "Fusion Summit", "Supplier": "Finastra", "Capability": "Order management", "Product": "Rates", "Location": "United Kingdom", "Status": "green", "To": "", "From": "2023-09-15" },
                { "Name": "Fusion Summit", "Supplier": "Finastra", "Capability": "Order execution", "Product": "Rates", "Location": "United Kingdom", "Status": "green", "To": "", "From": "" },
                { "Name": "Azure API GW", "Supplier": "Microsoft", "Capability": "Market gateway", "Product": "Rates", "Location": "Italy", "Status": "green", "To": "", "From": "2023-02-20" },
                { "Name": "Wall St", "Supplier": "Wall Street Systems", "Capability": "Market gateway", "Product": "MM", "Location": "United Kingdom", "Status": "green", "To": "2022-10-01", "From": "" },
                { "Name": "Wall St", "Supplier": "Wall Street Systems", "Capability": "Order management", "Product": "MM", "Location": "United Kingdom", "Status": "green", "To": "", "From": "" },
                { "Name": "Wall St", "Supplier": "Wall Street Systems", "Capability": "Order execution", "Product": "FX", "Location": "United Kingdom", "Status": "green", "To": "", "From": "2022-11-15" },
                { "Name": "Wall St", "Supplier": "Wall Street Systems", "Capability": "Order execution", "Product": "MM", "Location": "United Kingdom", "Status": "green", "To": "", "From": "2022-11-15" },
                { "Name": "Wall St", "Supplier": "Wall Street Systems", "Capability": "Order management", "Product": "FX", "Location": "United Kingdom", "Status": "green", "To": "", "From": "" },
                { "Name": "Azure API GW", "Supplier": "Microsoft", "Capability": "Market gateway", "Product": "MM", "Location": "Italy", "Status": "green", "To": "", "From": "2022-01-01" },
                { "Name": "MX II", "Supplier": "Murex", "Capability": "Order execution", "Product": "Credit", "Location": "United Kingdom", "Status": "amber", "To": "", "From": "" },
                { "Name": "MX II", "Supplier": "Murex", "Capability": "Order execution", "Product": "MM", "Location": "United Kingdom", "Status": "amber", "To": "2023-01-15", "From": "" },
                { "Name": "MX II", "Supplier": "Murex", "Capability": "Market gateway", "Product": "MM", "Location": "United Kingdom", "Status": "amber", "To": "2022-03-01", "From": "" },
                { "Name": "ION Markets", "Supplier": "ION", "Capability": "Market gateway", "Product": "MM", "Location": "Italy", "Status": "red", "To": "2022-01-01", "From": "" },
                { "Name": "Kdb+", "Supplier": "Kx Systems", "Capability": "Order management", "Product": "Equities", "Location": "Germany", "Status": "green", "To": "", "From": "" },
                { "Name": "Kdb+", "Supplier": "Kx Systems", "Capability": "Market gateway", "Product": "Equities", "Location": "Germany", "Status": "green", "To": "", "From": "" },
                { "Name": "Kdb+", "Supplier": "Kx Systems", "Capability": "Confirmations", "Product": "Equities", "Location": "Germany", "Status": "red", "To": "2024-09-09", "From": "" },
                { "Name": "Kdb+", "Supplier": "Kx Systems", "Capability": "Order execution", "Product": "Equities", "Location": "Germany", "Status": "green", "To": "", "From": "2022-06-01" },
                { "Name": "Kdb+", "Supplier": "Kx Systems", "Capability": "Order execution", "Product": "Equities", "Location": "Germany", "Status": "amber", "To": "2022-07-01", "From": "" }
            ],
            axes: undefined,
            cube: undefined,
            filtered: undefined
        };

        // populate the controle
        const options = Object.getOwnPropertyNames(defaults.dimensions).map(column => `<option value=${column}>${column}</option>`).join("");
        xAxisSelector.innerHTML = options;
        yAxisSelector.innerHTML = options;
        xAxisSelector.value = defaults.xAxisColumn;
        yAxisSelector.value = defaults.yAxisColumn;
        document.getElementById("earliestDate").textContent = fromToday(-1095);
        document.getElementById("todaysDate").textContent = fromToday();
        document.getElementById("latestDate").textContent = fromToday(1096);

        // initialise the page
        createCube();

        // create the cube on initial load or when an axis changes
        function createCube() {
            // create the dimensions to use as the x and y axes; derive from the source data and sort by a pre-defined sequence
            console.time('axes');
            context.axes = {
                x: defaults.dimensions[xAxisSelector.value].map(landscape.criteria(xAxisSelector.value)),
                y: defaults.dimensions[yAxisSelector.value].map(landscape.criteria(yAxisSelector.value))
            };
            console.timeEnd('axes');

            // create the cube
            console.time('cube');
            context.cube = pivot.pivot(context.json, context.axes.y, context.axes.x);
            console.timeEnd('cube');

            filterCube();
        }

        // filter the cube on initial load or when the cube changes then create the raw table
        function filterCube() {
            // get the date of interet from the slider control
            const date = fromToday(slider.value);

            // update the date in the page
            dateOfInterest.textContent = date;

            // filter the cube using the date of interest
            context.filtered = pivot.aggregate(context.cube, records => records.filter(app => (!app.From || app.From < date) && (!app.To || date < app.To)));

            renderCube();
        }

        // merge adjacent cells based on user
        function renderCube() {
            console.time('split');

            // split the cube and axes along the desired axis, creating a table
            const table = landscape.table(context.filtered, context.axes.y, context.axes.x, key, splitByX.checked); // NOTE: as the merge function modifies the table, this must be done each time

            console.timeEnd('split');
            console.time('merge');

            // merge the split cube along the desired axes
            landscape.merge(table, mergeByX.checked, mergeByY.checked);

            console.timeEnd('merge');

            // render the table in the target element
            document.getElementById('tablan').replaceWith(render.table(table, 'tablan', 'landscape'));
        }

        // create text and style to be used when rendering the table
        function key(app) {
            return {
                key: "Name",
                value: app.Name,
                //text: `APP: ${app.Name}`,
                style: app.Status
            };
        }

        // helper function to add days to a date
        function fromToday(offset) {
            var result = new Date();

            result.setMilliseconds(result.getMilliseconds() + ((offset || 0) * 86400000));

            return result.toISOString().substr(0, 10);
        }
    </script>
</body>

</html>