mirror of
https://github.com/heavyai/heavyai-charting.git
synced 2026-01-25 14:57:45 +00:00
* geoheatmap raster layer set mapbox token in examples, set cap in example 3, raise poly point cap in other examples (#92) include all css and js files in one bundle bump tp webpack 2 begin raster heatmap example basic heatmap raster layer mixin fix lint xyDim getter/setter for heatmap raster layer raster heatmap layer example with sliders example to use distinct lang refactor use mapd data layer use webpack dev server for examples update example build script calculate proper gap size update geoheatmap example image update example image styling dynamic number of bins on geoheatmap vega-lite api for heat layer implement hex binning update aggregate ability to switch mark types update babelrx manual pixel sizing add destroy hook dynamic color domain linting use layer.genSQL in genVega handle aggregation parsing rebuild do not set null colordomain fix test update bundle fix test * tweak example
443 lines
16 KiB
HTML
443 lines
16 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<title>MapD</title>
|
|
<meta charset="UTF-8">
|
|
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
|
|
<link href='https://fonts.googleapis.com/css?family=Roboto:400,700,300' rel='stylesheet' type='text/css'>
|
|
<style>
|
|
.title {
|
|
font-weight: bold;
|
|
text-align:center;
|
|
}
|
|
.mapd {
|
|
position: relative;
|
|
top: 2px;
|
|
}
|
|
.search{
|
|
display: inline-block;
|
|
margin-top: 12px;
|
|
margin-left: 50px;
|
|
}
|
|
.data-count {
|
|
padding-right:20px;
|
|
}
|
|
.filter-count {
|
|
font-weight: bold;
|
|
color: #45B1E8;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<nav class="navbar navbar-default">
|
|
<div class="container-fluid">
|
|
<div class="navbar-header" style="margin-top:10px">
|
|
<img alt="Brand" src="images/favicon.png" height="30" width="30">
|
|
<span class="mapd">MapD Demo</span>
|
|
</div>
|
|
<div class='search'>Filter Term(s): <small>use a comma between terms to search multiple values</small>
|
|
<input class='search-bar'>
|
|
</div>
|
|
<div class="navbar-text navbar-right">
|
|
<div class="data-count">
|
|
<span class="total"><span><span class="filter-count"></span></span>
|
|
<span>of <span class="total-count"></span> tweets</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="main-container">
|
|
<div class="col-xs-12">
|
|
<div class="title">Point Map with Backend Rendering</div>
|
|
<div id="chart1-example"></div>
|
|
</div>
|
|
|
|
<div class="col-xs-12">
|
|
<div class="title"># of Tweets per 5 Minutes</div>
|
|
<div class="chart2-example"></div>
|
|
</div>
|
|
</div>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
|
|
<script src="https://code.jquery.com/jquery-2.2.4.min.js" integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=" crossorigin="anonymous"></script>
|
|
<script src="assets/app.bundle.js"></script>
|
|
<script>
|
|
/*
|
|
* This is example code that shows how to make 3 cross-filtered charts with the
|
|
* mapdc.js API. This example is not meant to be a replacement for dc.js
|
|
* documentation. For the dc.js API docs, see here
|
|
* - https://github.com/dc-js/dc.js/blob/master/web/docs/api-latest.md.
|
|
* For an annotated example of using dc.js - see here:
|
|
* https://dc-js.github.io/dc.js/docs/stock.html.
|
|
*/
|
|
var globalGeocoder = null;
|
|
|
|
function createCharts(crossFilter, con, tableName) {
|
|
var w = document.documentElement.clientWidth - 30;
|
|
var h = Math.max(document.documentElement.clientHeight, window.innerHeight || 0) - 200;
|
|
|
|
/*---------------------BASIC COUNT ON CROSSFILTER--------------------------*/
|
|
/*
|
|
* A basic operation is getting the filtered count and total count
|
|
* of crossFilter. This performs that operation. It is built into DC.
|
|
* Note that for the count we use crossFilter itself as the dimension.
|
|
*/
|
|
var countGroup = crossFilter.groupAll();
|
|
var dataCount = dc.countWidget(".data-count")
|
|
.dimension(crossFilter)
|
|
.group(countGroup);
|
|
|
|
/*----------------BACKEND RENDERED POINT MAP EXAMPLE-----------------------*/
|
|
var langDomain = ['en', 'pt', 'es', 'in', 'und', 'ja', 'tr', 'fr', 'tl', 'ru', 'ar', 'th', 'it', 'nl', 'sv', 'ht', 'de', 'et', 'pl', 'sl', 'ko', 'fi', 'lv', 'sk', 'uk', 'da', 'zh', 'ro', 'no', 'cy', 'iw', 'hu', 'bg', 'lt', 'bs', 'vi', 'el', 'is', 'hi', 'hr', 'fa', 'ur', 'ne', 'ta', 'sr', 'bn', 'si', 'ml', 'hy', 'lo', 'iu', 'ka', 'ps', 'te', 'pa', 'am', 'kn', 'chr', 'my', 'gu', 'ckb', 'km', 'ug', 'sd', 'bo', 'dv'];
|
|
var langOriginColors = ["#27aeef", "#ea5545", "#87bc45", "#b33dc6", "#f46a9b", "#ede15b", "#bdcf32", "#ef9b20", "#4db6ac", "#edbf33", "#7c4dff"]
|
|
var langColors = [];
|
|
var pointMapDim = crossFilter.dimension(null).projectOn(["conv_4326_900913_x(lon) as x", "conv_4326_900913_y(lat) as y", "lang as color", "followers as size"]);
|
|
var xDim = crossFilter.dimension("lon");
|
|
var yDim = crossFilter.dimension("lat");
|
|
var parent = document.getElementById("chart1-example");
|
|
mapLangColors(40);
|
|
var mapboxToken = "pk.eyJ1IjoibWFwZCIsImEiOiJjaWV1a3NqanYwajVsbmdtMDZzc2pneDVpIn0.cJnk8c2AxdNiRNZWtx5A9g";
|
|
/* Point Map Radius Size:
|
|
* in order to calculate the radius size. We use d3 scale and pass in a
|
|
* domain and range.
|
|
|
|
To learn more about d3 scales, please read this:
|
|
https://github.com/d3/d3-scale
|
|
|
|
We then pass this scale into the r function within bubbleRasterChart
|
|
*/
|
|
var sizeScale = d3.scale.linear().domain([0,5000]).range([1,5]);
|
|
|
|
var pointMapChart = dc
|
|
.rasterChart(parent, true)
|
|
.con(con)
|
|
.height(h/1.5)
|
|
.width(w)
|
|
.mapUpdateInterval(750)
|
|
.mapStyle('json/dark-v8.json')
|
|
.mapboxToken(mapboxToken) // need a mapbox accessToken for loading the tiles
|
|
|
|
var pointLayer = dc
|
|
.rasterLayer("points")
|
|
.dimension(pointMapDim)
|
|
.group(pointMapDim)
|
|
.cap(500000)
|
|
.sampling(true)
|
|
.sizeAttr("size")
|
|
.dynamicSize(d3.scale.sqrt().domain([20000,0]).range([1.0,7.0]).clamp(true))
|
|
.sizeScale(sizeScale)
|
|
.xAttr("x")
|
|
.yAttr("y")
|
|
.xDim(xDim)
|
|
.yDim(yDim)
|
|
.fillColorAttr("color")
|
|
.defaultFillColor("#80DEEA")
|
|
.fillColorScale(d3.scale.ordinal().domain(langDomain).range(langColors))
|
|
.popupColumns(['tweet_text', 'sender_name', 'tweet_time', 'lang', 'origin', 'followers'])
|
|
|
|
function mapLangColors(n) {
|
|
langDomain = langDomain.slice(0, n);
|
|
for (var i = 0; i < langDomain.length; i++) {
|
|
langColors.push(langOriginColors[i%langOriginColors.length]);
|
|
}
|
|
}
|
|
|
|
pointMapChart.pushLayer("points", pointLayer).init().then((chart) => {
|
|
// custom click handler with just event data (no network calls)
|
|
pointMapChart.map().on('mouseup', logClick)
|
|
function logClick (result) {
|
|
console.log("clicked!", result)
|
|
}
|
|
// disable with pointMapChart.map().off('mouseup', logClick)
|
|
|
|
// custom click handler with event and nearest row data
|
|
pointMapChart.map().on('mouseup', logClickWithData)
|
|
function logClickWithData (event) {
|
|
pointMapChart.getClosestResult(event.point, function(result){
|
|
console.log(result && result.row_set[0])
|
|
})
|
|
}
|
|
|
|
// hover effect with popup
|
|
var debouncedPopup = _.debounce(displayPopupWithData, 250)
|
|
pointMapChart.map().on('mousewheel', pointMapChart.hidePopup);
|
|
pointMapChart.map().on('mousemove', pointMapChart.hidePopup)
|
|
pointMapChart.map().on('mousemove', debouncedPopup)
|
|
function displayPopupWithData (event) {
|
|
pointMapChart.getClosestResult(event.point, pointMapChart.displayPopup)
|
|
}
|
|
|
|
initGeocoder(pointMapChart);
|
|
|
|
/* Find additional mapbox styles here:
|
|
*
|
|
* https://github.com/mapbox/mapbox-gl-styles/tree/master/styles
|
|
*/
|
|
|
|
/*
|
|
* Callback used for JSONP request for Google's Geocoder
|
|
*/
|
|
function mapApiLoaded() {
|
|
globalGeocoder = new google.maps.Geocoder();
|
|
geocoderObject.geocoder = globalGeocoder;
|
|
}
|
|
|
|
/*
|
|
* Style and position your geocoder textbox here using standard jQuery
|
|
* Also, set any event listeners you'd like.
|
|
*/
|
|
function initGeocoder (chart) {
|
|
this.geocoder = new Geocoder(chart);
|
|
this.geocoder.init(chart.map());
|
|
this.geocoderInput = $('<input class="geocoder-input" type="text" placeholder="Zoom to"></input>').appendTo($("#chart1-example"));
|
|
this.geocoderInput.css({ top: '5px', right: '5px', float: 'right', position: 'absolute'});
|
|
|
|
// set a key-up event handler for the enter key
|
|
this.geocoderInput.keyup(function(e) {
|
|
if(e.keyCode == 13) { this.geocoder.geocode(this.geocoderInput.val()); }
|
|
}.bind(this));
|
|
}
|
|
|
|
/*
|
|
* The Geocoder wrapper for Google Maps' geocoder.
|
|
* Has an ultra-small API that simply allows for geocoding a placeName.
|
|
*/
|
|
function Geocoder(chart) {
|
|
this.geocoder = null;
|
|
|
|
this.geocode = function(placeName) {
|
|
this.geocoder.geocode({ 'address': placeName }, this._onResult);
|
|
}
|
|
|
|
this._onResult = function(data, status) {
|
|
if (status != google.maps.GeocoderStatus.OK) {
|
|
//throw "Geocoder error";
|
|
return null;
|
|
}
|
|
var viewport = data[0].geometry.viewport;
|
|
var sw = viewport.getSouthWest();
|
|
var ne = viewport.getNorthEast();
|
|
|
|
chart.map().fitBounds([ // api specifies lng/lat pairs
|
|
[sw.lng(), sw.lat()],
|
|
[ne.lng(), ne.lat()]
|
|
], { animate: false }); // set animate to true if you want to pan-and-zoom to the location
|
|
|
|
}
|
|
|
|
this.init = function() {
|
|
if (globalGeocoder == null) { // have to give global callback for when the loaded google js is executed
|
|
geocoderObject = this;
|
|
$.getScript("https://maps.google.com/maps/api/js?sensor=false&async=2&callback=mapApiLoaded", function() {});
|
|
}
|
|
else {
|
|
this.geocoder = globalGeocoder;
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/*---------------------TIME CHART EXAMPLE----------------------------------*/
|
|
|
|
/*
|
|
* First we want to determine the extent (min,max) of the time variable so we
|
|
* can set the bounds on the time chart appropriately.
|
|
*
|
|
* If you know the bounds a priori you can do this manually but here we will
|
|
* do it dymaically via a query sent to the backend through the crossfilter
|
|
* api.
|
|
*
|
|
* We create a reduceMulti expression that will get the min and max of the
|
|
* variable dep_timestamp.
|
|
*
|
|
*/
|
|
|
|
var timeChartMeasures = [
|
|
{
|
|
expression: "tweet_time",
|
|
agg_mode:"min",
|
|
name: "minimum"
|
|
},
|
|
{
|
|
expression: "tweet_time",
|
|
agg_mode:"max",
|
|
name: "maximum"}
|
|
]
|
|
|
|
/* Note than when we are doing aggregations over the entire dataset we use
|
|
* the crossfilter object itself as the dimension with the groupAll method
|
|
*
|
|
* values(true) gets the values for our groupAll measure (here min and max
|
|
* of dep_timestamp) - true means to ignore currently set filters - i.e.
|
|
* get a global min and max
|
|
*/
|
|
|
|
crossFilter
|
|
.groupAll()
|
|
.reduce(timeChartMeasures)
|
|
.valuesAsync(true).then(function(timeChartBounds) {
|
|
|
|
var timeChartDimension = crossFilter.dimension("tweet_time");
|
|
|
|
/* We would like to bin or histogram the time values. We do this by
|
|
* invoking setBinParams on the group. Here we are asking for 400 equal
|
|
* sized bins from the min to the max of the time range
|
|
*/
|
|
|
|
var timeChartGroup = timeChartDimension
|
|
.group()
|
|
.reduceCount('*')
|
|
|
|
/* We create the time chart as a line chart
|
|
* with the following parameters:
|
|
*
|
|
* Width and height - as above
|
|
*
|
|
* elasticY(true) - cause the y-axis to scale as filters are changed
|
|
*
|
|
* renderHorizontalGridLines(true) - add grid lines to the chart
|
|
*
|
|
* brushOn(true) - Request a filter brush to be added to the chart - this
|
|
* will allow users to drag a filter window along the time chart and filter
|
|
* the rest of the data accordingly
|
|
*
|
|
*/
|
|
|
|
var dcTimeChart = dc.lineChart('.chart2-example')
|
|
.width(w)
|
|
.height(h/2.5)
|
|
.elasticY(true)
|
|
.renderHorizontalGridLines(true)
|
|
.brushOn(true)
|
|
.xAxisLabel('Time of Day')
|
|
.yAxisLabel('Number of Tweets')
|
|
.dimension(timeChartDimension)
|
|
.group(timeChartGroup)
|
|
.binParams({
|
|
numBins: 288, // 288 * 5 = number of minutes in a day
|
|
binBounds: [timeChartBounds.minimum, timeChartBounds.maximum]
|
|
});
|
|
|
|
/* Set the x and y axis formatting with standard d3 functions */
|
|
|
|
dcTimeChart
|
|
.x(d3.time.scale.utc().domain([timeChartBounds.minimum, timeChartBounds.maximum]))
|
|
.yAxis().ticks(5);
|
|
|
|
dcTimeChart
|
|
.xAxis()
|
|
.scale(dcTimeChart.x())
|
|
.tickFormat(dc.utils.customTimeFormat)
|
|
.orient('top');
|
|
|
|
|
|
/*---------------------SET UP FILTER ----------------------------------*/
|
|
|
|
/* We create the filter
|
|
* with the following parameters:
|
|
*
|
|
* Dimension: What column you would like to search.
|
|
*
|
|
* OPTIONAL: setDrillDownFilter: Boolean
|
|
* if set to true, your search will "AND" for all values, instead of
|
|
* the default "OR" for all values.
|
|
*
|
|
* eg. If searching "Keyboard Cat":
|
|
* This will only only return results where the tweet contains:
|
|
* "Keyboard" AND "Cat"
|
|
*
|
|
*/
|
|
var searchFilterTweets = crossFilter.dimension("tweet_tokens")
|
|
.setDrillDownFilter(true);
|
|
|
|
function createMultiFilterArray(search) {
|
|
|
|
// Empty Search
|
|
if (search == "") {
|
|
return undefined;
|
|
}
|
|
|
|
return search.split(',').map(function(searchValue) {
|
|
return searchValue.trim();
|
|
});
|
|
}
|
|
|
|
$('.search-bar').keypress(function(e){
|
|
if (e.keyCode === 13) {
|
|
var mutipleFilterArray = createMultiFilterArray(e.target.value);
|
|
if (mutipleFilterArray) {
|
|
searchFilterTweets.filterMulti(mutipleFilterArray);
|
|
} else {
|
|
// Clears all filters
|
|
searchFilterTweets.filter();
|
|
}
|
|
// You must call redrawAll after applying custom filters.
|
|
dc.redrawAllAsync();
|
|
}
|
|
})
|
|
|
|
/* Calling dc.renderAll() will render all of the charts we set up. Any
|
|
crossFilters applied by the user (via clicking the bar chart, scatter plot or dragging the time brush) will automagically call redraw on the charts without any intervention from us
|
|
*/
|
|
|
|
dc.renderAllAsync()
|
|
|
|
/*--------------------------RESIZE EVENT------------------------------*/
|
|
|
|
/* Here we listen to any resizes of the main window. On resize we resize the corresponding widgets and call dc.renderAll() to refresh everything */
|
|
|
|
window.addEventListener("resize", _.debounce(reSizeAll, 500));
|
|
|
|
function reSizeAll(){
|
|
|
|
var w = document.documentElement.clientWidth - 30;
|
|
var h = Math.max(document.documentElement.clientHeight, window.innerHeight || 0) - 200;
|
|
|
|
pointMapChart.map().resize();
|
|
pointMapChart.isNodeAnimate = false;
|
|
pointMapChart
|
|
.width(w)
|
|
.height(h/1.5)
|
|
.render();
|
|
dcTimeChart
|
|
.width(w)
|
|
.height(h/2.5)
|
|
|
|
dc.redrawAllAsync();
|
|
}
|
|
});
|
|
})
|
|
}
|
|
|
|
function init() {
|
|
// A MapdCon instance is used for performing raw queries on a MapD GPU database.
|
|
new MapdCon()
|
|
.protocol("https")
|
|
.host("metis.mapd.com")
|
|
.port("443")
|
|
.dbName("mapd")
|
|
.user("mapd")
|
|
.password("HyperInteractive")
|
|
.connect(function(error, con) {
|
|
// Get a table from the database
|
|
var tableName = 'tweets_nov_feb';
|
|
// A CrossFilter instance is used for generating the raw query strings for your MapdCon.
|
|
var crossFilter = crossfilter.crossfilter(con, tableName)
|
|
.then(function(cf) {
|
|
createCharts(cf, con, tableName)
|
|
})
|
|
// Pass instance of crossfilter into our CreateCharts.
|
|
;
|
|
});
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', init, false);
|
|
|
|
function mapApiLoaded() {
|
|
globalGeocoder = new google.maps.Geocoder();
|
|
geocoderObject.geocoder = globalGeocoder;
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|