fix color lookup for popup

click filtering

fix examples

push layer filter to chart filter handlers; filter null values from popup

rewrite query to use table aliases for self-join

change query to use non-join when tables are the same

improved click filtering

fix race between mapbox style load and data fetch

layer filter listeners

import d3
This commit is contained in:
Tai Dupree 2018-01-10 12:18:24 -08:00 committed by ʈᵃᵢ
parent dd15c167c3
commit afc4f50838
15 changed files with 259 additions and 95 deletions

View File

@ -1,38 +1,27 @@
document.addEventListener("DOMContentLoaded", function init() {
var config = {
table: "contributions_donotmodify",
valueColumn: "contributions_donotmodify.amount",
joinColumn: "contributions_donotmodify.contributor_zipcode",
valueColumn: "amount",
joinColumn: "contributor_zipcode",
polyTable: "zipcodes",
polyJoinColumn: "ZCTA5CE10",
timeColumn: "contrib_date",
timeLabel: "Number of Contributions",
domainBoundMin: 0,
domainBoundMax: 2600,
domainBoundMin: 1,
domainBoundMax: 2600, //00000,
numTimeBins: 423
}
new MapdCon()
.protocol("http")
.host("kali.mapd.com")
.port("9092")
.protocol("https")
.host("metis.mapd.com")
.port("443")
.dbName("mapd")
.user("mapd")
.password("HyperInteractive")
.connect(function(error, con) {
crossfilter
.crossfilter(
con,
["contributions_donotmodify", "zipcodes"],
[
{
table1: "contributions_donotmodify",
attr1: "contributor_zipcode",
table2: "zipcodes",
attr2: "ZCTA5CE10"
}
]
)
.crossfilter(con, "contributions_donotmodify")
.then(function(cf) {
crossfilter
.crossfilter(con, "contributions_donotmodify")
@ -48,12 +37,12 @@ document.addEventListener("DOMContentLoaded", function init() {
var parent = document.getElementById("polymap")
// The values in the table and column specified in crossFilter.dimension
// must correspond to values in the table and keysColumn specified in polyRasterChart.polyJoin.
var dim = crossFilter.dimension("zipcodes.rowid") // Values to join on.
var grp = dim
.group()
.reduceAvg("contributions_donotmodify.amount", "avgContrib") // Values to color on.
// var dim = crossFilter.dimension("tweets_nov_feb.state_abbr") // Values to join on.
// var grp = dim.group().reduceAvg("tweets_nov_feb.tweet_count") // Values to color on.
var dim = crossFilter.dimension(config.joinColumn) // Values to join on.
// var grp = dim
// .group()
// .reduceAvg("contributions_donotmodify.amount", "avgContrib") // Values to color on.
// // var dim = crossFilter.dimension("tweets_nov_feb.state_abbr") // Values to join on.
// // var grp = dim.group().reduceAvg("tweets_nov_feb.tweet_count") // Values to color on.
// Can use getDomainBounds to dynamically find min and max of values that will be colored,
// or the domain [min, max] can be set directly
@ -93,15 +82,16 @@ document.addEventListener("DOMContentLoaded", function init() {
var polyLayer = dc
.rasterLayer("polys")
.crossfilter(crossFilter)
.dimension(dim)
.setState({
data: [
{
table: "contributions_donotmodify",
attr: "contributor_zipcode"
table: config.table,
attr: config.joinColumn
},
{
table: "zipcodes",
attr: "ZCTA5CE10"
table: config.polyTable,
attr: config.polyJoinColumn
}
],
mark: {
@ -114,45 +104,48 @@ document.addEventListener("DOMContentLoaded", function init() {
encoding: {
color: {
type: "quantitative",
aggregrate: "AVG(contributions_donotmodify.amount)",
aggregrate: `SUM(${config.valueColumn})`,
domain: colorDomain,
range: colorRange
}
}
})
polyLayer.popupColumns(["color"])
polyLayer.popupColumns(["key0", "color"])
polyLayer.popupColumnsMapped({"key0": "zipcode", "color": "total amount"})
polyMap.pushLayer("polys", polyLayer).init().then(() => {
// polyMap.borderWidth(zoomToBorderWidth(polyMap.map().getZoom()))
// Keeps the border widths reasonable regardless of zoom level.
polyMap.map().on("zoom", function() {
polyMap
.pushLayer("polys", polyLayer)
.init()
.then(() => {
// polyMap.borderWidth(zoomToBorderWidth(polyMap.map().getZoom()))
// Keeps the border widths reasonable regardless of zoom level.
polyMap.map().on("zoom", function() {
// polyMap.borderWidth(zoomToBorderWidth(polyMap.map().getZoom()))
})
dc.renderAllAsync()
window.addEventListener(
"resize",
_.debounce(function() {
resizeChart(polyMap, 1.5)
}, 500)
)
})
dc.renderAllAsync()
window.addEventListener(
"resize",
_.debounce(function() {
resizeChart(polyMap, 1.5)
}, 500)
)
})
// hover effect with popup
var debouncedPopup = _.debounce(displayPopupWithData, 250)
polyMap.map().on('mousewheel', polyMap.hidePopup);
polyMap.map().on('wheel', polyMap.hidePopup);
polyMap.map().on('mousemove', polyMap.hidePopup);
polyMap.map().on('mousemove', debouncedPopup);
function displayPopupWithData (event) {
polyMap.map().on("mousewheel", polyMap.hidePopup)
polyMap.map().on("wheel", polyMap.hidePopup)
polyMap.map().on("mousemove", polyMap.hidePopup)
polyMap.map().on("mousemove", debouncedPopup)
function displayPopupWithData(event) {
polyMap.getClosestResult(event.point, polyMap.displayPopup)
}
})
}
function getDomainBounds(column, groupAll, callback) {
groupAll
.reduce([
@ -237,7 +230,10 @@ document.addEventListener("DOMContentLoaded", function init() {
chart.map().resize()
chart.isNodeAnimate = false
}
chart.width(width()).height(height() / heightDivisor).renderAsync()
chart
.width(width())
.height(height() / heightDivisor)
.renderAsync()
dc.redrawAllAsync()
}

View File

@ -134,6 +134,11 @@ document.addEventListener("DOMContentLoaded", function init() {
domain: langDomain,
range: langOriginColors
}
},
config: {
point: {
shape: "circle"
}
}
})
.popupColumns(['tweet_text', 'sender_name', 'tweet_time', 'lang', 'origin', 'followers'])

View File

@ -115,6 +115,11 @@
domain: langDomain,
range: langColors
}
},
config: {
point: {
shape: "circle"
}
}
})
.xDim(xDim)

View File

@ -79,11 +79,11 @@ document.addEventListener("DOMContentLoaded", function init() {
// 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");
var polyDim1 = polycfLayer1.dimension("contributions_donotmodify.amount");
// 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_donotmodify.amount", "avgContrib");
// var polyGrp1 = polyDim1.group().reduceAvg("contributions_donotmodify.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.
@ -99,6 +99,7 @@ document.addEventListener("DOMContentLoaded", function init() {
// setup the first layer, the zipcode polygons
var polyLayer1 = dc.rasterLayer("polys")
.crossfilter(polycfLayer1)
.dimension(polyDim1)
.setState({
data: [
{
@ -131,8 +132,8 @@ document.addEventListener("DOMContentLoaded", function init() {
}
}
})
.popupColumns(['color', 'ZCTA5CE10'])
.popupColumnsMapped({color: "avg contribution", ZCTA5CE10: 'zipcode'})
.popupColumns(['color', 'key0'])
.popupColumnsMapped({color: "avg contribution", key0: 'zipcode'})
/*-----------BUILD LAYER #2, POINTS OF TWEETS-------------*/
/*-----SIZED BY # OF FOLLOWERS AND COLORED BY LANGUAGE----*/
@ -201,6 +202,11 @@ document.addEventListener("DOMContentLoaded", function init() {
title: "tweets[lang]"
}
}
},
config: {
point: {
shape: "circle"
}
}
})
.popupColumns(['tweet_text', 'sender_name', 'tweet_time', 'lang', 'origin', 'followers'])
@ -266,6 +272,11 @@ document.addEventListener("DOMContentLoaded", function init() {
title: "contributions[recipient_party]"
}
}
},
config: {
point: {
shape: "circle"
}
}
})
// for point layers

View File

@ -77,7 +77,7 @@ document.addEventListener("DOMContentLoaded", function init() {
// 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");
var polyDim1 = polycfLayer1.dimension("contributor_zipcode");
// we're going to color based on the average contribution of the zipcode,
// so reduce the average from the join
@ -97,6 +97,7 @@ document.addEventListener("DOMContentLoaded", function init() {
// setup the first layer, the zipcode polygons
var polyLayer1 = dc.rasterLayer("polys")
.crossfilter(polycfLayer1)
.dimension(polyDim1)
.setState({
data: [
{
@ -205,6 +206,11 @@ document.addEventListener("DOMContentLoaded", function init() {
domain: langDomain,
range: langColors
}
},
config: {
point: {
shape: "circle"
}
}
}) // of a tweet is not found in the domain fo the scale
.popupColumns(['tweet_text', 'sender_name', 'tweet_time', 'lang', 'origin', 'followers'])
@ -269,6 +275,11 @@ document.addEventListener("DOMContentLoaded", function init() {
domain: ["D", "R"],
range: ["blue", "red"]
}
},
config: {
point: {
shape: "circle"
}
}
})
.xDim(xDim3)

10
package-lock.json generated
View File

@ -13,7 +13,7 @@
}
},
"@mapd/crossfilter": {
"version": "git://github.com/mapd/mapd-crossfilter.git#9340d15de829dc91a42cef74d5beec35462774b3",
"version": "git://github.com/mapd/mapd-crossfilter.git#9bdeae1012f97eecc0bc1fa86420049e38a76799",
"dev": true
},
"@mapd/mapd-draw": {
@ -6682,10 +6682,10 @@
"resolved": "https://registry.npmjs.org/mapbox-gl-supported/-/mapbox-gl-supported-1.2.0.tgz",
"integrity": "sha1-y9NN+JQgbK3amjPI2aRgnya7GYk="
},
"mapd-data-layer": {
"version": "0.0.4-rc.2",
"resolved": "https://registry.npmjs.org/mapd-data-layer/-/mapd-data-layer-0.0.4-rc.2.tgz",
"integrity": "sha512-zYjfG0FGG+K6+/jpZF0iULDJcQoT5wpm/YJW2X9qVQrgTa2aGbXWEOCmN6dNCP4MeGNdAVOqfn2dkaAd0M0Vsg==",
"mapd-data-layer-2": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/mapd-data-layer-2/-/mapd-data-layer-2-0.0.1.tgz",
"integrity": "sha1-4zhejliwdPKcCG47ATUIHatDvEg=",
"requires": {
"invariant": "2.2.2"
}

View File

@ -58,14 +58,14 @@
"http-server": "^0.11.1",
"legendables": "git://github.com/mapd/legendables.git#d678a36de78b008814fab72ce260ee33ee7d48b7",
"mapbox-gl": "https://github.com/mapd/mapbox-gl-js/tarball/9c04de6949fe498c8c79f5c0627dfd6d6321f307",
"mapd-data-layer": "0.0.4-rc.2",
"mapd-data-layer-2": "0.0.1",
"moment": "2.13.0",
"ramda": "0.21.0",
"simplify-js": "^1.2.1"
},
"devDependencies": {
"@mapd/connector": "git://github.com/mapd/mapd-connector.git#v2.0.0",
"@mapd/crossfilter": "git://github.com/mapd/mapd-crossfilter.git#v1.1.0",
"@mapd/crossfilter": "git://github.com/mapd/mapd-crossfilter.git#tai/poly-stuff",
"atob": "^2.0.3",
"babel-cli": "^6.10.1",
"babel-core": "^6.10.4",

View File

@ -811,6 +811,10 @@ body {
pointer-events: none;
}
.map-polygon-shape {
pointer-events: auto;
}
.map-popup, .map-point-wrap {
width: 100%;

View File

@ -450,6 +450,10 @@ export default function rasterChart(parent, useMap, chartGroup, _mapboxgl) {
} else {
_chart._setOverlay(null, null, null, browser, Boolean(redraw))
}
} else {
_chart.map().once("style.load", () => {
_chart._doRender(data, redraw, doNotForceData)
})
}
}

View File

@ -585,6 +585,18 @@ export default function mapMixin(
})
}
})
_map.on("mousedown", event => {
_chart.getClosestResult(event.point, result => {
const data = result.row_set[0]
_chart.getLayerNames().forEach(layerName => {
const layer = _chart.getLayer(layerName)
if (typeof layer.onClick === "function") {
layer.onClick(_chart, data, event.originalEvent)
}
})
})
})
})
}

View File

@ -59,6 +59,7 @@ export function rasterDrawMixin(chart) {
let currYRange = null
const coordFilters = new Map()
let origFilterFunc = null
let origFilterAll = null
const defaultStyle = {
fillColor: "#22a7f0",
@ -281,9 +282,9 @@ export function rasterDrawMixin(chart) {
function filters() {
const shapes = drawEngine.getShapesAsJSON()
if (shapes[0]) {
return chart.zoomFilters().concat(Array.from(shapes))
return chart.nonDrawFilters().concat(Array.from(shapes))
}
return chart.zoomFilters()
return chart.nonDrawFilters()
}
function filter(filterArg) {
@ -442,11 +443,19 @@ export function rasterDrawMixin(chart) {
chart.map().on("resize", updateDrawResize)
origFilterFunc = chart.filter
origFilterAll = chart.filterAll
chart.filter = filter
chart.zoomFilters = chart.filters
chart.nonDrawFilters = chart.filters
chart.filters = filters
chart.filterAll = () => {
origFilterAll()
chart.getLayerNames().forEach(layerName => {
const layer = chart.getLayer(layerName)
if (layer.hasOwnProperty("filterAll")) {
layer.filterAll()
}
})
if (coordFilters) {
coordFilters.forEach(filterObj => {
filterObj.shapeFilters = []

View File

@ -3,6 +3,8 @@ import {
createRasterLayerGetterSetter,
createVegaAttrMixin
} from "../utils/utils-vega"
import d3 from "d3"
import { events } from "../core/events"
import { parser } from "../utils/utils"
const vegaLineJoinOptions = ["miter", "round", "bevel"]
@ -49,6 +51,7 @@ function validateMiterLimit(newMiterLimit, currMiterLimit) {
export default function rasterLayerPolyMixin(_layer) {
_layer.crossfilter = createRasterLayerGetterSetter(_layer, null)
_layer.filtersInverse = createRasterLayerGetterSetter(_layer, false)
createVegaAttrMixin(
_layer,
@ -91,18 +94,54 @@ export default function rasterLayerPolyMixin(_layer) {
return state
}
function getTransforms({ filter, globalFilter }) {
function getTransforms({
filter,
globalFilter,
layerFilter = [],
filtersInverse
}) {
const selfJoin = state.data[0].table === state.data[1].table
const groupby = {
type: "project",
expr: `${state.data[0].table}.${state.data[0].attr}`,
as: "key0"
}
const transforms = [
{
type: "rowid",
table: state.data[1].table
},
!selfJoin && {
type: "filter",
expr: `${state.data[0].table}.${state.data[0].attr} = ${
state.data[1].table
}.${state.data[1].attr}`
},
{
type: "aggregate",
fields: [parser.parseExpression(state.encoding.color.aggregrate)],
fields: [
layerFilter.length
? parser.parseExpression({
type: "case",
cond: [
[
{
type: filtersInverse ? "not in" : "in",
expr: `${state.data[0].table}.${state.data[0].attr}`,
set: layerFilter
},
parser.parseExpression(state.encoding.color.aggregrate)
]
],
else: null
})
: parser.parseExpression(state.encoding.color.aggregrate)
],
ops: [null],
as: ["color"],
groupby: {
type: "project",
expr: state.data[0].attr,
as: "key0"
}
groupby
}
]
@ -130,7 +169,13 @@ export default function rasterLayerPolyMixin(_layer) {
return transforms
}
_layer.__genVega = function({ filter, globalFilter, layerName }) {
_layer.__genVega = function({
filter,
globalFilter,
layerFilter,
filtersInverse,
layerName
}) {
const colorRange = state.encoding.color.range.map(c =>
adjustOpacity(c, state.encoding.color.opacity)
)
@ -138,14 +183,18 @@ export default function rasterLayerPolyMixin(_layer) {
data: {
name: layerName,
format: "polys",
shapeColGroup: "mapd",
sql: parser.writeSQL({
type: "root",
source: state.data[0].table, // .map(source => source.table).join(", "),
transform: getTransforms({ filter, globalFilter })
}),
dbTableName: state.data[1].table,
polysKey: state.data[1].attr
source: [
...new Set(state.data.map((source, index) => source.table))
].join(", "),
transform: getTransforms({
filter,
globalFilter,
layerFilter,
filtersInverse
})
})
},
scales: [
{
@ -153,7 +202,8 @@ export default function rasterLayerPolyMixin(_layer) {
type: "quantize",
domain: state.encoding.color.domain,
range: colorRange,
nullValue: "#CACACA"
nullValue: "#D6D7D6",
default: "#D6D7D6"
}
],
mark: {
@ -177,7 +227,7 @@ export default function rasterLayerPolyMixin(_layer) {
strokeColor:
typeof state.mark === "object" ? state.mark.strokeColor : "white",
strokeWidth:
typeof state.mark === "object" ? state.mark.strokeWidth : 0,
typeof state.mark === "object" ? state.mark.strokeWidth : 0.5,
lineJoin:
typeof state.mark === "object" ? state.mark.lineJoin : "miter",
miterLimit:
@ -195,8 +245,12 @@ export default function rasterLayerPolyMixin(_layer) {
_layer._genVega = function(chart, layerName, group, query) {
_vega = _layer.__genVega({
layerName,
filter: _layer.crossfilter().getFilterString(),
globalFilter: _layer.crossfilter().getGlobalFilterString()
filter: _layer
.crossfilter()
.getFilterString(_layer.dimension().getDimensionIndex()),
globalFilter: _layer.crossfilter().getGlobalFilterString(),
layerFilter: _layer.filters(),
filtersInverse: _layer.filtersInverse()
})
return _vega
}
@ -240,6 +294,41 @@ export default function rasterLayerPolyMixin(_layer) {
return false
}
let _filtersArray = []
const _isInverseFilter = false
const polyLayerEvents = ["filtered"]
const _listeners = d3.dispatch.apply(d3, polyLayerEvents)
_layer.filter = function(key, isInverseFilter) {
if (isInverseFilter !== _layer.filtersInverse()) {
_layer.filterAll()
_layer.filtersInverse(isInverseFilter)
}
if (_filtersArray.includes(key)) {
_filtersArray = _filtersArray.filter(v => v !== key)
} else {
_filtersArray = [..._filtersArray, key]
}
_filtersArray.length
? _layer
.dimension()
.filterMulti(_filtersArray, undefined, isInverseFilter)
: _layer.dimension().filterAll()
}
_layer.filters = function() {
return _filtersArray
}
_layer.filterAll = function() {
_filtersArray = []
}
_layer.on = function(event, listener) {
_listeners.on(event, listener)
return _layer
}
_layer._displayPopup = function(
chart,
parentElem,
@ -443,9 +532,13 @@ export default function rasterLayerPolyMixin(_layer) {
scale * (pts[i + 1] - bounds[2]) +
", ")
}
pointStr = pointStr.slice(0, pointStr.length - 2)
pointStr = pointStr.slice(0, pointStr.length - 2).replace(/NaN/g, "")
group.append("polygon").attr("points", pointStr)
group
.append("polygon")
.attr("points", pointStr)
.attr("class", "map-polygon-shape")
.on("click", () => _layer.onClick(chart, data, d3.event))
})
_scaledPopups[chart] = isScaled
@ -458,6 +551,21 @@ export default function rasterLayerPolyMixin(_layer) {
}
}
_layer.onClick = function(chart, data, event) {
if (!data) {
return
}
const isInverseFilter = Boolean(event && (event.metaKey || event.ctrlKey))
chart.hidePopup()
events.trigger(() => {
_layer.filter(data.key0, isInverseFilter)
chart.filter(data.key0, isInverseFilter)
_listeners.filtered(_layer, _filtersArray)
chart.redrawGroup()
})
}
_layer._hidePopup = function(chart, hideCallback) {
const mapPoly = chart.select(".map-poly")
if (mapPoly) {
@ -479,6 +587,7 @@ export default function rasterLayerPolyMixin(_layer) {
}
_layer._destroyLayer = function(chart) {
_layer.on("filtered", null)
// deleteCanvas(chart)
}

View File

@ -82,19 +82,17 @@ describe("rasterLayerPolyMixin", () => {
data: {
name: "polys",
format: "polys",
shapeColGroup: "mapd",
sql:
"SELECT zipcodes.rowid, AVG(contributions_donotmodify.amount) as color FROM contributions_donotmodify, zipcodes WHERE (contributions_donotmodify.contributor_zipcode = zipcodes.ZCTA5CE10) AND (amount=0) GROUP BY zipcodes.rowid ORDER BY color LIMIT 1000000"
"SELECT zipcodes.rowid, contributions_donotmodify.contributor_zipcode as key0, AVG(contributions_donotmodify.amount) as color FROM contributions_donotmodify, zipcodes WHERE (contributions_donotmodify.contributor_zipcode = zipcodes.ZCTA5CE10) AND (amount=0) GROUP BY zipcodes.rowid, key0 LIMIT 1000000"
},
scales: [
{
name: "polys_fillColor",
type: "linear",
type: "quantize",
domain: [0, 100],
range: ["black", "blue"],
default: "green",
nullValue: "#CACACA",
clamp: false
nullValue: "#D6D7D6",
default: "#D6D7D6"
}
],
mark: {

View File

@ -332,7 +332,7 @@ export default function rasterLayer(layerType) {
function renderPopupHTML(data, columnOrder, columnMap) {
let html = ""
columnOrder.forEach(key => {
if (!data[key] && !columnMap[key]) {
if (typeof data[key] === "undefined" || data[key] === null) {
return
}

View File

@ -1,4 +1,4 @@
import { createParser } from "mapd-data-layer"
import { createParser } from "mapd-data-layer-2"
import { formatDataValue, maybeFormatInfinity } from "./formatting-helpers"
import { DAYS, HOURS, MONTHS, QUARTERS } from "../constants/dates-and-times"