1

I am new to this CARTO VL tool and I am currently facing an issue that I can't fix by reading your Git repos or online help.

I have prepared two separate visualizations (one of them has a filter by category, the other one has an animation with time). Both work perfectly and come from the same data source, but I am not able to merge them so that I can have the category filter AND the progress/animation in the same viz.

How I can unify both files?

HERE'S THE FIRST ONE:

<!DOCTYPE html>
<html>

<head>
    <title>Interactive Filter</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta charset="UTF-8">
    <script src="https://libs.cartocdn.com/carto-vl/v1.2.5/carto-vl.min.js"></script>
    <script src="https://api.tiles.mapbox.com/mapbox-gl-js/v0.52.0/mapbox-gl.js"></script>
    <link href="https://api.tiles.mapbox.com/mapbox-gl-js/v0.52.0/mapbox-gl.css" rel="stylesheet" />
    <link href="https://carto.com/developers/carto-vl/v1.2.5/examples/maps/style.css" rel="stylesheet">

    <style>
        li {
            display: flex;
            align-items: center;
            padding: 8px 0;
        }

        .point-mark {
            margin-right: 4px;
        }
    </style>
</head>

<body>
    <div id="map"></div>
    <aside class="toolbox">
        <div class="box">
            <header>
                <h1>Types by country</h1>
            </header>
            <section>
                <p class="description open-sans">Turn types on and off using the checkboxes to filter each type.</p>
            </section>
            <footer class="js-footer"></footer>
            <!-- Add legend data -->
            <section>
                <div id="controls">
                    <ul id="content"></ul>
                </div>
            </section>
        </div>
    </aside>

    <script>
        const s = carto.expressions;

        const map = new mapboxgl.Map({
            container: 'map',
            style: carto.basemaps.darkmatter,
            center: [0, 30],
            zoom: 1
        });

        const nav = new mapboxgl.NavigationControl({
            showCompass: false
        });

        map.addControl(nav, 'top-left');

        carto.setDefaultAuth({
            username: 'ainhoa84',
            apiKey: 'default_public'
        });

        const source = new carto.source.Dataset("test_help_carto");

        let Categories = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K'];

        const viz = new carto.Viz(`
            filter: $type in ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K']
            color: ramp($type, bold)
            width: ramp($count, [20, 100])
        `);

        const layer = new carto.Layer('layer', source, viz);

        layer.addTo(map);

        let initialized = false;

        // When layer loads, trigger legend event
        layer.on('updated', () => {
            if (!initialized) {
                generateLegend();
                setupWidget();
                initialized = true;
            }
        });

        function generateLegend() {
            // Request data for legend from the layer viz
            const legend = layer.viz.color.getLegendData();

            // Create list elements for legend
            const legendList = legend.data.map((legend, index) => {
                const color = rgbToHex(legend.value);
                const key = legend.key.toLowerCase().replace(' ', '-');
                const checked = Categories.includes(legend.key);

                // Use "js-checkbox" class to get all the checkbox elements from the widget
                // Use "data-main_genre-category" attribute to set the category for each checkbox
                return `
                    <li>
                        <input 
                            id="js-checkbox-${key}"
                            class="js-checkbox"
                            type="checkbox"
                            name="categories"
                            data-type-category="${legend.key}"
                        checked>
                        <span class="point-mark" style="background-color:${color};"></span>
                        <label for="js-checkbox-${key}">
                            ${legend.key}
                        </label>
                    </li>\n`;
            }).join('');

            document.getElementById('content').innerHTML = legendList;
        }

        function setupWidget() {
            // 1. Get all the category checkboxes
            const checkboxes = document.getElementsByClassName('js-checkbox');
            // 2. Add an event listener to each category checkbox
            for (let i in checkboxes) {
                const checkbox = checkboxes.item(i);
                checkbox.addEventListener('change', toggleCategory);
            };
        }

        function toggleCategory(event) {
            // 1. Get the clicked checkbox element from the event
            const checkboxElement = event.currentTarget;
            // 2. Get the category through the "data-category-category" attribute
            const category = checkboxElement.getAttribute('data-type-category');
            // 3. Toggle the chosen category from the global Categories array
            Categories = _toggle(Categories, category);
            // 4. Check / uncheck the checkbox element
            checkboxElement.checked = Categories.includes(category);
            // 5. Update the filter
            viz.filter.blendTo(s.in(s.prop('type'), Categories));
        }

        //** Helper Methods **//

        // A function to convert map colors to HEX values for legend    
        function rgbToHex(color) {
            return "#" + ((1 << 24) + (color.r << 16) + (color.g << 8) + color.b).toString(16).slice(1);
        }

        // A function to toggle an element in an array  
        function _toggle(arr, item) {
            return arr.includes(item) ? arr.filter(i => i !== item) : [...arr, item];
        }
    </script>
