6. Class 2 — Selection, Queries, Popups & Symbology#

Global aims

  • Learn to draw geometries with the Sketch widget for interactive selection.

  • Master querying server-side data (FeatureLayer.queryFeatures, Query parameters).

  • Clone query results into a GraphicsLayer for custom rendering & popups.

  • Understand highlight workflows: default highlight vs custom symbology.

  • Use filters (definitionExpression) to make apps fast and targeted.

  • Build a results panel (HTML + data grouping) to bridge the map and the user’s tasks.

6.1. Drawing selections with Sketch#

Concept

  • The Sketch widget allows users to draw geometries (polygon, rectangle, point, etc.) directly on the map.

  • When drawing is complete, you can pass the geometry to a Query.

Demo

    require(["esri/widgets/Sketch", "esri/layers/GraphicsLayer"], function(Sketch, GraphicsLayer){

    const drawLayer = new GraphicsLayer();
    map.add(drawLayer);

    const sketch = new Sketch({
        view,
        layer: drawLayer,
        availableCreateTools: ["polygon","rectangle"],
        creationMode: "update"
    });

    view.ui.add(sketch, "top-left");

    sketch.on("create", (evt) => {
        if (evt.state === "complete") {
        console.log("User drew geometry:", evt.graphic.geometry);
        // Next step: query a FeatureLayer with it
        }
    });
    });

Pro tips

  • Use creationMode: "update" so drawn graphics are editable immediately.

Disable popup auto-open while drawing (restore later).

Hands-On

  • Add a button “Select Tiles” that activates rectangle drawing.

  • When finished, log the geometry extent.

    // button to start rectangle
    document.getElementById("btnSelectTiles").onclick = () => {
    sketch.create("rectangle");
    };

    // log extent when finished
    sketch.on("create", (e) => {
    if (e.state === "complete") {
        const ext = e.graphic.geometry.extent;
        console.log("Extent:", ext.xmin, ext.ymin, ext.xmax, ext.ymax);
    }
    });

6.2. Querying FeatureLayers#

Concept

FeatureLayer.queryFeatures() lets you fetch features by geometry or attribute conditions.

Always set outFields (e.g., ["*"] or specific fields).

Use returnGeometry if you need to render results.

Demo

    require(["esri/rest/support/Query"], function(Query){

    const q = new Query({
        geometry: userGeom,
        spatialRelationship: "intersects",
        outFields: ["*"],
        returnGeometry: true
    });

    featureLayer.queryFeatures(q).then((results)=>{
        console.log("Found features:", results.features.length);
    });
    });

Pro tips

  • Common spatial relationships: "intersects", "contains", "within".

  • Use maxRecordCountFactor (if supported) or page through results for large datasets.

  • Keep outFields minimal for speed (all fields only during dev).

Hands-On

  • Query counties intersecting your drawn rectangle.

  • Print their names in console.

    // assume `featureLayer` is your counties FL and `sketch` from #1
    sketch.on("create", (e) => {
    if (e.state !== "complete") return;

    const q = new Query({
        geometry: e.graphic.geometry,
        spatialRelationship: "intersects",
        outFields: ["NAME"],
        returnGeometry: true
    });

    featureLayer.queryFeatures(q).then((res) => {
        res.features.forEach(f => console.log(f.attributes.NAME));
    });
    });

6.3. Rendering query results in GraphicsLayer#

Concept

  • Don’t rely on FeatureLayer selection (in 4.x it doesn’t expose a “selected” state easily).

  • Instead: clone results → add to a GraphicsLayer → apply selection symbol and popupTemplate.

Demo

    const selectionGL = new GraphicsLayer({ id:"selections" });
    map.add(selectionGL);

    function showResults(features) {
    selectionGL.removeAll();
    const graphics = features.map(f=>{
        const g = f.clone();
        g.symbol = {
        type: "simple-fill",
        color: [255,255,0,0.3],
        outline: { color:[255,0,0,1], width:2 }
        };
        g.popupTemplate = { title: "{NAME}", content: "POP: {POP2007}" };
        return g;
    });
    selectionGL.addMany(graphics);
    }

Pro tips

  • Put selectionGL on top (map.reorder) so its popups override FeatureLayer popups.

  • Use distinct symbology (e.g., dashed outlines) so users can see selection vs base symbology.

Hands-On

  • Select features → clone them into selectionGL → click to open popups.

  • Clear selection with a “Clear” button.

    function renderSelection(features) {
    selectionGL.removeAll();
    const sym = {
        type: "simple-fill",
        color: [255, 255, 0, 0.3],
        outline: { color: [255, 0, 0, 1], width: 2 }
    };

    const graphics = features.map(f => {
        const g = f.clone();
        g.symbol = sym;
        g.popupTemplate = { title: "{NAME}", content: "POP: {POP2007}" };
        return g;
    });

    selectionGL.addMany(graphics);
    map.reorder(selectionGL, map.layers.length - 1); // keep on top
    }

    // hook into your query (from #2)
    featureLayer.queryFeatures(q).then(res => renderSelection(res.features));

    // Clear button
    document.getElementById("btnClear").onclick = () => selectionGL.removeAll();

