Years ago, there was a sample to visualize dynamic JSON by "joining" to a static FeatureLayer "template" by a shared pk (state, in this case). It is no longer available as the data source went offline, but here's the sample code to give you an idea of what it was doing:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no">
<title></title>
<link rel="stylesheet" href="http://js.arcgis.com/3.11/esri/css/esri.css">
<link rel="stylesheet" href="css/styles.css">
<script>var dojoConfig = {
packages: [{
name: "extras",
location: location.pathname.replace(/\/[^/]+$/, "") + "/extras"
}]
};
</script>
<script src="http://js.arcgis.com/3.11/"></script>
<script>
require([
"dojo/parser",
"dojo/json",
"dojo/_base/array",
"dojo/_base/connect",
"esri/Color",
"dojo/number",
"dojo/dom-construct",
"esri/map",
"esri/geometry/Extent",
"esri/symbols/SimpleLineSymbol",
"esri/symbols/SimpleFillSymbol",
"esri/renderers/SimpleRenderer",
"esri/renderers/ClassBreaksRenderer",
"esri/layers/FeatureLayer",
"esri/dijit/Legend",
"esri/request",
"extras/Tip",
"dijit/layout/BorderContainer",
"dijit/layout/ContentPane",
"dojo/domReady!"
], function(
parser, JSON, arr, conn, Color, number, domConstruct,
Map, Extent, SimpleLineSymbol, SimpleFillSymbol, SimpleRenderer, ClassBreaksRenderer,
FeatureLayer, Legend, esriRequest, Tip) {
// call the parser to create the dijit layout dijits
parser.parse();
var bounds = new Extent({"xmin":-2332499,"ymin":-1530060,"xmax":2252197,"ymax":1856904,"spatialReference":{"wkid":102003}});
window.map = new Map("map", {
extent: bounds,
lods: [{"level":0, "resolution": 3966, "scale": 15000000}],
slider: false
});
window.fl = new FeatureLayer("http://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer/3", {
maxAllowableOffset: window.map.extent.getWidth() / window.map.width,
mode: FeatureLayer.MODE_SNAPSHOT,
outFields: ["STATE_NAME"],
visible: true
});
// override default renderer so that states aren't drawn
// until the gas price data has been loaded and is joined
fl.setRenderer(new SimpleRenderer(null));
var template = "<strong>${STATE_NAME}: $${GAS_DISPLAY}</strong>";
window.tip = new Tip({
"format": template,
"node": "legend"
});
var updateEnd = fl.on("update-end", function() {
// get gas price data
// using apify: http://apify.heroku.com/resources
// edit the apify thing: http://apify.heroku.com/resources/53b34e28d804760002000023/edit
updateEnd.remove();
var prices = esriRequest({
// url: "http://apify.heroku.com/api/aaagasprices.json",
// Alternatively, fallback to a local file, use this if APIfy is unavailable.
url: "fallback-gas-price-data.json",
callbackParamName: "callback"
});
prices.then(drawFeatureLayer, pricesError);
// wire up the tip
fl.on("mouse-over", window.tip.showInfo);
fl.on("mouse-out", window.tip.hideInfo);
});
window.map.addLayer(fl);
function drawFeatureLayer(data) {
// If data comes back as text (which it does when coming from apify), parse it.
var gas = (typeof data === "string" ) ? JSON.parse(data) : data;
console.log("join prices, number of graphics: ", fl.graphics.length);
// loop through gas price data, find min/max and populate an object
// to keep track of the price of regular in each state
window.statePrices = {};
var gasMin = Infinity;
var gasMax = -Infinity;
arr.forEach(gas, function(g) {
if ( g.state !== "State" ) {
var price = parseFloat(parseFloat(g.regular.replace("$", "")).toFixed(2));
statePrices[g.state] = price;
if ( price < gasMin ) {
gasMin = price;
}
if ( price > gasMax ) {
gasMax = price;
}
}
});
// format max gas price with two decimal places
gasMax = formatDollars(gasMax);
// add an attribute to each attribute so gas price is displayed
// on mouse over below the legend
arr.forEach(fl.graphics, function(g) {
var displayValue = statePrices[g.attributes.STATE_NAME].toFixed(2);
g.attributes.GAS_DISPLAY = displayValue;
});
// create a class breaks renderer
var breaks = calcBreaks(gasMin, gasMax, 4);
// console.log("gas price breaks: ", breaks);
var SFS = SimpleFillSymbol;
var SLS = SimpleLineSymbol;
var outline = new SLS("solid", new Color("#444"), 1);
var br = new ClassBreaksRenderer(null, findGasPrice);
br.setMaxInclusive(true);
br.addBreak(breaks[0], breaks[1], new SFS("solid", outline, new Color([255, 255, 178, 0.75])));
br.addBreak(breaks[1], breaks[2], new SFS("solid", outline, new Color([254, 204, 92, 0.75])));
br.addBreak(breaks[2], breaks[3], new SFS("solid", outline, new Color([253, 141, 60, 0.75])));
br.addBreak(breaks[3], gasMax, new SFS("solid", outline, new Color([227, 26, 28, 0.75])));
fl.setRenderer(br);
fl.redraw();
var legend = new Legend({
map: window.map,
layerInfos: [{ "layer": fl, "title": "Regular Gas" }]
},"legend");
legend.startup();
// remove the loading div
domConstruct.destroy("loading");
}
// function used by the class breaks renderer to get the
// value used to symbolize each state
function findGasPrice(graphic) {
var state = graphic.attributes.STATE_NAME;
return statePrices[state];
}
function calcBreaks(min, max, numberOfClasses) {
var range = (max - min) / numberOfClasses;
var breakValues = [];
for ( var i = 0; i < numberOfClasses; i++ ) {
breakValues[i] = formatDollars(min + ( range * i ));
}
// console.log("break values: ", breakValues);
return breakValues;
}
function formatDollars(num) {
return number.format(num, { "places": 2 });
}
function pricesError(e) {
console.log("error getting gas price data: ", e);
}
}
);
</script>
</head>
<body>
<div id="loading" class="shadow loading">
Getting Latest Gas Price Data...
<img src="http://dl.dropbox.com/u/2654618/loading_gray_circle.gif">
</div>
<div id="legend" class="shadow info"></div>
<div data-dojo-type="dijit.layout.BorderContainer"
data-dojo-props="design:'headline',gutters:false"
style="width: 100%; height: 100%; margin: 0;">
<div id="map"
data-dojo-type="dijit.layout.ContentPane"
data-dojo-props="region:'center'">
<div id="title" class="shadow info">Current Gas Prices by State</div>
</div>
</div>
</body>
</html>
Back in 2014, we ran with this idea for our application in v3 since our maps were a tertiary consideration for a mature product, and it was the easiest way to add choropleths within an existing framework... we simply formatted the data returned by a SQL stored procedure as JSON and "joined" it to a static "template" FeatureLayer map service on the front end, manually calculating breaks along the way (we found a nice JS stats library for this), setting tooltips, etc.
Revisiting this in v4, we'd like to do something similar to Visualize data with class breaks | ArcGIS API for JavaScript 4.9 - what's the best way to accomplish this? I don't see any specific samples regarding JSON... it seems most samples either have the data in the map service, or join to tables in the workspace. This, unfortunately, is not an option for us.
Thanks!
Solved! Go to Solution.
Chris,
I am having to luck since the Classbreakrenderer does not provide a function property to due a calculation other than valueExpression (which only uses Arcade). Arcade does not seem to have access to the window.statePrices. But I found another way to do it using a FeatureLayer from graphics.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">
<title>Intro to graphics - 4.9</title>
<link rel="stylesheet" href="https://js.arcgis.com/4.9/esri/css/main.css">
<link rel="stylesheet" href="css/styles.css">
<script src="https://js.arcgis.com/4.9/"></script>
<script>
require([
"dojo/parser",
"dojo/json",
"dojo/_base/array",
"esri/Color",
"dojo/number",
"dojo/dom-construct",
"esri/Map",
"esri/views/MapView",
"esri/geometry/Extent",
"esri/symbols/SimpleLineSymbol",
"esri/symbols/SimpleFillSymbol",
"esri/renderers/ClassBreaksRenderer",
"esri/layers/FeatureLayer",
"esri/tasks/QueryTask",
"esri/tasks/support/Query",
"esri/widgets/Legend",
"esri/request",
"dijit/layout/BorderContainer",
"dijit/layout/ContentPane",
"dojo/domReady!"
], function(
parser, JSON, arr, Color, number, domConstruct,
Map, MapView, Extent, SimpleLineSymbol, SimpleFillSymbol, ClassBreaksRenderer,
FeatureLayer, QueryTask, Query, Legend, esriRequest
) {
// call the parser to create the dijit layout dijits
parser.parse();
var bounds = new Extent({"xmin":-2332499,"ymin":-1530060,"xmax":2252197,"ymax":1856904,"spatialReference":{"wkid":102003}});
map = new Map({});
view = new MapView({
container: "viewDiv",
map: map,
extent: bounds
});
var prices = esriRequest("fallback-gas-price-data.json");
prices.then(drawFeatureLayer, pricesError);
var queryTask = new QueryTask({
url: "http://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer/3"
});
var query = new Query();
query.returnGeometry = true;
query.outFields = ["STATE_NAME"];
query.where = "1=1";
function drawFeatureLayer(response) {
// If data comes back as text (which it does when coming from apify), parse it.
var gas = (typeof response.data === "string" ) ? JSON.parse(response.data) : response.data;
// loop through gas price data, find min/max and populate an object
// to keep track of the price of regular in each state
statePrices = {};
var gasMin = Infinity;
var gasMax = -Infinity;
arr.forEach(gas, function(g) {
if ( g.state !== "state" ) {
var price = parseFloat(parseFloat(g.regular.replace("$", "")).toFixed(2));
statePrices[g.state] = price;
if ( price < gasMin ) {
gasMin = price;
}
if ( price > gasMax ) {
gasMax = price;
}
}
});
// format max gas price with two decimal places
gasMax = formatDollars(gasMax);
queryTask.execute(query).then(function(results){
gColl = arr.map(results.features, function(g) {
var displayValue = statePrices[g.attributes.STATE_NAME].toFixed(2);
g.attributes.GAS_DISPLAY = statePrices[g.attributes.STATE_NAME];
return g;
});
// create a class breaks renderer
var breaks = calcBreaks(gasMin, gasMax, 4);
// console.log("gas price breaks: ", breaks);
var SFS = SimpleFillSymbol;
var SLS = SimpleLineSymbol;
var outline = {
type: "simple-line", // autocasts as new SimpleLineSymbol()
color: [0, 4, 68, 1],
width: 1,
style: "solid"
};
var br = new ClassBreaksRenderer({
field: "GAS_DISPLAY",
defaultSymbol: null,
classBreakInfos: [
{
minValue: breaks[0],
maxValue: breaks[1],
symbol: {
type: "simple-fill", // autocasts as new SimpleFillSymbol()
color: [ 255, 255, 178, 0.75 ],
style: "solid",
outline: outline
}
}, {
minValue: breaks[1],
maxValue: breaks[2],
symbol: {
type: "simple-fill", // autocasts as new SimpleFillSymbol()
color: [ 254, 204, 92, 0.75 ],
style: "solid",
outline: outline
}
}, {
minValue: breaks[2],
maxValue: breaks[3],
symbol: {
type: "simple-fill", // autocasts as new SimpleFillSymbol()
color: [ 253, 141, 60, 0.75 ],
style: "solid",
outline: outline
}
}, {
minValue: breaks[3],
maxValue: gasMax,
symbol: {
type: "simple-fill", // autocasts as new SimpleFillSymbol()
color: [ 227, 26, 28, 0.75 ],
style: "solid",
outline: outline
}
}
]
});
var fields = [
{
name: "ObjectID",
alias: "ObjectID",
type: "oid"
},
{
name: "STATE_NAME",
alias: "State Name",
type: "string"
}, {
name: "GAS_DISPLAY",
alias: "Gas Price",
type: "double"
}
];
var pTemplate = {
title: "Average Gas Price",
content: "<strong>{STATE_NAME}: ${GAS_DISPLAY}</strong>",
fieldInfos: [{
fieldName: "GAS_DISPLAY",
label: "Gas Price",
format: {
places: 2,
digitSeparator: true
}
}]
};
gassfl = new FeatureLayer({
source: gColl,
fields: fields,
objectIdField: "ObjectID",
renderer: br,
visible: true,
popupTemplate: pTemplate
});
map.add(gassfl);
cState = null;
var legend = new Legend({
view: view,
layerInfos: [{ "layer":gassfl, "title": "Regular Gas" }],
container:"legend"
});
// remove the loading div
domConstruct.destroy("loading");
view.on("pointer-move", eventHandler);
view.on("pointer-down", eventHandler);
function eventHandler(event) {
view.hitTest(event)
.then(getGraphics);
}
function getGraphics(response) {
if (response.results.length) {
var graphic = response.results.filter(function(result) {
return result.graphic.layer === gassfl;
})[0].graphic;
if(graphic.attributes.STATE_NAME !== cState){
view.popup.open({
features: [graphic],
location: graphic.geometry.extent.center
});
cState = graphic.attributes.STATE_NAME;
}
}else{
view.popup.clear();
view.popup.close();
}
}
});
}
function calcBreaks(min, max, numberOfClasses) {
var range = (max - min) / numberOfClasses;
var breakValues = [];
for ( var i = 0; i < numberOfClasses; i++ ) {
breakValues[i] = formatDollars(min + ( range * i ));
}
// console.log("break values: ", breakValues);
return breakValues;
}
function formatDollars(num) {
return number.format(num, { "places": 2 });
}
function pricesError(e) {
console.log("error getting gas price data: ", e);
}
}
);
</script>
</head>
<body>
<div id="loading" class="shadow loading">
Getting Latest Gas Price Data...
<img src="images/loading_gray_circle.gif">
</div>
<div id="legend" class="shadow info"></div>
<div data-dojo-type="dijit/layout/BorderContainer"
data-dojo-props="design:'headline',gutters:false"
style="width: 100%; height: 100%; margin: 0;">
<div id="viewDiv"
data-dojo-type="dijit/layout/ContentPane"
data-dojo-props="region:'center'">
<div id="title" class="shadow info">Current Gas Prices by State</div>
</div>
</div>
</body>
</html>
Chris,
Do you have the fallback-gas-price-data.json file? If so can you attach it? I have most of the code converted over to 4.9 but I need the json file.
Thanks, Robert! I'm checking now, but it's possible I no longer have the failover file - it's been so long. I will look into replicating with some test data, if that is the case.
Having test data is what I need or a sample of your data would work as well.
Robert,
I was unable to find the original test JSON for this sample, so I went ahead and created a new file (see attached - fallback-gas-price-data.json.zip). It looks like the sample simply had "state" and "regular" as fields and used state name as the pk.
Thanks!
Chris,
I am having to luck since the Classbreakrenderer does not provide a function property to due a calculation other than valueExpression (which only uses Arcade). Arcade does not seem to have access to the window.statePrices. But I found another way to do it using a FeatureLayer from graphics.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">
<title>Intro to graphics - 4.9</title>
<link rel="stylesheet" href="https://js.arcgis.com/4.9/esri/css/main.css">
<link rel="stylesheet" href="css/styles.css">
<script src="https://js.arcgis.com/4.9/"></script>
<script>
require([
"dojo/parser",
"dojo/json",
"dojo/_base/array",
"esri/Color",
"dojo/number",
"dojo/dom-construct",
"esri/Map",
"esri/views/MapView",
"esri/geometry/Extent",
"esri/symbols/SimpleLineSymbol",
"esri/symbols/SimpleFillSymbol",
"esri/renderers/ClassBreaksRenderer",
"esri/layers/FeatureLayer",
"esri/tasks/QueryTask",
"esri/tasks/support/Query",
"esri/widgets/Legend",
"esri/request",
"dijit/layout/BorderContainer",
"dijit/layout/ContentPane",
"dojo/domReady!"
], function(
parser, JSON, arr, Color, number, domConstruct,
Map, MapView, Extent, SimpleLineSymbol, SimpleFillSymbol, ClassBreaksRenderer,
FeatureLayer, QueryTask, Query, Legend, esriRequest
) {
// call the parser to create the dijit layout dijits
parser.parse();
var bounds = new Extent({"xmin":-2332499,"ymin":-1530060,"xmax":2252197,"ymax":1856904,"spatialReference":{"wkid":102003}});
map = new Map({});
view = new MapView({
container: "viewDiv",
map: map,
extent: bounds
});
var prices = esriRequest("fallback-gas-price-data.json");
prices.then(drawFeatureLayer, pricesError);
var queryTask = new QueryTask({
url: "http://sampleserver6.arcgisonline.com/arcgis/rest/services/Census/MapServer/3"
});
var query = new Query();
query.returnGeometry = true;
query.outFields = ["STATE_NAME"];
query.where = "1=1";
function drawFeatureLayer(response) {
// If data comes back as text (which it does when coming from apify), parse it.
var gas = (typeof response.data === "string" ) ? JSON.parse(response.data) : response.data;
// loop through gas price data, find min/max and populate an object
// to keep track of the price of regular in each state
statePrices = {};
var gasMin = Infinity;
var gasMax = -Infinity;
arr.forEach(gas, function(g) {
if ( g.state !== "state" ) {
var price = parseFloat(parseFloat(g.regular.replace("$", "")).toFixed(2));
statePrices[g.state] = price;
if ( price < gasMin ) {
gasMin = price;
}
if ( price > gasMax ) {
gasMax = price;
}
}
});
// format max gas price with two decimal places
gasMax = formatDollars(gasMax);
queryTask.execute(query).then(function(results){
gColl = arr.map(results.features, function(g) {
var displayValue = statePrices[g.attributes.STATE_NAME].toFixed(2);
g.attributes.GAS_DISPLAY = statePrices[g.attributes.STATE_NAME];
return g;
});
// create a class breaks renderer
var breaks = calcBreaks(gasMin, gasMax, 4);
// console.log("gas price breaks: ", breaks);
var SFS = SimpleFillSymbol;
var SLS = SimpleLineSymbol;
var outline = {
type: "simple-line", // autocasts as new SimpleLineSymbol()
color: [0, 4, 68, 1],
width: 1,
style: "solid"
};
var br = new ClassBreaksRenderer({
field: "GAS_DISPLAY",
defaultSymbol: null,
classBreakInfos: [
{
minValue: breaks[0],
maxValue: breaks[1],
symbol: {
type: "simple-fill", // autocasts as new SimpleFillSymbol()
color: [ 255, 255, 178, 0.75 ],
style: "solid",
outline: outline
}
}, {
minValue: breaks[1],
maxValue: breaks[2],
symbol: {
type: "simple-fill", // autocasts as new SimpleFillSymbol()
color: [ 254, 204, 92, 0.75 ],
style: "solid",
outline: outline
}
}, {
minValue: breaks[2],
maxValue: breaks[3],
symbol: {
type: "simple-fill", // autocasts as new SimpleFillSymbol()
color: [ 253, 141, 60, 0.75 ],
style: "solid",
outline: outline
}
}, {
minValue: breaks[3],
maxValue: gasMax,
symbol: {
type: "simple-fill", // autocasts as new SimpleFillSymbol()
color: [ 227, 26, 28, 0.75 ],
style: "solid",
outline: outline
}
}
]
});
var fields = [
{
name: "ObjectID",
alias: "ObjectID",
type: "oid"
},
{
name: "STATE_NAME",
alias: "State Name",
type: "string"
}, {
name: "GAS_DISPLAY",
alias: "Gas Price",
type: "double"
}
];
var pTemplate = {
title: "Average Gas Price",
content: "<strong>{STATE_NAME}: ${GAS_DISPLAY}</strong>",
fieldInfos: [{
fieldName: "GAS_DISPLAY",
label: "Gas Price",
format: {
places: 2,
digitSeparator: true
}
}]
};
gassfl = new FeatureLayer({
source: gColl,
fields: fields,
objectIdField: "ObjectID",
renderer: br,
visible: true,
popupTemplate: pTemplate
});
map.add(gassfl);
cState = null;
var legend = new Legend({
view: view,
layerInfos: [{ "layer":gassfl, "title": "Regular Gas" }],
container:"legend"
});
// remove the loading div
domConstruct.destroy("loading");
view.on("pointer-move", eventHandler);
view.on("pointer-down", eventHandler);
function eventHandler(event) {
view.hitTest(event)
.then(getGraphics);
}
function getGraphics(response) {
if (response.results.length) {
var graphic = response.results.filter(function(result) {
return result.graphic.layer === gassfl;
})[0].graphic;
if(graphic.attributes.STATE_NAME !== cState){
view.popup.open({
features: [graphic],
location: graphic.geometry.extent.center
});
cState = graphic.attributes.STATE_NAME;
}
}else{
view.popup.clear();
view.popup.close();
}
}
});
}
function calcBreaks(min, max, numberOfClasses) {
var range = (max - min) / numberOfClasses;
var breakValues = [];
for ( var i = 0; i < numberOfClasses; i++ ) {
breakValues[i] = formatDollars(min + ( range * i ));
}
// console.log("break values: ", breakValues);
return breakValues;
}
function formatDollars(num) {
return number.format(num, { "places": 2 });
}
function pricesError(e) {
console.log("error getting gas price data: ", e);
}
}
);
</script>
</head>
<body>
<div id="loading" class="shadow loading">
Getting Latest Gas Price Data...
<img src="images/loading_gray_circle.gif">
</div>
<div id="legend" class="shadow info"></div>
<div data-dojo-type="dijit/layout/BorderContainer"
data-dojo-props="design:'headline',gutters:false"
style="width: 100%; height: 100%; margin: 0;">
<div id="viewDiv"
data-dojo-type="dijit/layout/ContentPane"
data-dojo-props="region:'center'">
<div id="title" class="shadow info">Current Gas Prices by State</div>
</div>
</div>
</body>
</html>