steelbreeze/landscape

View on GitHub
docs/dynamic/index.html

Summary

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

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Dynamic landscape map</title>
    <style>
        @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+Display:ital,wght@0,200;0,300;0,400;0,500;1,200;1,300;1,400;1,500&family=Noto+Sans+Mono:wght@200;300;400;500&family=Noto+Serif+Display:ital,wght@0,200;0,300;0,400;0,500;1,200;1,300;1,400;1,500&display=swap');

        /* Reset browser */
        * {
            margin: 0;
            padding: 0;
        }

        table {
            border-collapse: collapse;
        }

        html {
            background-color: #CCCCCC;

            height: 100vh;
        }

        body {
            background-color: #F9F9F9;

            min-height: 100vh;

            font-family: 'Noto Sans Display', sans-serif;
            font-weight: 300;

            color: rgba(0, 0, 0, 0.75);

            display: flex;
            flex-direction: column;
        }

        /* Consistent spacing of layout */
        header,
        main,
        footer,
        article {
            padding: 0.75em;
        }

        header {
            padding-bottom: 0;
        }

        main {
            padding-top: 0;
        }

        header h1 {
            color: rgb(86, 89, 92);
            font-family: 'Noto Serif Display', serif;
            font-weight: 500;
            font-size: 2em;
        }

        header h2 {
            color: rgb(48, 117, 172);
            font-family: 'Noto Serif Display', serif;
            font-weight: 500;
            font-size: 2em;
        }

        h3 {
            color: rgb(48, 117, 172);
            font-family: 'Noto Serif Display', serif;
            font-weight: 500;
        }

        h4 {
            font-weight: 500;
        }

        main {
            flex-grow: 1;
        }

        footer {
            font-size: 0.85em;
            background-color: rgb(86, 89, 92);
            color: rgba(255, 255, 255, 0.75);
        }

        footer section {
            display: flex;
            flex-wrap: wrap;
        }

        footer article {
            flex: 1;
        }

        footer li {
            white-space: nowrap;
            /* TODO: use a media query to remove for small widths */
        }

        h1 {
            font-family: 'Noto Serif Display', serif;
            font-weight: 500;
        }

        h2 {
            font-family: 'Noto Serif Display', serif;
            font-weight: 500;
        }

        p+p,
        p+h3 {
            margin-top: 0.75em;
        }

        ul {
            list-style-type: none;
        }

        main {
            display: flex;
            flex-direction: row-reverse;
        }

        #title::first-letter {
            text-transform: capitalize;
        }

        @media only screen and (max-width: 750px) {
            main {
                flex-direction: column;
            }
        }

        aside {
            white-space: nowrap;
        }

        article {
            overflow-y: auto;
        }

        label {
            font-size: 0.8em;
            color: rgb(48, 117, 172);
            display: block;
            margin-top: 0.75em;
        }

        label p {
            color: rgba(0, 0, 0, 0.75);

        }

        #fileName {
            cursor: pointer;
            font-size: 1.25em;
        }

        input[type="file"] {
            display: none;
        }

        select {
            appearance: none;
            -moz-appearance: none;
            -webkit-appearance: none;

            cursor: pointer;

            border: none;

            background: none;
            color: rgba(0, 0, 0, 0.75);

            font-family: 'Open Sans', sans-serif;
            font-weight: 300;
            font-size: 1em;

        }

        table {
            width: 100%;
        }

        th {
            background-color: rgba(48, 117, 172, 0.125);

            font-weight: 500;

            font-size: 0.8em;
        }

        th.xy {
            background-color: rgba(48, 117, 172, 0);
        }

        td {
            background-color: rgba(0, 0, 0, 0.125);

            font-size: 0.8em;

            text-align: center;
        }

        td,
        th {
            border: #F9F9F9 1.5px solid;

            padding: 0.5em;
        }

        .axis.y,
        .axis.x {
            width: 20%;
        }

        #title {
            display: inline-block;
        }

        .empty {
            background-color: transparent;
        }

        #title::first-letter {
            text-transform: capitalize;
        }
    </style>
</head>

