6. Class 2 — Selection, Queries, Popups & Symbology#
Global aims
Learn to draw geometries with the
Sketchwidget for interactive selection.Master querying server-side data (
FeatureLayer.queryFeatures,Queryparameters).Clone query results into a
GraphicsLayerfor 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
Sketchwidget 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
outFieldsminimal 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 andpopupTemplate.
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
selectionGLon 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
popupcloses (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/catchand 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
FeatureLayerfor 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
checkboxlist 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&popupsin aGraphicsLayer.Manage highlighting and filtering.
Provide a results table to bridge UI and data.