mirror of
https://github.com/heavyai/heavyai-charting.git
synced 2026-01-25 14:57:45 +00:00
446 lines
16 KiB
HTML
446 lines
16 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<title>MapD</title>
|
|
<meta charset="UTF-8">
|
|
<link rel="stylesheet" type="text/css" href="css/bootstrap.min.css" />
|
|
<link rel="stylesheet" type="text/css" href="css/mapdc.min.css" />
|
|
<link rel="stylesheet" type="text/css" href="css/chart.min.css" />
|
|
<link rel="stylesheet" type="text/css" href="css/mapbox-gl.css" />
|
|
<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="js/lodash.js"></script>
|
|
<script src="https://code.jquery.com/jquery-2.2.4.min.js" integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=" crossorigin="anonymous"></script>
|
|
<script src="js/d3.min.js"></script>
|
|
<script src="js/thrift.js"></script>
|
|
<script src="js/mapd.thrift.js"></script>
|
|
<script src="js/mapd_types.js"></script>
|
|
<script src="js/MapdCon.js"></script>
|
|
<script src="js/mapd-crossfilter.js"></script>
|
|
<script src="js/mapdc.js"></script>
|
|
<script src="js/mapbox-gl.js"></script>
|
|
<script src="js/mapboxgl-overrides.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.dataCount(".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);
|
|
/* 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 rScale = d3.scale.linear().domain([0,5000]).range([2,12]);
|
|
|
|
var pointMapChart = dc
|
|
.bubbleRasterChart(parent, true)
|
|
.con(con)
|
|
.height(h/1.5)
|
|
.width(w)
|
|
.dimension(pointMapDim)
|
|
.group(pointMapDim)
|
|
.tableName(tableName)
|
|
.mapUpdateInterval(750)
|
|
.cap(500000)
|
|
.sampling(true)
|
|
.dynamicR(d3.scale.sqrt().domain([20000,0]).range([1.0,7.0]).clamp(true))
|
|
.othersGrouper(false)
|
|
.xDim(xDim)
|
|
.yDim(yDim)
|
|
.colorBy('lang')
|
|
.r(rScale)
|
|
.defaultColor("#80DEEA")
|
|
.colors(d3.scale.ordinal().domain(langDomain).range(langColors))
|
|
.mapStyle('json/dark-v8.json')
|
|
.popupSearchRadius(2)
|
|
.popupColumns(['tweet_text', 'sender_name', 'tweet_time', 'lang', 'origin', 'followers'])
|
|
|
|
// 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
|
|
*/
|
|
|
|
function mapLangColors(n) {
|
|
langDomain = langDomain.slice(0, n);
|
|
for (var i = 0; i < langDomain.length; i++) {
|
|
langColors.push(langOriginColors[i%langOriginColors.length]);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* 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('*')
|
|
.setBinParams({
|
|
numBins: 288, // 288 * 5 = number of minutes in a day
|
|
binBounds: [timeChartBounds.minimum, timeChartBounds.maximum]
|
|
});
|
|
|
|
/* 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);
|
|
|
|
/* 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().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.renderAll();
|
|
}
|
|
});
|
|
}
|
|
|
|
function init() {
|
|
// A MapdCon instance is used for performing raw queries on a MapD GPU database.
|
|
new MapdCon()
|
|
.protocol("http")
|
|
.host("kali.mapd.com")
|
|
.port("9092")
|
|
.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>
|