<body>
    <header>
        <article>
            <h1>Steelbreeze</h1>
            <h2>Dynamic landscape map</h2>
        </article>
    </header>
    <main>
        <aside>
            <article>
                <h3>
                    Settings
                </h3>
                <form>
                    <label for="filePicker">Data source
                        <p id="fileName">None selected</p>
                    </label>
                    <input id="filePicker" type="file" accept=".csv">

                    <label for="xAxisPicker">x-axis</label>
                    <select id="xAxisPicker" name="xAxisPicker">
                        <option value=""></option>
                    </select>

                    <label for="xAxisPicker">y-axis</label>
                    <select id="yAxisPicker" name="yAxisPicker">
                        <option value=""></option>
                    </select>
                    <label for="factPicker">Fact</label>
                    <select id="factPicker" name="factPicker">
                        <option value=""></option>
                    </select>

                </form>
            </article>
        </aside>
        <section>
            <article>
                <h3>
                    <span id="title">Select a source file...</span>
                </h3>
                <table id="landscapeTable">
                    <tr>
                        <th class="axis xy">&nbsp;</th>
                        <th class="axis x">Select x-axis</th>
                    </tr>
                    <tr></tr>
                    <th class="axis y">Select y-axis</th>
                    <td>Select fact</td>
                    </tr>
                </table>
            </article>
            <article>
                <h3>
                    Instructions
                </h3>
                <p>
                    This landscape map viewpoint will be genenrated from a .csv file of your choosing with axes based on
                    the columns found
                    within the data and an associated fact.
                </p>
                <p>Firstly, select a .csv file as a data source under thet Settings header; once selected, the x-axis,
                    y-axis and Fact will be populates with the column headings from the data source and can be used to
                    control the landscape map.</p>
                <p>
                    Note: once you select the .csv file, the data is only loaded into your web browser; it goes no
                    further than this.
                </p>
                <h3>
                    Background
                </h3>
                <p>
                    Landscape maps are a viewpoint in ArchiMate. They represent relationships between
                    concepts projected on to a two-dimensional grid. They are a little like a pivot table, but in the
                    place of numerical aggregates, other facts can be displayed and adjacent cells with the same value
                    can be merged.
                </p>
            </article>
        </section>
    </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 type="module">
        import * as csv from 'https://cdn.skypack.dev/pin/@steelbreeze/csv@v1.0.0-rc.1-4nHggLhodIp0gDFtdQi7/mode=imports,min/optimized/@steelbreeze/csv.js';
        import * as pivot from 'https://cdn.skypack.dev/pin/@steelbreeze/pivot@v4.3.0-JN707WjcEaD97F4i6JD7/mode=imports,min/optimized/@steelbreeze/pivot.js';

        const context = {
            file: undefined,
            json: undefined,
            dimensions: undefined,
            axes: undefined,
            cube: undefined,
            getKey: undefined
        };

        const title = document.getElementById('title');
        const filePicker = document.getElementById('filePicker');
        const fileName = document.getElementById('fileName');
        const factPicker = document.getElementById('factPicker');
        const xAxisPicker = document.getElementById('xAxisPicker');
        const yAxisPicker = document.getElementById('yAxisPicker');

        filePicker.addEventListener('change', fileSelected);
        factPicker.addEventListener('change', pivotAndRender);
        xAxisPicker.addEventListener('change', pivotAndRender);
        yAxisPicker.addEventListener('change', pivotAndRender);

        function fileSelected(event) {
            context.file = event.target.files[0];

            title.innerText = context.file.name.slice(0, -4);
            fileName.innerText = context.file.name;

            fileOpen(context.file);

            // reset the file picker now that we've processed it
            filePicker.value = null;
        }

        function fileOpen(file) {
            const reader = new FileReader();

            reader.onloadend = fileRead;

            reader.readAsText(file);
        }

        function fileRead(event) {
            context.json = csv.parse(event.target.result);

            const columnList = Object.getOwnPropertyNames(context.json[0]);
            const optionList = `<option value="">None selected</option>${columnList.map(column => `<option value="${column}">${column}</option>`).join('')}`;

            context.dimensions = {};

            for (const column of columnList) {
                context.dimensions[column] = context.json.map(row => row[column]).filter((value, index, source) => source.indexOf(value) === index);
            }

            factPicker.innerHTML = optionList;
            xAxisPicker.innerHTML = optionList;
            yAxisPicker.innerHTML = optionList;

            pivotAndRender();
        }

        function pivotAndRender() {
            context.axes = {
                x: xAxisPicker.selectedIndex ? context.json.map(row => row[xAxisPicker.value]).filter(distinct).map(landscape.criteria(xAxisPicker.value)) : ['Select x-axis'].map(value => Object.assign(() => true, {metadata:[{ key: "noX", value: "Select x-axis"}]})),
                y: yAxisPicker.selectedIndex ? context.json.map(row => row[yAxisPicker.value]).filter(distinct).map(landscape.criteria(yAxisPicker.value)) : ['Select y-axis'].map(value => Object.assign(() => true, {metadata:[{ key: "noY", value: "Select y-axis"}]}))
            }

            context.getKey = factPicker.selectedIndex ? (record) => { return { key: factPicker.value, value: record[factPicker.value], style: 'none' }; } : () => { return { key: '', value: 'Select fact', style: 'none' }; };

            context.cube = pivot.pivot(context.json, context.axes.y, context.axes.x);

            const tab = landscape.table(context.cube, context.axes.y, context.axes.x, context.getKey, false);
            landscape.merge(tab, true, true);

            renderTable(tab);
        }

        function renderTable(tableData, elementId, className) {
            const tableElement = document.createElement('table');
            tableElement.id = 'landscapeTable';

            for (const rowData of tableData) {
                tableElement.appendChild(renderRow(rowData));
            }

            document.getElementById('landscapeTable').replaceWith(tableElement);
        }

        function renderRow(rowData) {
            const rowElement = document.createElement('tr');

            for (const cellData of rowData) {
                rowElement.appendChild(renderCell(cellData));
            }

            return rowElement;
        }

        function renderCell(cellData) {
            const cellElement = document.createElement(cellData.style.includes('axis') ? 'th' : 'td');
            cellElement.colSpan = cellData.cols;
            cellElement.rowSpan = cellData.rows;
            cellElement.className = `cell ${cellData.style}`;

            const divElement = document.createElement('div');
            divElement.appendChild(document.createTextNode(cellData.value));

            cellElement.appendChild(divElement);

            return cellElement;
        }

        function distinct(value, index, source) {
            return source.indexOf(value) === index;
        }
    </script>
</body>

</html>