mirror of
https://github.com/heavyai/heavyai-charting.git
synced 2026-01-25 14:57:45 +00:00
* Adding new rasterChart mapdc widget with multi-layer BE-rendering support.
The raster chart is a wrapper covering all 2D backend-rendering widgets.
This currently includes pointmaps and scatterplots. Changes include:
1) new point and poly layer mixins that can be added to rasterChart
2) Adds hit-testing support for all layers, including polys. This includes animations and popups. Requires some new css.
3) fixed bug to ensure pixel radius set to getResultRowForPixel is an int
4) several small changes to crossfilter:
a) each crossfilter object is given a unique id
b) added a getProjectOn() function to groups in order to retrieve the projections in a join/group-by query.
c) added flags to extract the sql from certain dimension/group functions rather than run the sql immediately.
5) Change dc-group-all-mixin to cache the last count by crossfilter id.
* adding multi-layer backend rendering examples for maps and scatterplots
332 lines
18 KiB
JavaScript
332 lines
18 KiB
JavaScript
/*
|
|
* This is example code that shows how to make a map widget that backend
|
|
* renders multiple layers.
|
|
*/
|
|
|
|
document.addEventListener("DOMContentLoaded", 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) {
|
|
// Tables for the first layer of the pointmap.
|
|
// This layer will be polygons of zipcodes and
|
|
// will be colored by data joined from the contributions
|
|
// table
|
|
var tableName1 = ["contributions", "zipcodes"];
|
|
var table1Joins = [{
|
|
table1: "contributions",
|
|
attr1: "contributor_zipcode",
|
|
table2: "zipcodes",
|
|
attr2: "ZCTA5CE10"
|
|
}];
|
|
// Table to use for the 2nd layer, which will be points
|
|
// from a tweets table.
|
|
var tableName2 = 'tweets_nov_feb';
|
|
|
|
// Table to use for the 3nd layer, which will be points
|
|
// from the contributions table.
|
|
var tableName3 = 'contributions';
|
|
|
|
// make 3 crossfilters for all 3 layers
|
|
// A CrossFilter instance is used for generating the raw query strings for your MapdCon.
|
|
|
|
// first layer
|
|
var crossFilter = crossfilter.crossfilter(con, tableName1, table1Joins).then(function(cf1) {
|
|
|
|
// second layer
|
|
var crossFilter = crossfilter.crossfilter(con, tableName2).then(function(cf2) {
|
|
|
|
// third layer
|
|
var crossFilter = crossfilter.crossfilter(con, tableName3).then(function(cf3) {
|
|
createPointMap(cf1, cf2, cf3, con)
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
// function to create the backend-rendered map.
|
|
function createPointMap(polycfLayer1, pointcfLayer2, pointcfLayer3, con) {
|
|
var w = document.documentElement.clientWidth - 30;
|
|
var h = Math.max(document.documentElement.clientHeight, window.innerHeight || 0) - 200;
|
|
|
|
/*---------------------BASIC COUNT ON CROSSFILTER--------------------------*/
|
|
/*
|
|
* Adding a basic count of the point layers using crossfilter.
|
|
* Note that for the count we use crossFilter itself as the dimension.
|
|
*/
|
|
var countGroup1 = pointcfLayer2.groupAll();
|
|
var dataCount1 = dc.dataCount(".data-count1")
|
|
.dimension(pointcfLayer2)
|
|
.group(countGroup1);
|
|
|
|
var countGroup2 = pointcfLayer3.groupAll();
|
|
var dataCount2 = dc.dataCount(".data-count2")
|
|
.dimension(pointcfLayer3)
|
|
.group(countGroup2);
|
|
|
|
|
|
/*----------------BUILD THE LAYERS OF THE POINTMAP-------------------------*/
|
|
|
|
/*-----BUILD LAYER #1, POLYGONS OF ZIPCODES COLORED BY AVG CONTRIBUTION----*/
|
|
|
|
// get the dimensions used for the first layer, the polygon layer
|
|
// we need the rowid for polygon rendering, so the dimension will be based on
|
|
// the rowid of the zipcodes
|
|
var polyDim1 = polycfLayer1.dimension("zipcodes.rowid");
|
|
|
|
// we're going to color based on the average contribution of the zipcode,
|
|
// so reduce the average from the join
|
|
var polyGrp1 = polyDim1.group().reduceAvg("contributions.amount", "avgContrib");
|
|
|
|
// create the scale to use for the fill color of the polygons.
|
|
// We're going to use the avg contribution of the zipcode to color the poly.
|
|
// First, we define a range of colors to use. And then create a quantize scale
|
|
// to map avg contributions to a color. In this case, quantize equally divides the
|
|
// domain of the scale into bins to match with the # of colors. We're going to use
|
|
// a domain of avg contributions of $0-5000. Since we've got 9 colors, the domain
|
|
// will be split up into 9 equally-sized bins for coloring:
|
|
// [0, 555], [556, 1100], [1101, 1665], etc.
|
|
var polyColorRange = ["#115f9a","#1984c5","#22a7f0","#48b5c4","#76c68f","#a6d75b","#c9e52f","#d0ee11","#d0f400"]
|
|
var polyFillColorScale = d3.scale.quantize().domain([0, 5000]).range(polyColorRange)
|
|
|
|
// setup the first layer, the zipcode polygons
|
|
var polyLayer1 = dc.rasterLayer("polys")
|
|
.dimension(polyDim1)
|
|
.group(polyGrp1)
|
|
// .cap(100) // We can add a cap if we want.
|
|
.fillColorScale(polyFillColorScale) // set the fill color scale
|
|
.fillColorAttr('avgContrib') // set the driving attribute for the fill color scale, in
|
|
// this case, the average contribution
|
|
.defaultFillColor("green") // Set a default fill color. This is unnecessary here
|
|
// as the quantize scale will catch all possible values
|
|
|
|
// .defaultStrokeColor("red") // can optionally set up stroking of the polys to see
|
|
// the boundaries of the zipcodes. This requires
|
|
// a stroke color and a stroke width. Stroke width is
|
|
// in pixels.
|
|
// .defaultStrokeWidth(4)
|
|
.popupColumns(['avgContrib', 'ZCTA5CE10']) // setup the columns we want to show when
|
|
// hit-testing the polygons
|
|
.popupColumnsMapped({avgContrib: "avg contribution", ZCTA5CE10: 'zipcode'})
|
|
// setup a map so rename the popup columns
|
|
// to something readable.
|
|
|
|
// .popupStyle({ // can optionally setup a different style for the popup
|
|
// fillColor: "transparent" // geometry. By default, the popup geom is colored the
|
|
// }) // same as the fill/stroke color attributes
|
|
|
|
|
|
/*-----------BUILD LAYER #2, POINTS OF TWEETS-------------*/
|
|
/*-----SIZED BY # OF FOLLOWERS AND COLORED BY LANGUAGE----*/
|
|
|
|
// build the dimensions for the 2rd layer, to be rendered as points from a tweets table.
|
|
// Note that we're converting longitude and latitude to mercator-projected x,y respectively
|
|
// as the map is a mercator-projected map.
|
|
// We're also grabbing the language of the tweet as well as the number
|
|
// of followers the twitter user has to color and size the points
|
|
var pointMapDim2 = pointcfLayer2.dimension(null).projectOn(["conv_4326_900913_x(lon) as x", "conv_4326_900913_y(lat) as y", "lang as color", "followers as size"]);
|
|
|
|
// we need separate dimensions for the x and y coordinates for point layers.
|
|
// A filter is applied to these dimensions under the hood so that we only
|
|
// render points that are within the view.
|
|
var xDim2 = pointcfLayer2.dimension("lon");
|
|
var yDim2 = pointcfLayer2.dimension("lat");
|
|
|
|
// setup a d3 scale for the tweet layer to scale the points based on the number of
|
|
// followers of the user.
|
|
// # of followers will be mapped to point sizes that are linearly scaled from 2 to 12 pixels
|
|
// 0 followers = 2 pixels in size, 5000 followers = 12 pixels, and is linearly interpolated
|
|
// for values in between, so 2500 followers will get a point size of 7.
|
|
// We'll clamp this scale, so points will go no smaller than 2 and no larger than 12.
|
|
var sizeScaleLayer2 = d3.scale.linear().domain([0,5000]).range([2,12]).clamp(true);
|
|
|
|
// setup a d3 scale to color the points. In this case we're going to color by
|
|
// the language of the tweets. As language is a string, or category, and not a numeric domain
|
|
// we need to use an ordinal scale, which is used to map categories to output values.
|
|
var langDomain = ['en', 'pt', 'es', 'in', 'und', 'ja', 'tr', 'fr', 'tl', 'ru', 'ar']
|
|
var langColors = ["#27aeef", "#ea5545", "#87bc45", "#b33dc6", "#f46a9b", "#ede15b", "#bdcf32", "#ef9b20", "#4db6ac", "#edbf33", "#7c4dff"]
|
|
|
|
var layer2ColorScale = d3.scale.ordinal().domain(langDomain).range(langColors);
|
|
|
|
// setup the second layer, points of the tweets.
|
|
var pointLayer2 = dc.rasterLayer("points")
|
|
.dimension(pointMapDim2) // need a dimension and a group, but just supply
|
|
.group(pointMapDim2) // the dimension as the group too as we're not grouping anything
|
|
.xDim(xDim2) // add the x dimension
|
|
.yDim(yDim2) // add the y dimension
|
|
.xAttr("x") // indicate which column will drive the x dimension
|
|
.yAttr("y") // indicate which column will drive the y dimension
|
|
.sizeScale(sizeScaleLayer2) // setup the scale used to adjust the size of the points
|
|
.sizeAttr("size") // indicate which column will drive the size scale
|
|
.fillColorScale(layer2ColorScale) // set the scale to use to define the fill color
|
|
// of the points
|
|
.fillColorAttr("color") // indicate which column will drive the fill color scale
|
|
.defaultFillColor("#80DEEA") // set a default color for cases where the language
|
|
// of a tweet is not found in the domain fo the scale
|
|
.cap(500000) // set a max number of points to render. This is required
|
|
// for point layers.
|
|
.sampling(true) // set sampling so you get a more equal distribution
|
|
// of the points.
|
|
.popupColumns(['tweet_text', 'sender_name', 'tweet_time', 'lang', 'origin', 'followers'])
|
|
// setup the columns to show when a point is properly hit-tested
|
|
// against
|
|
|
|
|
|
/*---------------BUILD LAYER #3, POINTS OF CONTRIBUTIONS-------------------*/
|
|
/*--------COLORED BY THE CONTRIBUTION RECIPIENT'S PARTY AFFILIATON---------*/
|
|
/*--AND WHOSE SIZE IS DYNAMICALLY CONTROLLED BASED ON NUMBER OF PTS DRAWN--*/
|
|
|
|
// build the dimensions for the 3nd layer, to be rendered as points from the contributions table
|
|
// Note that we're converting longitude and latitude to mercator-projected x,y respectively
|
|
// here as well. We're also going to color by the recepient's
|
|
// party affiliation, so need to project on that column as well.
|
|
var pointMapDim3 = pointcfLayer3.dimension(null).projectOn(["conv_4326_900913_x(lon) as x", "conv_4326_900913_y(lat) as y", "recipient_party as color"]);
|
|
|
|
// we need separate dimensions for the x and y coordinates for point layers.
|
|
// A filter is applied to these dimensions under the hood so that we only
|
|
// render points that are within the view.
|
|
var xDim3 = pointcfLayer3.dimension("lon");
|
|
var yDim3 = pointcfLayer3.dimension("lat");
|
|
|
|
// we're going to dynamically scale the size of the points here based on how many
|
|
// points in this layer are visible in the current view.
|
|
// If there are 20,000 pts in view, the point size will be 1, if there is 1
|
|
// point, it's size will be 7 pixels. We'll use a non-linear scale, sqrt in this case,
|
|
// so that sizes will converge to 7.0 faster as the # of pts goes fro 20K to 1.
|
|
// We'll also clamp so that sizes go no less than 1 and no greater than 7 pixels.
|
|
var dynamicSizeScale = d3.scale.sqrt().domain([100000,0]).range([1.0,7.0]).clamp(true)
|
|
|
|
// setup a categorical, in other words ordinal, scale for the fill color of the
|
|
// points based on the contribution recipient's party affiliation. Republicans
|
|
// will be red and democrats will be blue.
|
|
var layer3ColorScale = d3.scale.ordinal().domain(["D", "R"]).range(["blue", "red"]);
|
|
|
|
var pointLayer3 = dc.rasterLayer("points")
|
|
.dimension(pointMapDim3) // need a dimension and a group, but just supply
|
|
.group(pointMapDim3) // the dimension as the group too as we're not grouping anything
|
|
.xDim(xDim3) // add the x dimension
|
|
.yDim(yDim3) // add the y dimension
|
|
.xAttr("x") // indicate which column will drive the x dimension
|
|
.yAttr("y") // indicate which column will drive the y dimension
|
|
.fillColorScale(layer3ColorScale) // set the scale to use to define the fill color
|
|
// of the points
|
|
.fillColorAttr("color") // indicate which column will drive the fill color scale
|
|
.defaultFillColor("green") // set a default color so points that aren't democrat or
|
|
// republican get a color
|
|
.defaultSize(1) // set a default size for the points
|
|
.dynamicSize(dynamicSizeScale) // but setup dynamic sizing of the points according
|
|
// to the number of points drawn
|
|
.cap(500000) // set a cap for the # of points to draw, this is required
|
|
// for point layers
|
|
.sampling(true) // activate sampling so the points rendered are evenly
|
|
// distributed
|
|
.popupColumns(['amount', 'recipient_party', 'recipient_name'])
|
|
// setup columns to show when a point is properly hit-tested
|
|
|
|
|
|
|
|
|
|
/*---------------BUILD THE POINTMAP-------------*/
|
|
// grab the parent div.
|
|
var parent = document.getElementById("chart1-example");
|
|
|
|
var pointMapChart = dc.rasterChart(parent, true) // create a raster chart. true indicates a pointmap
|
|
.con(con) // indicate the connection layer
|
|
.usePixelRatio(true) // tells the widget to use the pixel ratio of the
|
|
// screen for proper sizing of the backend-rendered image
|
|
.useLonLat(true) // all point layers need their x,y coordinates, which
|
|
// are lon,lat converted to mercator.
|
|
.height(h/1.5) // set width/height
|
|
.width(w)
|
|
.mapUpdateInterval(750)
|
|
.mapStyle('mapbox://styles/mapbox/light-v8')
|
|
|
|
// add the layers to the pointmap
|
|
.pushLayer('polytable1', polyLayer1)
|
|
.pushLayer('pointtable1', pointLayer2)
|
|
.pushLayer('pointtable2', pointLayer3)
|
|
|
|
// and setup a buffer radius around the pixels for hit-testing
|
|
// This radius helps to properly resolve hit-testing at boundaries
|
|
.popupSearchRadius(2)
|
|
|
|
// now render the pointmap
|
|
dc.renderAllAsync()
|
|
|
|
|
|
/*---------------SETUP HIT-TESTING-------------*/
|
|
// hover effect with popup
|
|
// Use a flag to determine if the map is in motion
|
|
// or not (pan/zoom/etc)
|
|
var mapmove = false;
|
|
|
|
// debounce the popup - we only want to show the popup when the
|
|
// cursor is idle for a portion of a second.
|
|
var debouncedPopup = _.debounce(displayPopupWithData, 250)
|
|
pointMapChart.map().on('movestart', function() {
|
|
// map has started moving in some way, so cancel
|
|
// any debouncing, and hide any current popups.
|
|
mapmove = true;
|
|
debouncedPopup.cancel();
|
|
pointMapChart.hidePopup();
|
|
});
|
|
|
|
pointMapChart.map().on('moveend', function(event) {
|
|
// map has stopped moving, so start a debounce event.
|
|
// If the cursor is idle, a popup will show if the
|
|
// cursor is over a layer element.
|
|
mapmove = false;
|
|
debouncedPopup(event);
|
|
pointMapChart.hidePopup();
|
|
});
|
|
|
|
pointMapChart.map().on('mousemove', function(event) {
|
|
// mouse has started moving, so hide any existing
|
|
// popups. 'true' in the following call says to
|
|
// animate the hiding of the popup
|
|
pointMapChart.hidePopup(true);
|
|
|
|
// start a debound popup event if the map isn't
|
|
// in motion
|
|
if (!mapmove) {
|
|
debouncedPopup(event);
|
|
}
|
|
})
|
|
|
|
// callback function for when the mouse has been idle for a moment.
|
|
function displayPopupWithData (event) {
|
|
if (event.point) {
|
|
// check the pointmap for hit-testing. If a layer's element is found under
|
|
// the cursor, then display a popup of the resulting columns
|
|
pointMapChart.getClosestResult(event.point, function(closestPointResult) {
|
|
// 'true' indicates to animate the popup when starting to display
|
|
pointMapChart.displayPopup(closestPointResult, true)
|
|
});
|
|
}
|
|
}
|
|
|
|
/*--------------------------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
|
|
.width(w)
|
|
.height(h/1.5);
|
|
|
|
dc.renderAll();
|
|
}
|
|
|
|
}
|
|
|
|
});
|