<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<title>Animate layer view effect | Sample | ArcGIS Maps SDK for JavaScript</title>
<!-- Load the ArcGIS Maps SDK for JavaScript from CDN -->
<script type="module" src="https://js.arcgis.com/5.0/"></script>
--calcite-color-brand: #333333;
--calcite-color-brand-hover: #222222;
transition: opacity 200ms;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
transform: translate3d(-50%, -120%, 0);
border-top: 1px solid lightgray;
.chart .row .labels > span {
.chart .row .value > span {
border-top: 1px solid lightgray;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
background-color: rgb(0, 92, 230);
background-color: rgb(255, 20, 20);
url("https://js.arcgis.com/5.0/esri/symbols/patterns/backward-diagonal.png") repeat;
border-bottom: 1px solid gray;
border-top: 1px solid gray;
border-left: 1px dashed gray;
border-right: 1px dashed gray;
<calcite-shell id="appShell">
<div id="titleDiv" slot="top-left">Obama / McCain 2008 Election - voting districts gap</div>
<arcgis-expand slot="top-left" expand-icon="list-bullet">
<arcgis-legend legend-style="classic"></arcgis-legend>
<div id="footer" slot="footer">
<calcite-block id="sliderValue-block" expanded label="current year">
<span id="sliderValue"></span>of the votes separate the two candidates
<calcite-block id="slider-block" label="slider block" expanded>
<calcite-slider min="0" max="100" step="0.25" value="50" label-ticks ticks="100">
<calcite-block label="play button block" id="play-block" expanded>
<calcite-button id="playButton" appearance="transparent" icon-start="play"
<div id="tooltipContent" class="tooltip-content" style="visibility: hidden">
<div id="chart" class="chart">
<div class="row democrat">
<div class="labels"><span>Obama:</span></div>
<div class="data"><div class="bar"></div></div>
<div class="value"><span></span></div>
<div class="row republican">
<div class="labels"><span>McCain:</span></div>
<div class="data"><div class="bar"></div></div>
<div class="value"><span></span></div>
<div class="labels"><span>Gap:</span></div>
<div class="bar"><div></div></div>
<div class="value"><span></span></div>
<div class="tooltip-text">Click to view precincts with similar gap</div>
const [Map, FeatureLayer, promiseUtils] = await $arcgis.import([
"@arcgis/core/layers/FeatureLayer.js",
"@arcgis/core/core/promiseUtils.js",
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
const layer = new FeatureLayer({
id: "359bc19d9bbb4f2ba1b2baec7e13e757",
outFields: ["PERCENT_GAP"],
// don't show precincts that didn't record any votes
definitionExpression: "(P2008_D > 0) AND (P2008_R > 0)",
title: "Voting precincts",
color: "rgb(0, 92, 230)",
color: "rgb(255, 20, 20)",
color: "rgb(158, 85, 156)",
valueExpression: "$feature.P2008_D + $feature.P2008_R",
valueExpressionTitle: "Turnout",
id: "3582b744bba84668b52a16b0b6942544",
const viewElement = document.querySelector("arcgis-map");
viewElement.constraints = {
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
const sliderValue = document.getElementById("sliderValue");
const playButton = document.getElementById("playButton");
const slider = document.querySelector("calcite-slider");
slider.labelFormatter = function (value, type) {
return value === slider.min
slider.addEventListener("calciteSliderInput", inputHandler);
function inputHandler(event) {
setGapValue(parseInt(slider.value));
// Toggle animation on/off when user
// clicks on the play button
playButton.addEventListener("click", () => {
playButton.iconStart === "pause" ? stopAnimation() : startAnimation();
// When the layerview is available, setup hovering interactivity
const layerView = await viewElement.whenLayerView(layer);
setupHoverTooltip(layerView);
// Starts the application by visualizing a gap of 50% between the two candidates
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
* Sets the current visualized gap.
function setGapValue(value) {
sliderValue.textContent = `${(Math.round(value * 100) / 100).toFixed(2)}%`;
layerView.featureEffect = createEffect(value);
* Creates a feature effect centered around a gap between the 2 candidates.
* If the precincts have the specified gap percentage, the drop-shadow
* effect is applied to make them stand out from the rest. If they
* fall outside of the specified gap percentage, grayscale, blur
* and opacity effects are applied to subdue their presence.
function createEffect(gapValue) {
gapValue = Math.min(100, Math.max(0, gapValue));
function roundToTheTenth(value) {
return Math.round(value * 10) / 10;
where: `PERCENT_GAP > ${roundToTheTenth(gapValue - 1)} AND PERCENT_GAP < ${roundToTheTenth(gapValue + 1)}`,
includedEffect: "drop-shadow(0, 2px, 2px, black)",
excludedEffect: "grayscale(25%) blur(5px) opacity(25%)",
* Sets up a moving tooltip that displays
* a chart with the voter count for each candidate,
* and the gap between the two.
function setupHoverTooltip(layerview) {
const tooltip = createTooltip();
const hitTest = promiseUtils.debounce((point) => {
return viewElement.hitTest(point).then((hit) => {
const results = hit.results.filter((result) => {
return result.graphic.layer === layer;
const graphic = results[0].graphic;
const screenPoint = hit.screenPoint;
screenPoint: screenPoint,
democrat: Math.round(graphic.getAttribute("P2008_D")),
republican: Math.round(graphic.getAttribute("P2008_R")),
viewElement.addEventListener("arcgisViewPointerMove", async (event) => {
const result = await hitTest(event.detail);
highlight = layerview.highlight(result.graphic);
tooltip.show(result.screenPoint, result.values);
if (error.name !== "AbortError") {
console.error("Unexpected hitTest error:", error);
viewElement.addEventListener("arcgisViewClick", async (event) => {
const result = await hitTest(event.detail);
const dem = result.values.democrat;
const rep = result.values.republican;
const p_gap = ((Math.max(dem, rep) - Math.min(dem, rep)) / (dem + rep)) * 100;
animation = animateTo(p_gap);
if (error.name !== "AbortError") {
console.error("Unexpected hitTest error:", error);
* Starts the animation that cycle
* through the gap between the two candidates.
function startAnimation() {
animation = animate(slider.value);
playButton.iconStart = "pause";
playButton.textContent = "Pause";
function stopAnimation() {
playButton.iconStart = "play";
playButton.textContent = "Play";
* Animates the visualized gap continously.
function animate(startValue) {
requestAnimationFrame(frame);
requestAnimationFrame(frame);
* Animates to a gap value.
function animateTo(targetValue) {
const value = slider.value;
if (Math.abs(targetValue - value) < 1) {
setGapValue(targetValue);
setGapValue(value + (targetValue - value) * 0.25);
requestAnimationFrame(frame);
requestAnimationFrame(frame);
* Creates a tooltip to display a chart showing the raw voters count
* and the gap between the two candidates.
function createTooltip() {
const tooltip = document.createElement("div");
const style = tooltip.style;
tooltip.setAttribute("role", "tooltip");
tooltip.classList.add("tooltip");
const content = document.getElementById("tooltipContent");
content.style.visibility = "visible";
content.classList.add("esri-widget");
tooltip.appendChild(content);
viewElement.appendChild(tooltip);
// Cache chart DOM nodes once (avoid repeated querySelector on every update)
const demBar = content.querySelector("#chart .row.democrat .bar");
const demValue = content.querySelector("#chart .row.democrat .value > span");
const repBar = content.querySelector("#chart .row.republican .bar");
const repValue = content.querySelector("#chart .row.republican .value > span");
const gapBar = content.querySelector("#chart .row.gap .bar");
const gapValue = content.querySelector("#chart .row.gap .value > span");
x += (targetX - x) * 0.5;
y += (targetY - y) * 0.5;
if (Math.abs(targetX - x) < 1 && Math.abs(targetY - y) < 1) {
moveRaFTimer = requestAnimationFrame(moveStep);
style.transform = "translate3d(" + Math.round(x) + "px," + Math.round(y) + "px, 0)";
moveRaFTimer = requestAnimationFrame(moveStep);
function updateContent(values) {
if (dem === values.democrat && rep === values.republican) {
cancelAnimationFrame(updateRaFTimer);
updateRaFTimer = requestAnimationFrame(() => {
let p_gap = (Math.max(dem, rep) - Math.min(dem, rep)) / (dem + rep);
p_gap = Math.round(p_gap * 10000) / 100;
const p_dem = (dem / (dem + rep)) * 100;
const p_rep = (rep / (dem + rep)) * 100;
demBar.style.width = p_dem + "%";
demValue.textContent = dem;
repBar.style.width = p_rep + "%";
repValue.textContent = rep;
gapBar.style.width = p_gap + "%";
gapBar.style.marginLeft = Math.min(p_dem, p_rep) + "%";
gapValue.textContent = p_gap + "%";
show: (point, values) => {