6.4. Highlight workflows#

Concept

  • FeatureLayer popups trigger defaultHighlightEnabled highlight.

  • GraphicsLayer doesn’t support this; you need a custom highlight.

Demo: track popup selection

    require(["esri/core/reactiveUtils"], function(reactiveUtils){

    reactiveUtils.watch(
        () => view.popup.selectedFeature,
        (feature) => {
        if (!feature) { clearHighlight(); return; }

        selectionGL.graphics.forEach(g=>{
            g.symbol = (g === feature)
            ? { type:"simple-fill", color:[0,255,255,0.25], outline:{ color:[0,255,255,1], width:3 } }
            : { type:"simple-fill", color:[255,255,0,0.3], outline:{ color:[255,0,0,1], width:2 } };
        });
        }
    );
    });

Pro tips

  • Use consistent highlight styles across the app.

  • Clear highlights when popup closes (popup.visible = false).

Hands-On

  • When you click a selected tile, make its outline thicker/cyan.

  • Clicking another tile should restore the first tile’s normal symbol.

    // base + highlight symbols
    const baseSelSym = { type:"simple-fill", color:[255,255,0,0.3], outline:{ color:[255,0,0,1], width:2 } };
    const hiSelSym   = { type:"simple-fill", color:[0,255,255,0.25], outline:{ color:[0,255,255,1], width:3 } };

    function clearGLHighlight() {
    selectionGL.graphics.forEach(g => g.symbol = baseSelSym);
    }

    reactiveUtils.watch(
    () => view.popup.selectedFeature,
    (feat) => {
        if (!feat) { clearGLHighlight(); return; }
        selectionGL.graphics.forEach(g => {
        g.symbol = (g.attributes.OBJECTID === feat.attributes.OBJECTID) ? hiSelSym : baseSelSym;
        });
    }
    );

    reactiveUtils.watch(
    () => view.popup.visible,
    (vis) => { if (!vis) clearGLHighlight(); }
    );

6.5. Filtering with definitionExpression#

Concept

  • Restricts features at the server level (fastest way to “filter” a FeatureLayer).

Uses SQL-like syntax.

Demo

    featureLayer.definitionExpression = "POP2007 > 100000";

Pro tips

  • Combine filters with parentheses: "STATE_NAME = 'West Virginia' AND POP2007 > 100000".

  • When adding year/attribute filters from UI, always sanitize inputs.

Hands-On

Add checkboxes for years → build a definitionExpression.

Show only features from selected years.

    <!-- example checkboxes -->
    <label><input type="checkbox" class="yearCB" value="2018"> 2018</label>
    <label><input type="checkbox" class="yearCB" value="2019"> 2019</label>
    <label><input type="checkbox" class="yearCB" value="2020"> 2020</label>

    function applyYearFilterFromUI() {
    const years = Array.from(document.querySelectorAll(".yearCB:checked")).map(cb => cb.value);
    featureLayer.definitionExpression = years.length
        ? "(" + years.map(y => `Year LIKE '%${y}%'`).join(" OR ") + ")"
        : "1=1";
    }

    document.querySelectorAll(".yearCB").forEach(cb => {
    cb.addEventListener("change", applyYearFilterFromUI);
    });

6.6. Building a results panel#

Concept

  • Map + table complement each other.

  • Group features by dataset, build collapsible HTML, connect with downloads.

Demo skeleton

    function buildTable(features) {
    let html = "<table class='table'>";
    html += "<tr><th>Name</th><th>Population</th></tr>";
    features.forEach(f=>{
        html += `<tr><td>${f.attributes.NAME}</td><td>${f.attributes.POP2007}</td></tr>`;
    });
    html += "</table>";
    document.getElementById("resultsDiv").innerHTML = html;
    }

Pro tips

  • Use groupings (dataset_name) as table headers.

  • Make rows clickable to trigger view.goTo(feature.geometry).

  • Bootstrap/jQuery (or plain JS) can add collapsible headers.

Hands-On

  • When user selects counties, build a table of {NAME, POP2007}.

  • Clicking a row zooms to that county.

    function buildResultsTable(features) {
    const el = document.getElementById("resultsDiv");
    let html = "<table class='table'><tr><th>Name</th><th>Population</th></tr>";
    features.forEach(f => {
        const a = f.attributes;
        html += `<tr class="resRow" data-oid="${a.OBJECTID}"><td>${a.NAME}</td><td>${a.POP2007}</td></tr>`;
    });
    html += "</table>";
    el.innerHTML = html;

    el.querySelectorAll(".resRow").forEach(row => {
        row.addEventListener("click", () => {
        const oid = row.getAttribute("data-oid");
        const f = features.find(ff => ff.attributes.OBJECTID == oid);
        if (f) view.goTo({ target: f.geometry, zoom: 12 });
        });
    });
    }

    // call right after you have query results:
    featureLayer.queryFeatures(q).then(res => {
    renderSelection(res.features);      // from #3
    buildResultsTable(res.features);    // this function
    });

