• Jump To … +
    ./demo/canvas-001.js ./demo/canvas-002.js ./demo/canvas-003.js ./demo/canvas-004.js ./demo/canvas-005.js ./demo/canvas-006.js ./demo/canvas-007.js ./demo/canvas-008.js ./demo/canvas-009.js ./demo/canvas-010.js ./demo/canvas-011.js ./demo/canvas-012.js ./demo/canvas-013.js ./demo/canvas-014.js ./demo/canvas-015.js ./demo/canvas-016.js ./demo/canvas-017.js ./demo/canvas-018.js ./demo/canvas-019.js ./demo/canvas-020.js ./demo/canvas-021.js ./demo/canvas-022.js ./demo/canvas-023.js ./demo/canvas-024.js ./demo/canvas-025.js ./demo/canvas-026.js ./demo/canvas-027.js ./demo/canvas-028.js ./demo/canvas-029.js ./demo/canvas-030.js ./demo/canvas-031.js ./demo/canvas-032.js ./demo/canvas-033.js ./demo/canvas-034.js ./demo/canvas-035.js ./demo/canvas-036.js ./demo/canvas-037.js ./demo/canvas-038.js ./demo/canvas-039.js ./demo/canvas-040.js ./demo/canvas-041.js ./demo/canvas-042.js ./demo/canvas-043.js ./demo/canvas-044.js ./demo/canvas-045.js ./demo/canvas-046.js ./demo/canvas-047.js ./demo/component-001.js ./demo/component-002.js ./demo/component-003.js ./demo/component-004.js ./demo/component-005.js ./demo/component-006.js ./demo/component-007.js ./demo/core-001.js ./demo/dom-001.js ./demo/dom-002.js ./demo/dom-003.js ./demo/dom-004.js ./demo/dom-005.js ./demo/dom-006.js ./demo/dom-007.js ./demo/dom-008.js ./demo/dom-009.js ./demo/dom-010.js ./demo/dom-011.js ./demo/dom-012.js ./demo/dom-013.js ./demo/dom-014a.js ./demo/dom-014b.js ./demo/dom-014c.js ./demo/dom-015.js ./demo/dom-016.js ./demo/filters-001.js ./demo/filters-002.js ./demo/filters-003.js ./demo/filters-004.js ./demo/filters-005.js ./demo/filters-006.js ./demo/filters-007.js ./demo/filters-008.js ./demo/filters-009.js ./demo/filters-010.js ./demo/filters-011.js ./demo/filters-012.js ./demo/filters-013.js ./demo/filters-014.js ./demo/filters-015.js ./demo/filters-016.js ./demo/filters-017.js ./demo/filters-018.js ./demo/filters-019.js ./demo/filters-020.js ./demo/filters-501.js ./demo/filters-502.js ./demo/filters-503.js ./demo/filters-504.js ./demo/filters-505.js ./demo/particles-001.js ./demo/particles-002.js ./demo/particles-003.js ./demo/particles-004.js ./demo/particles-005.js ./demo/particles-006.js ./demo/particles-007.js ./demo/particles-008.js ./demo/particles-009.js ./demo/particles-010.js ./demo/particles-011.js ./demo/particles-012.js ./demo/particles-013.js ./demo/particles-014.js ./demo/particles-015.js ./demo/particles-016.js ./demo/temp-001.js ./demo/temp-inkscapeSvgFilters.js
  • ¶

    Demo Component 006

    Spiral charts

  • ¶

    Run code

    import scrawl from '../source/scrawl.js';
  • ¶

    Scene setup

    const canvas = scrawl.library.canvas.mycanvas;
  • ¶

    Function to fetch and parse Wikipedia page view timeseries data

    const getData = (page = 'Cat') => {
    
        return new Promise((resolve, reject) => {
    
            const data = {
                page: '',
                max: 0,
                min: 0,
                fromdate: '20200101',
                todate: '20200101',
                sunday: [], 
                monday: [], 
                tuesday: [], 
                wednesday: [], 
                thursday: [], 
                friday: [], 
                saturday: [], 
            };
    
            let t = new Date(),
                f = new Date();
    
            t.setDate(t.getDate() - 1);
            f.setFullYear(f.getFullYear() - 3);
    
            let fromdate = f.toISOString().split('T')[0].replace(/-/g, ''),
                todate = t.toISOString().split('T')[0].replace(/-/g, ''),
                dayCounter = f.getDay(),
                maxViews = 0, minViews = -1;
    
            let url = `https://wikimedia.org/api/rest_v1/metrics/pageviews/per-article/en.wikipedia/all-access/user/${page}/daily/${fromdate}/${todate}`;
    
            fetch(url)
            .then(response => response.json())
            .then(dataObject => {
    
                let dataArray = dataObject.items;
    
                let results = [[],[],[],[],[],[],[]];
    
                dataArray.forEach(d => {
    
                    let views = d.views;
    
                    maxViews = (views > maxViews) ? views : maxViews;
    
                    if (minViews < 0) minViews = maxViews;
                    else minViews = (views < minViews) ? views : minViews;
    
                    results[dayCounter].push(views);
    
                    dayCounter++;
                    dayCounter = dayCounter % 7;
                });
    
                data.page = page;
                data.max = maxViews;
                data.min = minViews;
                data.fromdate = fromdate;
                data.todate = todate;
                data.sun = results[0];
                data.mon = results[1];
                data.tue = results[2];
                data.wed = results[3];
                data.thu = results[4];
                data.fri = results[5];
                data.sat = results[6];
    
                resolve(data);
            })
            .catch(e => {
    
                console.log(e);
                reject(data);
            });
        });
    };
  • ¶

    Function to build a new chart

    const buildChart = function (page, canvasWrapper, reqs = {}) {
  • ¶

    We will eventually return a Picture entity to the calling invocation - but generating it takes time, hence we return a promise and resolve the Picture entity in due course

        return new Promise((resolve, reject) => {
  • ¶

    We will create a new Cell for the chart, fabricate the required entitys, render the Cell, then capture the output into a single Picture entity which we assign to the canvasWrapper’s Group. After this all completes we can kill the Cell, its Group, and all of the Group’s entitys

            const cell = canvasWrapper.buildCell({
                name: `${page}-cell`,
                width: 400, // reqs.size
                height: 400, // reqs.size
                cleared: false,
                compiled: false,
                shown: false,
                backgroundColor: 'transparent', // reqs.backgroundColor
            });
    
            const chartGroup = scrawl.library.group[cell.name];
  • ¶

    Use Color objects to determine the appropriate color for each charted view value. Again, we’ll kill these objects once we’ve finished with them

            const lowViewsFactory = scrawl.makeColor({
                name: `${page}-low-views`,
                minimumColor: 'azure', // reqs.minimumColor
                maximumColor: 'blue', // reqs.medialColor
            });
    
            const highViewsFactory = scrawl.makeColor({
                name: `${page}-high-views`,
                minimumColor: 'blue', // reqs.medialColor
                maximumColor: 'red', // reqs.maximumColor
            });
  • ¶

    We start by retrieving the data from Wikipedia

            getData(page)
            .then(data => {
  • ¶

    Some initial calculations

                const maxViews = data.max,
                    minViews = data.min,
                    medianViews = (maxViews - minViews) / 2;
    
                const maxDataLen = Math.max(data.mon.length, data.tue.length, data.wed.length, data.thu.length, data.fri.length, data.sat.length, data.sun.length);
  • ¶

    Each day series is a spiral of Line entitys. We’ll cut down on the code by creating a factory to generate each day’s spiral

                const buildDayChart = function (dayData, offsetVal, dayName, missFirst) {
    
                    let currentWeek, currentYear, nextWeek, nextYear;
    
                    for (let i = 0, iz = dayData.length - 2; i < iz; i++) {
    
                        if (missFirst) {
    
                            currentWeek = (i + 1) % 52;
                            currentYear = Math.floor((i + 1) / 52);
    
                            nextWeek = (i + 2) % 52;
                            nextYear = Math.floor((i + 2) / 52);
    
                            nextWeek = nextWeek % 52;
                        }
                        else {
    
                            currentWeek = i % 52;
                            currentYear = Math.floor(i / 52);
    
                            nextWeek = (i + 1) % 52;
                            nextYear = Math.floor((i + 1) / 52);
    
                            nextWeek = nextWeek % 52;
                        }
    
                        let views = dayData[i] - minViews,
                            dataColor;
  • ¶

    Get the appropriate color for this data point’s value

                        if (views < medianViews) dataColor = lowViewsFactory.getRangeColor(views / medianViews);
                        else dataColor = highViewsFactory.getRangeColor((views - medianViews) / medianViews);
  • ¶

    Each line represents a siongle data point, positioned in its appropriate place on the spiral by reference to the chart’s spoke lines

                        scrawl.makeLine({
    
                            name: `${page}-${dayName}-line-${i}`,
                            group: chartGroup,
    
                            lineWidth: 5,
                            strokeStyle: dataColor,
    
                            path: `${page}-week-line-${currentWeek}`,
                            pathPosition: (currentYear * 0.33) + offsetVal,
                            lockTo: 'path',
    
                            endPath: `${page}-week-line-${nextWeek}`,
                            endPathPosition: (nextYear * 0.33) + offsetVal,
                            endLockTo: 'path',
    
                            method: 'draw',
                        });
                    }
                }
  • ¶

    A year is made up of 52 weeks. We create 52 Line entity spokes around a central point to act as the frame for our chart. By setting an appropriate value for each Line’s handleY and roll attributes means that when it comes to building the day spirals we just need to pivot those Lines to our spokes - positioning made easy!

                for (let i = 0; i < 52; i++) {
    
                    scrawl.makeLine({
    
                        name: `${page}-week-line-${i}`,
                        group: chartGroup,
    
                        start: [200, 220],
                        end: [200, (220 - 120)],
                        handle: [0, ((i / 52) * (120 / 3)) + 30],
                        roll: (i / 52) * 360,
                        useAsPath: true,
  • ¶

    We don’t need to see the spokes in the finished chart; we just need them positioned correctly

                        method: 'none',
                    });
                }
  • ¶

    Build each day’s spiral

                buildDayChart(data.mon, 0, 'mon', (data.mon.length < maxDataLen) ? true : false);
                buildDayChart(data.tue, 0.042, 'tue', (data.tue.length < maxDataLen) ? true : false);
                buildDayChart(data.wed, 0.084, 'wed', (data.wed.length < maxDataLen) ? true : false);
                buildDayChart(data.thu, 0.126, 'thu', (data.thu.length < maxDataLen) ? true : false);
                buildDayChart(data.fri, 0.168, 'fri', (data.fri.length < maxDataLen) ? true : false);
                buildDayChart(data.sat, 0.21, 'sat', (data.sat.length < maxDataLen) ? true : false);
                buildDayChart(data.sun, 0.252, 'sun', (data.sun.length < maxDataLen) ? true : false);
  • ¶

    Now all the chart entitys have been created, we no longer need the Color objects

                lowViewsFactory.kill();
                highViewsFactory.kill();
  • ¶

    We will render this Cell manually, outside of the Display cycle animation loop

                return cell.clear();
            })
            .then(res => {
  • ¶

    Setup the cell to capture its image as it compiles …

                scrawl.createImageFromCell(cell, true);
  • ¶

    … and invoke the compile step

                cell.compile()
            })
            .then(res => {
  • ¶

    The compile step will (eventually) add a hidden <img> element to the DOM. This means we no longer require the Cell or its entitys. We need to kill the Cell’s Group before we kill the Cell itself

                chartGroup.kill(true);
                cell.kill();
  • ¶

    Now we can create a single Picture entity for display in the canvasWrapper’s <canvas> element

                let img = scrawl.makePicture({
                    name: `${page}-chart-image`,
                    group: canvasWrapper.base.name,
    
                    asset: `${page}-cell-image`,
    
                    dimensions: ['100%', '100%'],
                    copyDimensions: ['100%', '100%'],
                });
    
                resolve(img);
            })
            .catch(e => {
    
                console.log('buildChart error', e);
                resolve(false);
            });
        });
    
    };
  • ¶

    Initiate the process on page load

    let currentPage = false;
    
    buildChart('Cat', canvas, {})
    .then(res => currentPage = res)
    .catch(e => {});
  • ¶

    Scene animation

    Function to display frames-per-second data, and other information relevant to the demo

    let report = function () {
    
        let testTicker = Date.now(),
            testTime, testNow,
            testMessage = document.querySelector('#reportmessage');
    
        return function () {
    
            testNow = Date.now();
            testTime = testNow - testTicker;
            testTicker = testNow;
    
            testMessage.textContent = `Screen refresh: ${Math.ceil(testTime)}ms; fps: ${Math.floor(1000 / testTime)}
    Entity count: ${scrawl.library.entitynames.length}`;
        };
    }();
  • ¶

    Create the Display cycle animation

    scrawl.makeRender({
    
        name: 'demo-animation',
        target: canvas,
        afterShow: report,
    });
  • ¶

    User interaction

    scrawl.addNativeListener('click', () => {
    
        let page = document.querySelector('#wikipedia-page').value;
    
        if (page) {
    
            buildChart(page, canvas, {})
            .then(res => {
    
                if (currentPage) currentPage.kill();
                currentPage = res;
            })
            .catch(e => {
    
                console.log('main invoker error', e);
            });
        };
    }, '#page-request');
  • ¶

    Development and testing

    console.log(scrawl.library);