</body>

</html>

HERE'S THE SECOND ONE:

<!DOCTYPE html>
<html>

<head>
    <title>Evolution</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta charset="UTF-8">
    <script src="https://libs.cartocdn.com/carto-vl/v1.2.5/carto-vl.min.js"></script>
    <script src="https://api.tiles.mapbox.com/mapbox-gl-js/v0.52.0/mapbox-gl.js"></script>
    <link href="https://api.tiles.mapbox.com/mapbox-gl-js/v0.52.0/mapbox-gl.css" rel="stylesheet" />
    <link href="https://carto.com/developers/carto-vl/v1.2.5/examples/maps/style.css" rel="stylesheet">
    <style type="text/css">
        aside.toolbox {
            right: 296px;
        }
        .box {
            width: 320px;
            background: #f2f2f2;
        }
        section {
            display: flex;
            flex-direction: row;
            justify-content: space-between;
            align-items: center;
        }
        hr {
            margin: 15px auto;
        }
        input[type=range] {
            -webkit-appearance: none;
            border: 1px solid white;
            background: transparent;
            border: none;
            cursor: pointer;
            flex: 1;
            padding: auto 10px;
            margin: auto 5px;
        }
        input[type=range]::-webkit-slider-runnable-track {
            height: 3px;
            background: #1785FB;
            border: none;
            border-radius: 3px;
        }
        input[type=range]::-webkit-slider-thumb {
            -webkit-appearance: none;
            border: none;
            height: 10px;
            width: 10px;
            border-radius: 50%;
            background: #1785FB;
            margin-top: -4px;
        }
        input[type=range]:focus {
            outline: none;
        }
        input[type=range]::-moz-range-track {
            height: 3px;
            background: #1785FB;
            border: none;
            border-radius: 3px;
        }
        input[type=range]::-moz-range-thumb {
            border: none;
            height: 10px;
            width: 10px;
            border-radius: 50%;
            background: #1785FB;
        }
        input[type=range].white-thumb::-moz-range-thumb {
            border-radius: 50%;
            border: 2px solid #1785FB;
            background: white;
            height: 12px;
            width: 12px;
        }
        input[type=range].white-thumb::-webkit-slider-thumb {
            border-radius: 50%;
            border: 3px solid #1785FB;
            background: white;
            height: 15px;
            width: 15px;
            margin-top: -6px;
        }
        input[type=range].white-thumb::-ms-thumb {
            border-radius: 50%;
            border: 2px solid #1785FB;
            background: white;
            height: 12px;
            width: 12px;
        }
        input[type=range]:-moz-focusring {
            outline: 1px solid white;
            outline-offset: -1px;
        }
        input[type=range]::-ms-track {
            height: 3px;
            background: transparent;
            border-color: transparent;
            border-width: 6px 0;
            color: transparent;
        }
        input[type=range]::-ms-fill-lower {
            background: #1785FB;
        }
        input[type="range"]::-moz-range-progress {
            background: #1785FB;
        }
        input[type=range]::-ms-fill-upper {
            background: #ccc;
        }
        input[type="range"]::-moz-range-track {
            background: #ccc;
        }
        input[type=range]::-ms-thumb {
            border: none;
            height: 10px;
            width: 10px;
            border-radius: 50%;
            background: #1785FB;
        }
        input[type="button"] {
            width: 36px;
            height: 36px;
            border-radius: 4px;
            border: 0;
            box-shadow: none;
            color: #fff;
            cursor: pointer;
            display: inline-flex;
            font: 500 12px/20px 'Roboto';
            margin: 0;
            margin-right: 10px;
        }
        #map {
            flex: 1;
        }
        #js-duration-span {
            background: white;
            border: 1px solid #ddd;
            width: 32px;
            height: 32px;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        #js-play-button {
            background: url('./play.svg') no-repeat;
            background-color: #1785FB;
            background-position: center;
        }
        #js-pause-button {
            background: url('./pause.svg') no-repeat;
            background-color: #1785FB;
            background-position: center;
        }
    </style>