6.7. Real app polish#

  • Limits: If >100 results, prompt user to refine (avoid UI lag).

  • Error handling: Wrap queries in try/catch and show friendly messages.

  • Performance: Don’t keep every selection layer in memory; clear old graphics.

  • UX: Provide “Clear selection” and “Filter reset” buttons.

  • Accessibility: Use meaningful button labels, not just icons.

  • Testing: Always test large queries and slow networks.

6.8. Hands-On Labs (Class 2)#

6.8.1. Lab A — Draw & query#

  • Enable rectangle drawing via Sketch.

  • Query FeatureLayer for features intersecting the rectangle.

  • Log the count.

// Draw a rectangle and query intersecting features.
    // Assume sketch + featureLayer already defined
    sketch.on("create", (evt) => {
    if (evt.state === "complete") {
        const query = new Query({
        geometry: evt.graphic.geometry,
        spatialRelationship: "intersects",
        outFields: ["*"],
        returnGeometry: true
        });

        featureLayer.queryFeatures(query).then((res) => {
        console.log("Features found:", res.features.length);
        // Optional: pass to display function
        showResults(res.features);
        });
    }
    });

6.8.2. Lab B — Show results in GraphicsLayer#

  • Clone query results into selectionGL.

  • Apply custom symbol + popupTemplate.

  • Add “Clear” button.

// Clone results into a selection layer with custom symbol and popups.
    const selectionGL = new GraphicsLayer({ id: "selection" });
    map.add(selectionGL);

    function showResults(features) {
    selectionGL.removeAll();

    const graphics = features.map(f => {
        const g = f.clone();
        g.symbol = {
        type: "simple-fill",
        color: [255, 255, 0, 0.3],
        outline: { color: [255, 0, 0, 1], width: 2 }
        };
        g.popupTemplate = {
        title: "{NAME}",
        content: "Population: {POP2007}"
        };
        return g;
    });

    selectionGL.addMany(graphics);
    }

    // Clear button
    document.getElementById("clearBtn").onclick = () => selectionGL.removeAll();

6.8.3. Lab C — Popup-driven highlight#

  • Watch view.popup.selectedFeature.

  • Apply cyan outline to the active graphic.

  • Restore others to base symbol.

// Watch popup selection and highlight active graphic.
    reactiveUtils.watch(
    () => view.popup.selectedFeature,
    (feature) => {
        if (!feature) {
        clearHighlight();
        return;
        }
        selectionGL.graphics.forEach(g => {
        g.symbol = (g.attributes.OBJECTID === feature.attributes.OBJECTID)
            ? { type: "simple-fill", color: [0, 255, 255, 0.25],
                outline: { color: [0, 255, 255, 1], width: 3 } }
            : { type: "simple-fill", color: [255, 255, 0, 0.3],
                outline: { color: [255, 0, 0, 1], width: 2 } };
        });
    }
    );

    function clearHighlight() {
    selectionGL.graphics.forEach(g => {
        g.symbol = { type: "simple-fill", color: [255, 255, 0, 0.3],
                    outline: { color: [255, 0, 0, 1], width: 2 } };
    });
    }

6.8.4. Lab D — Add year filter UI#

  • Create checkbox list of years.

  • Build definitionExpression.

  • Apply to FeatureLayer.

// Use checkboxes to build a definitionExpression.
    function applyYearFilter(years) {
    if (years.length === 0) {
        featureLayer.definitionExpression = "1=1"; // reset
        return;
    }
    const expr = years.map(y => `Year = '${y}'`).join(" OR ");
    featureLayer.definitionExpression = `(${expr})`;
    }

    // Example: checkboxes
    document.querySelectorAll(".yearCheckbox").forEach(cb => {
    cb.addEventListener("change", () => {
        const years = Array.from(document.querySelectorAll(".yearCheckbox:checked"))
        .map(el => el.value);
        applyYearFilter(years);
    });
    });

6.8.5. Lab E — Build results panel#

  • Group features by STATE_NAME.

  • Render table with name + population.

  • Add row click → zoom to feature.

// Table with feature list, clickable rows zoom to feature.
    function buildTable(features) {
    let html = "<table class='table'>";
    html += "<tr><th>Name</th><th>Population</th></tr>";

    features.forEach(f => {
        const name = f.attributes.NAME;
        const pop = f.attributes.POP2007;
        html += `<tr class="row-click" data-oid="${f.attributes.OBJECTID}">
                <td>${name}</td><td>${pop}</td></tr>`;
    });

    html += "</table>";
    document.getElementById("resultsDiv").innerHTML = html;

    // Add click-to-zoom
    document.querySelectorAll(".row-click").forEach(row => {
        row.addEventListener("click", () => {
        const oid = row.dataset.oid;
        const feature = features.find(f => f.attributes.OBJECTID == oid);
        if (feature) {
            view.goTo({ target: feature.geometry, zoom: 12 });
        }
        });
    });
    }

6.9. Wrap-up#

By the end of Class 2, you’ll know how to:

  • Let users draw their own queries.

  • Perform server-side spatial queries.

  • Visualize results with symbology & popups in a GraphicsLayer.

  • Manage highlighting and filtering.

  • Provide a results table to bridge UI and data.