<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no">
<title>Client-side queries with Cedar - 4.9</title>
<link rel="stylesheet" href="https://js.arcgis.com/4.9/esri/css/main.css">
<link rel="stylesheet" href="https://js.arcgis.com/4.9/dijit/themes/claro/claro.css">
<style>
html,
body,
#viewDiv {
padding: 0;
margin: 0;
height: 100%;
width: 100%;
overflow: hidden;
font-family: "Avenir Next W00", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
#cedarPanel {
background: #fff;
font: "Avenir Next W00";
line-height: 1.5em;
overflow: auto;
padding: 10px;
width: 400px;
height: 250px;
}
.chart {
height: 300px;
}
#instructions {
width: 300px;
padding: 15px;
}
</style>
<!-- Load the Chart.js library -->
<script src="https://www.amcharts.com/lib/3/amcharts.js"></script>
<!-- in this case, we only need bar charts, so we'll load the appropriate amCharts script -->
<script src="https://www.amcharts.com/lib/3/serial.js"></script>
<!-- load the arcgis-rest-js scripts -->
<script src="https://unpkg.com/@esri/arcgis-rest-request@1.7.1/dist/umd/request.umd.min.js"></script>
<script src="https://unpkg.com/@esri/arcgis-rest-feature-service@1.7.1/dist/umd/feature-service.umd.min.js"></script>
<!-- optionally load an amcharts theme; cedar provides a calcite theme -->
<script src="https://unpkg.com/@esri/cedar/dist/umd/themes/amCharts/calcite.js"></script>
<!-- load cedar -->
<script src="https://unpkg.com/@esri/cedar/dist/umd/cedar.js"></script>
<script src="https://js.arcgis.com/4.9/"></script>
<script>
require([
"esri/widgets/Sketch/SketchViewModel",
"esri/geometry/Polyline",
"esri/geometry/Point",
"esri/Graphic",
"esri/Map",
"esri/views/MapView",
"esri/layers/FeatureLayer",
"esri/layers/GraphicsLayer",
"esri/geometry/geometryEngine",
"esri/widgets/Expand",
"esri/widgets/Legend",
"esri/widgets/Search",
"esri/core/watchUtils"
], function(
SketchViewModel, Polyline, Point,
Graphic, Map, MapView, FeatureLayer, GraphicsLayer,
geometryEngine, Expand, Legend, Search, watchUtils
) {
// App 'globals'
let sketchViewModel, featureLayerView, pausableWatchHandle,
centerGraphic, edgeGraphic, polylineGraphic,
bufferGraphic, labelGraphic, chartExpand, cedarExpand, activeGraphic;
const unit = "kilometers";
// Create layers
const graphicsLayer = new GraphicsLayer();
const featureLayer = new FeatureLayer({
portalItem: {
id: "83c37666a059480bb8a7cb73f449ff52"
},
outFields: ["*"]
});
// Create map
const map = new Map({
basemap: "dark-gray",
layers: [featureLayer, graphicsLayer]
});
// Create view
const view = new MapView({
container: "viewDiv",
map: map,
zoom: 12,
center: [-122.083, 37.3069],
constraints: {
maxScale: 0,
minScale: 300000
}
});
// Set up statistics definition for client-side query
// Total popultion of age groups by gender in census tracts
const statDefinitions = [
"FEM85C10", "FEM80C10", "FEM75C10", "FEM70C10", "FEM65C10",
"FEM60C10",
"FEM55C10", "FEM50C10", "FEM45C10", "FEM40C10", "FEM35C10",
"FEM30C10",
"FEM25C10", "FEM20C10", "FEM15C10", "FEM10C10", "FEM5C10",
"FEM0C10",
"MALE85C10", "MALE80C10", "MALE75C10", "MALE70C10", "MALE65C10",
"MALE60C10",
"MALE55C10", "MALE50C10", "MALE45C10", "MALE40C10", "MALE35C10",
"MALE30C10",
"MALE25C10", "MALE20C10", "MALE15C10", "MALE10C10", "MALE5C10",
"MALE0C10"
].map(function(fieldName) {
return {
onStatisticField: fieldName,
outStatisticFieldName: fieldName + "_TOTAL",
statisticType: "sum"
};
});
// Update UI
setUpViewHandles();
setUpSketch();
setUpGraphicClickHandle();
/*****************************************************************
* Wire up handlers for different view states
*****************************************************************/
function setUpViewHandles() {
// When layer is loaded, create a watcher to trigger drawing of the buffer polygon
view.whenLayerView(featureLayer).then(function(layerView) {
featureLayerView = layerView;
pausableWatchHandle = watchUtils.pausable(layerView,
"updating",
function(val) {
if (!val) {
drawBufferPolygon();
}
});
// Display directions when the layerView is loading
watchUtils.whenFalseOnce(layerView, "updating", function() {
view.popup.open({
title: "Center point",
content: "Click on this point and then drag it to move the buffer.<br/> " +
"Or click on the <b>Edge</b> point and then drag it to resize the buffer.",
location: centerGraphic.geometry
});
});
});
view.when(function() {
// Display the chart in an Expand widget
cedarExpand = new Expand({
expandIconClass: "esri-icon-chart",
expandTooltip: "the illest",
expanded: true,
view: view,
content: document.getElementById("cedarPanel")
});
const search = new Search({
view: view,
resultGraphicEnabled: false,
popupEnabled: false
});
// Resume drawBufferPolygon() function; user searched for a new location
// Must update the buffer polygon and re-run the stats query
search.on("search-complete", function() {
pausableWatchHandle.resume();
});
// Legend widget
const legend = new Legend({
view: view,
layerInfos: [{
layer: featureLayer,
title: "2010 Population Density by Census tracts"
}]
});
// Display the Legend in an Expand widget
const legendExpand = new Expand({
expandTooltip: "Show Legend",
expanded: false,
view: view,
content: legend
});
// Display Instructions in an Expand widget
const instructionsExpand = new Expand({
expandIconClass: "esri-icon-question",
expandTooltip: "How to use this sample",
expanded: false,
view: view,
content: document.getElementById("instructions")
});
// Add our components to the UI
view.ui.add(cedarExpand, "bottom-left");
view.ui.add(search, "top-right");
view.ui.add(legendExpand, "bottom-right");
view.ui.add(instructionsExpand, "top-left");
});
// Close the 'help' popup when view is focused
view.watch("focused", function(newValue) {
if (newValue) {
view.popup.close();
}
});
}
/*****************************************************************
* Create SketchViewModel and wire up event listeners
*****************************************************************/
function setUpSketch() {
sketchViewModel = new SketchViewModel({
view: view,
layer: graphicsLayer
});
// Listen to SketchViewModel's move events so that population pyramid chart
// is updated as the graphics are updated
sketchViewModel.on("move-start", onMove);
sketchViewModel.on("move", onMove);
sketchViewModel.on("move-complete", onMove);
}
/*****************************************************************
* Listen to view.click event to update the center or edge graphic
*****************************************************************/
function setUpGraphicClickHandle() {
view.on("click", function(event) {
view.hitTest(event).then(function(response) {
const results = response.results;
// Loop through results returned from hitTest
results.forEach(function(result) {
// Call sketchViewModel.update method if point graphic is clicked
if (result.graphic === edgeGraphic || result.graphic ===
centerGraphic) {
// Save a reference to the current graphic being updated
activeGraphic = result.graphic;
sketchViewModel.update(result.graphic);
}
});
});
});
}
/*********************************************************************
* Edge or center graphics are being moved. Recalculate the buffer with
* updated geometry information and run the query stats again.
*********************************************************************/
function onMove(event) {
// User is moving the 'center' graphic.
// Adjust other graphics based on the movement of the center graphic
if (activeGraphic === centerGraphic) {
// Update the reference graphic's geometry
centerGraphic.geometry = event.geometry;
// Calculate location of edgeGraphic
// dx/dy values are in pixels
const edgeScreenPoint = view.toScreen(edgeGraphic.geometry);
const edgeX = edgeScreenPoint.x + event.dx;
const edgeY = edgeScreenPoint.y + event.dy;
edgeGraphic.geometry = view.toMap(edgeX, edgeY);
}
// User is moving on the 'edge' graphic.
// Resize the polyline graphic and recalculate the buffer polygon
else if (activeGraphic === edgeGraphic) {
// Update the reference graphic's geometry
edgeGraphic.geometry = event.geometry;
}
const vertices = [
[centerGraphic.geometry.x, centerGraphic.geometry.y],
[edgeGraphic.geometry.x, edgeGraphic.geometry.y]
];
// client-side stats query of features that intersect the buffer
calculateBuffer(vertices);
}
/*********************************************************************
* Edge or center point is being updated. Recalculate the buffer with
* updated geometry information.
*********************************************************************/
function calculateBuffer(vertices) {
// Update the geometry of the polyline based on location of edge and center points
polylineGraphic.geometry = new Polyline({
paths: vertices,
spatialReference: view.spatialReference
});
// Recalculate the polyline length and buffer polygon
const length = geometryEngine.geodesicLength(polylineGraphic.geometry,
unit);
const buffer = geometryEngine.geodesicBuffer(centerGraphic.geometry,
length, unit);
// Update the buffer polygon
bufferGraphic.geometry = buffer;
// Query female and male age groups of the census tracts that intersect
// the buffer polygon on the client
queryLayerViewAgeStats(buffer).then(function(newData) {
// Create a population pyramid chart from the returned result
// updateChart(newData);
});
// Update label graphic to show the length of the polyline
labelGraphic.geometry = edgeGraphic.geometry;
labelGraphic.symbol.text = length.toFixed(2) + " kilometers";
}
/*********************************************************************
* Spatial query the census tracts feature layer view for statistics
* using the updated buffer polygon.
*********************************************************************/
function queryLayerViewAgeStats(buffer) {
// Data storage for the chart
let femaleAgeData = [],
maleAgeData = [];
// Client-side spatial query:
// Get a sum of age groups for census tracts that intersect the polygon buffer
const query = featureLayerView.layer.createQuery();
query.outStatistics = statDefinitions;
query.geometry = buffer;
// Query the features on the client using FeatureLayerView.queryFeatures
return featureLayerView.queryFeatures(query).then(function(results) {
updateCedarChart(results.features);
// Statistics query returns a feature with 'stats' as attributes
const attributes = results.features[0].attributes;
// Loop through attributes and save the values for use in the population pyramid.
for (var key in attributes) {
if (key.includes("FEM")) {
femaleAgeData.push(attributes[key]);
} else {
// Make 'all male age group population' total negative so that
// data will be displayed to the left of female age group
maleAgeData.push(-Math.abs(attributes[key]));
}
}
// Return information, seperated by gender
return [femaleAgeData, maleAgeData];
})
.catch(function(error) {
console.log(error);
});
}
/***************************************************
* Draw the buffer polygon when application loads or
* when user searches for a new location
**************************************************/
// Function is called for the first time in app load.
// Function is also called from search widget's search-complete event
function drawBufferPolygon() {
// When pause() is called on the watch handle, the callback represented by the
// watch is no longer invoked, but is still available for later use
// this watch handle will be resumed when user searches for a new location
pausableWatchHandle.pause();
// Initial location for the center, edge and polylines on the view
const viewCenter = view.center.clone();
const centerScreenPoint = view.toScreen(viewCenter);
const centerPoint = view.toMap(centerScreenPoint.x + 120,
centerScreenPoint.y - 120);
const edgePoint = view.toMap(centerScreenPoint.x + 240,
centerScreenPoint.y - 120);
// Store updated vertices
const vertices = [
[centerPoint.x, centerPoint.y],
[edgePoint.x, edgePoint.y]
];
// Create center, edge, polyline and buffer graphics for the first time
if (!centerGraphic) {
const polyline = new Polyline({
paths: vertices,
spatialReference: view.spatialReference
});
// get the length of the initial polyline and create buffer
const length = geometryEngine.geodesicLength(polyline, unit);
const buffer = geometryEngine.geodesicBuffer(centerPoint, length,
unit);
// Create the graphics representing the line and buffer
centerGraphic = createGraphic(centerPoint, "center");
edgeGraphic = createGraphic(edgePoint, "handle");
polylineGraphic = createGraphic(polyline, "line");
bufferGraphic = createGraphic(buffer, "buffer");
labelGraphic = labelLength(edgePoint, length);
// Add graphics to layer
graphicsLayer.addMany([bufferGraphic, polylineGraphic,
centerGraphic, edgeGraphic, labelGraphic
]);
}
// Move the center and edge graphics to the new location returned from search
else {
centerGraphic.geometry = centerPoint;
edgeGraphic.geometry = edgePoint;
}
// Query features that intersect the buffer
calculateBuffer(vertices);
}
// Create an population pyramid chart for the census tracts that intersect the buffer polygon
function updateCedarChart (features) {
var cedarFeatures = []
for (var key in features[0].attributes) {
if (key.includes("FEM")) {
cedarFeatures.push({
attributes: {
// extract the age group from the indecipherable census attribute name
category: key.slice(3).slice(0,-9),
total: features[0].attributes[key]
}
});
}
}
// youngest to oldest
cedarFeatures.reverse();
var definition = {
type: "bar",
title: "tom is cool",
style: {
colors: ["#66eeea"]
},
datasets: [
{
data: {
features: cedarFeatures
}
}
],
series: [
{
category: {
field: "category",
label: "Age Group (Female)"
},
value: {
field: "total",
label: "Total Population"
}
}
]
};
var cedarChart = new cedar.Chart("cedarPanel", definition);
cedarChart.show();
}
// Helper function for creating graphics based on symbol type
function createGraphic(geometry, symbolType) {
let graphic = new Graphic(geometry);
switch (symbolType) {
case "handle":
graphic.attributes = {
edge: "edge"
};
graphic.symbol = {
type: "simple-marker",
style: "circle",
size: 12,
color: [255, 0, 255],
outline: {
color: [255, 255, 255],
width: 1
}
};
break;
case "center":
graphic.attributes = {
center: "center"
};
graphic.symbol = {
type: "simple-marker",
style: "circle",
size: 12,
color: "#e7903c",
outline: {
color: [255, 255, 255],
width: 1
}
};
break;
case "line":
graphic.symbol = {
type: "simple-line",
color: [254, 254, 254, 1],
width: 2.5
};
break;
default:
graphic.symbol = {
type: "simple-fill",
color: [150, 150, 150, 0.2],
outline: {
color: "#FFEB00",
width: 2
}
};
break;
}
return graphic;
}
// Helper function for formatting number labels with commas
function numberWithCommas(value) {
value = value || 0;
return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
// Label polyline with its length
function labelLength(geom, length) {
return new Graphic({
geometry: geom,
symbol: {
type: "text",
color: "#FFEB00",
text: length.toFixed(2) + " kilometers",
xoffset: 13,
yoffset: 3,
font: { // autocast as Font
size: 14,
family: "sans-serif"
}
}
});
}
});
</script>
</head>
<body>
<div id="viewDiv"></div>
<div id="cedarPanel" class="esri-widget">
</div>
<div id="instructions" class="esri-widget">
Click on the
<b>center</b> point and drag it to move the buffer. Click on the
<b>edge</b> point and drag it to resize the buffer.
</div>
</body>
</html>
Output
You can jump to the latest bin by adding /latest
to your URL
Keyboard Shortcuts
Shortcut | Action |
---|---|
ctrl + [num] | Toggle nth panel |
ctrl + 0 | Close focused panel |
ctrl + enter | Re-render output. If console visible: run JS in console |
Ctrl + l | Clear the console |
ctrl + / | Toggle comment on selected lines |
ctrl + ] | Indents selected lines |
ctrl + [ | Unindents selected lines |
tab | Code complete & Emmet expand |
ctrl + shift + L | Beautify code in active panel |
ctrl + s | Save & lock current Bin from further changes |
ctrl + shift + s | Open the share options |
ctrl + y | Archive Bin |
Complete list of JS Bin shortcuts |
JS Bin URLs
URL | Action |
---|---|
/ | Show the full rendered output. This content will update in real time as it's updated from the /edit url. |
/edit | Edit the current bin |
/watch | Follow a Code Casting session |
/embed | Create an embeddable version of the bin |
/latest | Load the very latest bin (/latest goes in place of the revision) |
/[username]/last | View the last edited bin for this user |
/[username]/last/edit | Edit the last edited bin for this user |
/[username]/last/watch | Follow the Code Casting session for the latest bin for this user |
/quiet | Remove analytics and edit button from rendered output |
.js | Load only the JavaScript for a bin |
.css | Load only the CSS for a bin |
Except for username prefixed urls, the url may start with http://jsbin.com/abc and the url fragments can be added to the url to view it differently. |