</head>

<body>
    <div id="map"></div>

    <!-- Animation control elements -->
    <aside class="toolbox">
        <div class="box">
            <header>
                <h1>Types by Year</h1>
            </header>
            <section>
                <p class="description open-sans">Play, stop and chose the duration </p>
            </section>
            <section style="margin-top: 15px;">
                <input type="button" id="js-play-button">
                <input type="button" id="js-pause-button">
                <input type="range" id="js-time-range" min="0" max="1" step="0.01">
            </section>
            <br />
            <section>
                <span id="js-current-time" class="open-sans"></span>
            </section>
            <hr>
            <section>
                <span style="margin-right: 5px" class="open-sans">Duration (seconds)</span>
                <input class="white-thumb" type="range" id="js-duration-range" min="1" max="60" step="1">
                <span style="margin-left: 5px" class="open-sans" id="js-duration-span">10</span>
            </section>
        </div>
    </aside>

    <script>
        const map = new mapboxgl.Map({
            container: 'map',
            style: carto.basemaps.darkmatter,
            center: [0, 30],
            zoom: 1.5,
        });
        const nav = new mapboxgl.NavigationControl();
        map.addControl(nav, 'top-left');
        carto.setDefaultAuth({
            username: 'ainhoa84',
            apiKey: 'default_public'
        });
        const source = new carto.source.Dataset('test_help_carto');
        // Define animation viz with variables
        const viz = new carto.Viz(`
            @duration: 20
            @animation: animation($year, @duration, fade(0, 0.5))
            filter: @animation
            color: ramp($type, bold)
            width: ramp($count, [20, 100])
        `);
        const yearslayer = new carto.Layer('yearslayer', source, viz);
        yearslayer.addTo(map);
        // Get HTML elements
        const $playbutton = document.getElementById('js-play-button');
        const $pausebutton = document.getElementById('js-pause-button');
        const $durationRange = document.getElementById('js-duration-range');
        const $timeRange = document.getElementById('js-time-range');
        const $spanDuration = document.getElementById('js-duration-span');
        const $currentTime = document.getElementById('js-current-time');
        // Save initial time range value
        let last = $timeRange.value;
        // Listen to interaction events
        $durationRange.addEventListener('change', () => {
            const duration = parseInt($durationRange.value, 10);
            // Update animation duration
            viz.variables.duration = $spanDuration.innerHTML = duration;
        });
        $playbutton.addEventListener('click', () => {
            // Play the animation
            viz.variables.animation.play()
        });
        $pausebutton.addEventListener('click', () => {
            // Pause the animation
            viz.variables.animation.pause();
        });
        $timeRange.addEventListener('change', () => {
            // Update animation progress
            viz.variables.animation.setProgressPct($timeRange.value);
            last = $timeRange.value;
        });
        // Listen to layer events
        layer.on('updated', () => {
            if ($timeRange.value == last) {
                $timeRange.value = viz.variables.animation.getProgressPct();
                last = $timeRange.value;
            }
            $currentTime.innerText = viz.variables.animation.getProgressValue().toISOString();
        });
        layer.on('loaded', hideLoader);
        function hideLoader() {
            document.getElementById('loader').style.opacity = '0';
        }
    </script>
</body>

</html>
  • Update status: I succeded to merge both but the only issue now is that when I change something in the legend/filter, the animation stops and time doesn't run anymore. – Ainhoa84 May 19 at 18:12
  • Sorry for the late comment, could you share your most updated code to check what is going on? – ramiroaznar Jul 29 at 19:23
  • Unfortunately I had to use another tool as I wasn't able to sort the issue by myself. The code is the same I posted, but my 30 days free trial expired so I guess the Dataset isn't accessible anymore. – Ainhoa84 Jul 31 at 9:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.