Last active
August 29, 2015 14:18
-
-
Save ajturner/2a80885e4c38bf370f18 to your computer and use it in GitHub Desktop.
Demo of ArcGIS GeoJSON to Cedar + Vega
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Cedar | |
* | |
* Generic charting / visualization library for the ArcGIS Platform | |
* that leverages vega + d3 internally. | |
*/ | |
(function(window){ | |
'use strict'; | |
/** | |
* Constructor | |
* @param {object} options Cedar options | |
*/ | |
var Cedar = function Cedar(options){ | |
//close over this for use in callbacks | |
var self = this; | |
//ensure an opts object | |
var opts = options || {}; | |
/** | |
* Internals for holding state | |
*/ | |
// Array to hold event handlers | |
this._events = []; | |
//initialize the internal definition hash | |
this._definition = Cedar._defaultDefinition(); | |
//the vega view aka the chart | |
this._view = undefined; | |
//queue to hold methods called while | |
//xhrs are in progress | |
this._methodQueue=[]; | |
/** | |
* Flag used to determine if the library is | |
* waiting for an xhr to return. | |
*/ | |
this._pendingXhr = false; | |
//defintion | |
if(opts.definition){ | |
//is it an object or string(assumed to be url) | |
if(typeof opts.definition === 'object'){ | |
//hold onto the definition | |
this._definition = opts.definition; | |
}else if(typeof opts.definition === 'string' ){ | |
//assume it's a url (relative or abs) and fetch the definition object | |
this._pendingXhr = true; | |
Cedar.getJson(opts.definition, function(err,data){ | |
self._pendingXhr = false; | |
self._definition = data; | |
self._purgeMethodQueue(); | |
}); | |
}else{ | |
throw new Error('parameter definition must be an object or string (url)'); | |
} | |
} | |
if(opts.override) { | |
this._definition.override = opts.override; | |
} | |
//template | |
if(opts.specification){ | |
//is it an object or string(assumed to be url) | |
if(typeof opts.specification === 'object'){ | |
//hold onto the template | |
this._definition.specification = opts.specification; | |
}else if(typeof opts.specification === 'string' ){ | |
//assume it's a url (relative or abs) and fetch the template object | |
this._pendingXhr = true; | |
Cedar.getJson(opts.specification, function(err,data){ | |
self._pendingXhr = false; | |
self._definition.specification = data; | |
self._purgeMethodQueue(); | |
}); | |
}else{ | |
throw new Error('parameter template must be an object or string (url)'); | |
} | |
} | |
//allow a dataset to be passed in... | |
if(opts.dataset && typeof opts.dataset === 'object'){ | |
opts.dataset.query = Cedar._mixin({}, Cedar._defaultQuery(), opts.dataset.query); | |
//assign it | |
this._definition.dataset = opts.dataset; | |
} | |
/** | |
* Properties | |
* | |
* ES 5.1 syntax, so IE 8 & lower won't work | |
* | |
* If the val is a url, should we expect | |
* cedar to fetch the object? | |
* | |
* I'd say no... as that violates the principal | |
* of least suprise. Also - cedar has a .getJSON | |
* helper method the dev should use. | |
* | |
*/ | |
Object.defineProperty(this, 'dataset', { | |
get: function() { | |
return this._definition.dataset; | |
}, | |
set: function(val) { | |
this._definition.dataset = val; | |
} | |
}); | |
Object.defineProperty(this, 'specification', { | |
get: function() { | |
return this._definition.specification; | |
}, | |
set: function(val) { | |
this._definition.specification = val; | |
} | |
}); | |
Object.defineProperty(this, 'override', { | |
get: function() { | |
return this._definition.override; | |
}, | |
set: function(val) { | |
this._definition.override = val; | |
} | |
}); | |
}; | |
/** | |
* Inspect the current state of the object | |
* and determine if we have sufficient information | |
* to render the chart | |
* @return {object} Hash of the draw state + any missing requirements | |
*/ | |
Cedar.prototype.canDraw = function(){ | |
//dataset? | |
//dataset.url || dataset.data? | |
//dataset.mappings? | |
//specification? | |
//specification.template? | |
//specification.inputs? | |
//specification.inputs ~ dataset.mappings? | |
return {drawable:true, errs:[]}; | |
}; | |
/** | |
* Render a chart in the specified element | |
* @param {object} options | |
* | |
* options.elementId [required] Id of the Dom element into which the chart will be rendered | |
* options.token [optional] Token to be used if the data or spec are on a secured server | |
*/ | |
Cedar.prototype.show = function(options){ | |
if(this._pendingXhr){ | |
this._addToMethodQueue('show', [options]); | |
}else{ | |
var err; | |
//ensure we got an elementId | |
if( !options.elementId ){ | |
err= "Cedar.show requires options.elementId"; | |
} | |
//check if element exists in the page | |
if(d3.select(options.elementId)[0][0] === null){ | |
err = "Element " + options.elementId + " is not present in the DOM"; | |
} | |
//hold onto the id | |
this._elementId = options.elementId; | |
this._renderer = options.renderer || "canvas"; //default to canvas | |
//hold onto the token | |
if(options.token){ | |
this._token = options.token; | |
} | |
if( err ){ | |
throw new Error( err ); | |
} | |
var chk = this.canDraw(); | |
if(chk.drawable){ | |
//update centralizes the spec compilation & drawing | |
this.update(); | |
}else{ | |
//report the issues | |
var errs = chk.issues.join(','); | |
throw new Error('Chart can not be drawn because: ' + errs); | |
} | |
} | |
}; | |
/** | |
* Render the chart using the internal state | |
* Should be called after a user modifies the | |
* of the dataset, query, mappings or template | |
*/ | |
Cedar.prototype.update = function(){ | |
var self = this; | |
if ( this._view ) { | |
this.emit('update-start'); | |
} | |
if(this._pendingXhr){ | |
this._addToMethodQueue('update'); | |
}else{ | |
if(this._view){ | |
//remove handlers | |
//TODO Remove existing handlers | |
this._remove(this._view); | |
} | |
try{ | |
//ensure we have required inputs or defaults | |
var compiledMappings = Cedar._applyDefaultsToMappings(this._definition.dataset.mappings, this._definition.specification.inputs); //Cedar._compileMappings(this._definition.dataset, this._definition.specification.inputs); | |
var queryFromSpec = Cedar._mixin({}, this._definition.specification.query, this._definition.dataset.query); | |
queryFromSpec = JSON.parse(Cedar._supplant(JSON.stringify(queryFromSpec), compiledMappings)); | |
//allow binding to query properties | |
compiledMappings.query = queryFromSpec; | |
//compile the template + mappings --> vega spec | |
var spec = JSON.parse(Cedar._supplant(JSON.stringify(this._definition.specification.template), compiledMappings)); | |
// merge in user specified style overrides | |
spec = Cedar._mergeRecursive(spec, this._definition.override); | |
//if the spec has a url in the data node, delete it | |
if(spec.data[0].url){ | |
delete spec.data[0].url; | |
} | |
if(this._definition.dataset.data){ | |
//create the data node using the passed in data | |
spec.data[0].values = this._definition.dataset.data; | |
//send to vega | |
this._renderSpec(spec); | |
}else{ | |
//we need to fetch the data so | |
var url = Cedar._createFeatureServiceRequest(this._definition.dataset, queryFromSpec); | |
//create a callback closure to carry the spec | |
var cb = function(err,data){ | |
//todo add error handlers for xhr and ags errors | |
spec.data[0].values = data; | |
console.dir(spec); | |
//send to vega | |
self._renderSpec(spec); | |
}; | |
//fetch the data from the service | |
Cedar.getJson(url, cb); | |
} | |
} | |
catch(ex){ | |
throw(ex); | |
} | |
} | |
}; | |
/** | |
* Render a fully cooked spec | |
*/ | |
Cedar.prototype._renderSpec = function(spec){ | |
var self = this; | |
try{ | |
//use vega to parse the spec | |
//it will handle the spec as an object or url | |
vg.parse.spec(spec, function(chartCtor) { | |
//create the view | |
self._view = chartCtor({ | |
el: self._elementId, | |
renderer: self._renderer | |
}); | |
//render into the element | |
self._view.update(); | |
//attach event proxies | |
self._attach(self._view); | |
if ( self._view ) { | |
self.emit('update-end'); | |
} | |
}); | |
} | |
catch(ex){ | |
throw(ex); | |
} | |
}; | |
/** | |
* highlight marker based on attribute value | |
*/ | |
Cedar.prototype.select = function( opt ) { | |
var self = this; | |
var view = this._view; | |
var items = view.model().scene().items[0].items[0].items; | |
items.forEach(function(item) { | |
if ( item.datum.data.attributes[opt.key] === opt.value ) { | |
if ( item.hasPropertySet("hover") ) { | |
self._view.update({props:"hover", items:item}); | |
} | |
} | |
}); | |
}; | |
/** | |
* highlight marker based on attribute value | |
*/ | |
Cedar.prototype.clearSelection = function( opt ) { | |
var self = this; | |
var view = this._view; | |
var items = view.model().scene().items[0].items[0].items; | |
if ( opt && opt.key ) { | |
items.forEach(function(item) { | |
if ( item.datum.data.attributes[opt.key] === opt.value ) { | |
self._view.update({props:"update", items:item}); | |
} | |
}); | |
} else { | |
//clear all | |
self._view.update(); | |
} | |
}; | |
// trigger callback | |
Cedar.prototype.emit = function(eventName) { | |
if (this._view._handler._handlers[ eventName ]){ | |
this._view._handler._handlers[ eventName ][0].handler(); | |
} | |
}; | |
/** | |
* Attach the generic proxy handlers to the chart view | |
*/ | |
Cedar.prototype._attach = function(view){ | |
view.on('mouseover', this._handler('mouseover')); | |
view.on('mouseout', this._handler('mouseout')); | |
view.on('click', this._handler("click")); | |
view.on('update-start', this._handler('update-start')); | |
view.on('update-end', this._handler('update-end')); | |
}; | |
/** | |
* Remove all event handlers from the view | |
*/ | |
Cedar.prototype._remove = function(view){ | |
view.off('mouseover'); | |
view.off('mouseout'); | |
view.off('click'); | |
view.off('update-start'); | |
view.off('update-end'); | |
}; | |
/** | |
* Helper function that validates that the | |
* mappings hash contains values for all | |
* the inputs | |
* @param {array} inputs Array of inputs | |
* @param {object} mappings Hash of mappings | |
* @return {array} Missing mappings | |
*/ | |
Cedar._validateMappings = function(inputs, mappings){ | |
var missingInputs = [], input; | |
for(var i=0;i<inputs.length;i++){ | |
input = inputs[i]; | |
if(input.required){ | |
if(!mappings[input.name]){ | |
missingInputs.push(input.name); | |
} | |
} | |
} | |
return missingInputs; | |
}; | |
/** | |
* Validate that the incoming data has the fields expected | |
* in the mappings | |
*/ | |
Cedar._validateData = function(data, mappings){ | |
var missingInputs = []; | |
if(!data.features || !Array.isArray(data.features)){ | |
throw new Error('Data is expected to have features array!'); | |
} | |
var firstRow = data.features[0].attributes; | |
for(var key in mappings){ | |
var fld = Cedar._getMappingFieldName(key, mappings[key].field); | |
if(!firstRow.hasOwnProperty(fld)){ | |
missingInputs.push(fld); | |
} | |
} | |
return missingInputs; | |
}; | |
/** | |
* Centralize and abstract the computation of | |
* expected field names, based on the mapping name | |
*/ | |
Cedar._getMappingFieldName = function(mappingName, fieldName){ | |
var name = fieldName; | |
//if(mappingName.toLowerCase() === 'count'){ | |
// name = fieldName + '_SUM'; | |
//} | |
return name; | |
}; | |
/** | |
* Return a default definition object | |
*/ | |
Cedar._defaultDefinition = function(){ | |
var defn = { | |
"dataset": { | |
"query": this._defaultQuery() | |
}, | |
"template":{} | |
}; | |
defn.dataset.query = Cedar._defaultQuery(); | |
return defn; | |
}; | |
/** | |
* Default Query Object | |
*/ | |
Cedar._defaultQuery = function(){ | |
var defaultQuery = { | |
"where": "1=1", | |
"returnGeometry": false, | |
"returnDistinctValues": false, | |
"returnIdsOnly": false, | |
"returnCountOnly": false, | |
"outFields": "*", | |
"f": "json" | |
}; | |
return defaultQuery; | |
}; | |
/** | |
* Generic event handler proxy | |
*/ | |
Cedar.prototype._handler = function(evtName) { | |
var self = this; | |
//return a handler function w/ the events hash closed over | |
var handler = function(evt, item){ | |
self._events.forEach( function(registeredHandler){ | |
if(registeredHandler.type === evtName){ | |
//invoke the callback with the data | |
if ( item ) { | |
registeredHandler.callback(item.datum.data); | |
} else { | |
registeredHandler.callback(); | |
} | |
} | |
}); | |
}; | |
return handler; | |
}; | |
/** | |
* Add a handler for the named event | |
*/ | |
Cedar.prototype.on = function(evtName, callback){ | |
this._events.push({"type":evtName, "callback":callback}); | |
}; | |
/** | |
* Remove a handler for the named event | |
*/ | |
Cedar.prototype.off = function(evtName /*, callback */){ | |
console.log('Handler for ' + evtName +' removed...'); | |
}; | |
/** | |
* Creates an entry in the method queue, excuted | |
* once a pending xhr is completed | |
*/ | |
Cedar.prototype._addToMethodQueue = function(name, args){ | |
this._methodQueue.push({ method: name, args: args }); | |
}; | |
/** | |
* empties the method queue by calling the queued methods | |
* This helps build a more syncronous api, while still | |
* doing async things in the code | |
*/ | |
Cedar.prototype._purgeMethodQueue = function(){ | |
var self = this; | |
if(self._methodQueue.length > 0){ | |
for(var i=0;i<self._methodQueue.length;i++){ | |
var action = self._methodQueue[i]; | |
self[action.method].apply(self, action.args); | |
} | |
} | |
}; | |
/** | |
* fetch json from a url | |
* @param {string} url Url to json file | |
* @param {Function} callback node-style callback function (err, data) | |
*/ | |
Cedar.getJson = function( url, callback ){ | |
d3.json(url, function(err,data) { | |
if(err){ | |
callback('Error loading ' + url + ' ' + err.message); | |
} | |
callback(null, data); | |
}); | |
}; | |
Cedar._mixin = function(source) { | |
for (var i = 1; i < arguments.length; i++) { | |
d3.entries(arguments[i]).forEach(function(p) { | |
source[p.key] = p.value; | |
}); | |
} | |
return source; | |
}; | |
/** | |
* Given a dataset hash, create the feature service | |
* query string | |
*/ | |
Cedar._createFeatureServiceRequest = function( dataset, queryFromSpec ) { | |
var mergedQuery = Cedar._mixin({}, Cedar._defaultQuery(), queryFromSpec); | |
//Handle bbox | |
if(mergedQuery.bbox){ | |
//make sure a geometry was not also passed in | |
if(mergedQuery.geometry){ | |
throw new Error('Dataset.query can not have both a geometry and a bbox specified'); | |
} | |
//get the bbox (W,S,E,N) | |
var bboxArr = mergedQuery.bbox.split(','); | |
//remove it so it's not serialized as-is | |
delete mergedQuery.bbox; | |
//cook it into a json string | |
mergedQuery.geometry = JSON.stringify({"xmin": bboxArr[0], "ymin": bboxArr[2],"xmax": bboxArr[1], "ymax": bboxArr[3] }); | |
//set the spatial ref as geographic | |
mergedQuery.inSR = '4326'; | |
} | |
if(!mergedQuery.groupByFieldsForStatistics && dataset.mappings.group) { | |
mergedQuery.groupByFieldsForStatistics = dataset.mappings.group.field; | |
} | |
if(!mergedQuery.outStatistics && dataset.mappings.count) { | |
mergedQuery.orderByFields = dataset.mappings.count.field + "_SUM"; | |
mergedQuery.outStatistics = JSON.stringify([{"statisticType": "sum", "onStatisticField": dataset.mappings.count.field, "outStatisticFieldName": dataset.mappings.count.field + "_SUM"}]); | |
} | |
//iterate the mappings keys to check for sort | |
//----------------------------------------------------------------- | |
//This approach would seem 'clean' but if there are multiple fields | |
//to sort by, the order would be determined by how javascript decides to | |
//iterate the mappings property hash. | |
//Thus, using mappings.sort gives the developer explicit control | |
//----------------------------------------------------------------- | |
// var sort = []; | |
// for (var property in dataset.mappings) { | |
// if (dataset.mappings.hasOwnProperty(property)) { | |
// if(dataset.mappings[property].sort){ | |
// //ok - build up the sort | |
// sort.push(dataset.mappings[property].field + ' ' + dataset.mappings[property].sort); | |
// } | |
// } | |
// } | |
// if(sort.length > 0){ | |
// mergedQuery.orderByFields = sort.join(','); | |
// } | |
//----------------------------------------------------------------- | |
//check for a sort passed directly in | |
if(dataset.mappings.sort){ | |
mergedQuery.orderByFields = dataset.mappings.sort; | |
} | |
var url = dataset.url + "/query?" + this._serializeQueryParams(mergedQuery); | |
if(dataset.token){ | |
url = url + '&token=' + dataset.token; | |
} | |
return url; | |
}; | |
Cedar._applyDefaultsToMappings = function(mappings, inputs){ | |
var errs = []; | |
//loop over the inputs | |
for(var i =0; i < inputs.length; i++){ | |
//get the input | |
var input = inputs[i]; | |
//if it's required and not in the mappings, add an exception | |
if(input.required && !mappings[input.name] ){ | |
errs.push(input.name); | |
} | |
//if it's not required, has a default and not in the mappings | |
if(!input.required && !mappings[input.name] && input.default){ | |
//add the default | |
mappings[input.name] = input.default; | |
} | |
} | |
if(errs.length > 0){ | |
throw new Error('Required Mappings Missing: ' + errs.join(',')); | |
}else{ | |
return mappings; | |
} | |
}; | |
/** | |
* Token Replacement on a string | |
* @param {string} template string template | |
* @param {object} params object hash that maps to the tokens to be replaced | |
* @return {string} string with values replaced | |
*/ | |
Cedar._supplant = function( tmpl, params ){ | |
return tmpl.replace(/{([^{}]*)}/g, | |
function (a, b) { | |
var r = Cedar._getTokenValue(params, b); | |
return typeof r === 'string' || typeof r === 'number' ? r : a; | |
} | |
); | |
}; | |
/* | |
* Recursively merge properties of two objects | |
*/ | |
Cedar._mergeRecursive = function(obj1, obj2) { | |
for (var p in obj2) { | |
try { | |
// Property in destination object set; update its value. | |
if ( obj2[p].constructor===Object || obj2[p].constructor===Array) { | |
obj1[p] = Cedar._mergeRecursive(obj1[p], obj2[p]); | |
} else { | |
obj1[p] = obj2[p]; | |
} | |
} catch(e) { | |
// Property in destination object not set; create it and set its value. | |
obj1[p] = obj2[p]; | |
} | |
} | |
return obj1; | |
}; | |
/** | |
* Get the value of a token from a hash | |
* @param {[type]} tokens [description] | |
* @param {[type]} tokenName [description] | |
* @return {[type]} [description] | |
* Pulled from gulp-token-replace (MIT license) | |
* https://github.com/Pictela/gulp-token-replace/blob/master/index.js | |
* | |
*/ | |
Cedar._getTokenValue = function(tokens, tokenName) { | |
var tmpTokens = tokens; | |
var tokenNameParts = tokenName.split('.'); | |
for (var i = 0; i < tokenNameParts.length; i++) { | |
if (tmpTokens.hasOwnProperty(tokenNameParts[i])) { | |
tmpTokens = tmpTokens[tokenNameParts[i]]; | |
} else { | |
return null; | |
} | |
} | |
return tmpTokens; | |
}; | |
/** | |
* Serilize an object into a query string | |
* @param {object} params Params for the query string | |
* @return {string} query string | |
*/ | |
Cedar._serializeQueryParams = function(params) { | |
var str = []; | |
for(var p in params){ | |
if (params.hasOwnProperty(p)) { | |
var val = params[p]; | |
if (typeof val !== "string") { | |
val = JSON.stringify(val); | |
} | |
str.push(encodeURIComponent(p) + "=" + encodeURIComponent(val)); | |
} | |
} | |
var queryString = str.join("&"); | |
return queryString; | |
}; | |
window.Cedar = Cedar; | |
})(window); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
<head> | |
<meta charset=utf-8 /> | |
<title></title> | |
<meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' /> | |
<!-- Load D3 --> | |
<script type="text/javascript" src="http://square.github.io/crossfilter/d3.v3.min.js"></script> | |
<script type="text/javascript" src="http://esri.github.io/cedar/js/vega.js"></script> | |
<!-- Load Cedar | |
<script type="text/javascript" src="http://esri.github.io/cedar/js/cedar-core.js"></script> | |
--> | |
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script> | |
<script type="text/javascript" src="https://gist.githubusercontent.com/ajturner/2a80885e4c38bf370f18/raw/9250a31f9df201f0252c9d0b46be4bfbe5a4ea17/cedar-core.js"></script> | |
</head> | |
<body> | |
<div class="row"> | |
<div class="col-lg-4" id="details"></div> | |
<div class="col-lg-8" id="chart"></div> | |
</div> | |
<script type="tmpl" id="detail-template"> | |
<ul> | |
<li>Name: <%=State%></li> | |
<li>Jobs: <%=Jobs%></li> | |
</ul> | |
</script> | |
<script> | |
var chart = new Cedar(); | |
var spec = { | |
"inputs": [ | |
], | |
"template":{ | |
"width": 1000, | |
"height": 850, | |
"data": [ | |
{ | |
"name": "states", | |
"format": {"type": "json", "property": "features"}, | |
"transform": [ | |
{"type":"geopath", "value":"data.geometry", "projection":"albersUsa"} | |
] | |
} | |
], | |
"scales": [ | |
{ | |
"domain": { | |
"data": "states", | |
"field": "data.properties.Jobs" | |
}, | |
"name": "color", | |
"type": "linear", | |
"range": [ | |
"#ffefdc", | |
"#7f0101" | |
] | |
}, | |
{ | |
"name": "legend_labels", | |
"type": "ordinal", | |
"range": ["LOW", "HIGH"] | |
}, | |
], | |
"legends": [ | |
{ | |
"fill": "color", | |
"title": "Jobs", | |
"orient": "right", | |
"properties": { | |
"gradient": {"stroke": "transparent"}, | |
"labels": { | |
"text": {"scale": "legend_labels", "field": "data"} | |
} | |
} | |
} | |
], | |
"marks": [ | |
{ | |
"type": "path", | |
"from": {"data": "states"}, | |
"properties": { | |
"enter": { | |
"stroke": {"value": "#fff"}, | |
"strokeWidth": {"value": 1.25}, | |
"path": {"field": "path"}, | |
"fill": {"scale":"color", "field": "data.properties.Jobs"} | |
}, | |
"update": { | |
"stroke": {"value": "#fff"} | |
}, | |
"hover": { | |
"stroke": {"value": "#888"} | |
} | |
} | |
} | |
] | |
} | |
} | |
//assign to chart | |
chart.specification = spec; | |
var url = "http://services.arcgis.com/hRUr1F8lE8Jq2uJo/arcgis/rest/services/Enriched%20IT%20Jobs%20by%20State%20(Glassdoor)/FeatureServer/0/query?where=1%3D1&objectIds=&time=&geometry=&geometryType=esriGeometryEnvelope&inSR=&spatialRel=esriSpatialRelIntersects&distance=&units=esriSRUnit_Meter&outFields=*&returnGeometry=true&maxAllowableOffset=&geometryPrecision=&outSR=4326&returnIdsOnly=false&returnCountOnly=false&returnExtentOnly=false&orderByFields=&groupByFieldsForStatistics=&outStatistics=&resultOffset=&resultRecordCount=&returnZ=false&returnM=false&quantizationParameters=&f=pgeojson&token="; | |
var data = Cedar.getJson(url, function(err, data){ | |
console.log("data", data) | |
var dataset = { | |
"data": data, | |
"mappings":{} | |
}; | |
//assign to the chart | |
chart.dataset = dataset; | |
//show the chart | |
chart.show({ | |
elementId: "#chart" | |
}); | |
function compileTemplate(data) { | |
var tmpl = document.getElementById("detail-template").innerHTML; | |
//this uses underscore's templating library, but it could use anything | |
return _.template(tmpl)(data.properties); | |
} | |
function showItem(data){ | |
console.log("showItem", data); | |
if(data !== undefined ) { | |
document.getElementById('details').innerHTML = compileTemplate(data); | |
} | |
} | |
chart.on('mouseover', showItem); | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment