Skip to content
This repository has been archived by the owner on Dec 28, 2021. It is now read-only.

Commit

Permalink
Update geomaps to allow dataframes as input. (#1187)
Browse files Browse the repository at this point in the history
Allow Tables to feed the Geo Maps visualization. Uses the columns longitude, latitude and label.
Extracts the data through an Enso preprocessor.
  • Loading branch information
MichaelMauderer authored Feb 16, 2021
1 parent b63ea78 commit 1ec89ee
Show file tree
Hide file tree
Showing 2 changed files with 142 additions and 121 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ read the notes of the `Enso 2.0.0-alpha.1` release.
- [Added the ability to reposition visualisations.][1096] There is now an icon in the visualization
action bar that allows dragging the visualization. Once the visualization has been moved, there
appears another icon that will reset the position to the original position.
- [Allow Tables to feed the Geo Map visualisation.][1187] Tables that have `latitude`, `longitude`
and optionally `label` columns can now be shown in a Geo Map visualisation where each row is
mapped to a point of the map with the given label.
- [Erroneous nodes are highlighted.][1182] The error description appears above the node. The nodes
affected by error originating from another node will be highlighted without any description.
- [There is now an API to show VCS status for node][1160].
Expand All @@ -51,6 +54,7 @@ read the notes of the `Enso 2.0.0-alpha.1` release.
[1182]: https://github.com/enso-org/ide/pull/1182
[1160]: https://github.com/enso-org/ide/pull/1160
[1190]: https://github.com/enso-org/ide/pull/1190
[1187]: https://github.com/enso-org/ide/pull/1187
<br/>


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@
*/
const TOKEN =
'pk.eyJ1IjoiZW5zby1vcmciLCJhIjoiY2tmNnh5MXh2MGlyOTJ5cWdubnFxbXo4ZSJ9.3KdAcCiiXJcSM18nwk09-Q'
const GEO_POINT = 'Geo_Point'
const GEO_MAP = 'Geo_Map'
const SCATTERPLOT_LAYER = 'Scatterplot_Layer'
const DEFAULT_POINT_RADIUS = 150

Expand All @@ -36,14 +34,14 @@ const LIGHT_ACCENT_COLOR = [1, 234, 146]
// =====================================

function loadScript(url) {
var script = document.createElement('script')
const script = document.createElement('script')
script.src = url

document.head.appendChild(script)
}

function loadStyle(url) {
var link = document.createElement('link')
const link = document.createElement('link')
link.href = url
link.rel = 'stylesheet'

Expand Down Expand Up @@ -87,28 +85,28 @@ const makeId = makeGenerator()
// ============================

/**
* Provides a mapbox & deck.gl-based map visualization for IDE.
* Provides a mapbox & deck.gl-based map visualization.
*
* > Example creates a map with described properties with a scatter plot overlay:
* {
* "type": "Geo_Map",
* "latitude": 37.8,
* "longitude": -122.45,
* "zoom": 15,
* "controller": true,
* "showingLabels": true, // Enables presenting labels when hovering over Geo_Point.
* "showingLabels": true, // Enables presenting labels when hovering over a point.
* "layers": [{
* "type": "Scatterplot_Layer",
* "data": [{
* "type": "Geo_Point",
* "latitude": -122.45,
* "longitude": 37.8,
* "latitude": 37.8,
* "longitude": -122.45,
* "color": [255, 0, 0],
* "radius": 100,
* "label": "an example label"
* }]
* }]
* }
*
* Can also consume a dataframe that has the columns `latitude`, `longitude` and optionally `label`.
*/
class GeoMapVisualization extends Visualization {
static inputType = 'Any'
Expand Down Expand Up @@ -158,6 +156,21 @@ class GeoMapVisualization extends Visualization {
}

onDataReceived(data) {
if (!this.isInit) {
this.setPreprocessor(
'df -> case df of\n' +
' Table.Table _ ->\n' +
" columns = df.select ['label', 'latitude', 'longitude'] . columns\n" +
" serialized = columns.map (c -> ['df_' + c.name, c.to_vector])\n" +
' Json.from_pairs serialized . to_text\n' +
' _ -> df . to_json . to_text'
)
this.isInit = true
// We discard this data the first time. We will get another update with
// the correct data that has been transformed by the preprocessor.
return
}

let parsedData = data
if (typeof data === 'string') {
parsedData = JSON.parse(data)
Expand All @@ -171,11 +184,9 @@ class GeoMapVisualization extends Visualization {
* Update the internal data with the new incoming data. Does not affect anything rendered.
*/
updateState(data) {
let { latitude, longitude } = this.prepareDataPoints(
data,
this.dataPoints,
this.accentColor
)
extractDataPoints(data, this.dataPoints, this.accentColor)

const { latitude, longitude } = this.centerPoint()

this.latitude = ok(data.latitude) ? data.latitude : latitude
this.longitude = ok(data.longitude) ? data.longitude : longitude
Expand Down Expand Up @@ -249,128 +260,134 @@ class GeoMapVisualization extends Visualization {
})
}

/**
* Prepares data points to be shown on the map.
*
* It checks the type of input data, whether user wants to display single `GEO_POINT`, array of
* those, `SCATTERPLOT_LAYER` or a fully defined `GEO_MAP`, and prepares data field of deck.gl
* layer for given input.
*
* @param preparedDataPoints - List holding data points to push the GeoPoints into.
* @param parsedData - All the parsed data to create points from.
* @param accentColor - accent color of IDE if element doesn't specify one.
*/
prepareDataPoints(parsedData, preparedDataPoints, accentColor) {
let latitude = 0.0
let longitude = 0.0

if (parsedData.type === GEO_POINT) {
this.pushGeoPoint(preparedDataPoints, parsedData, accentColor)
latitude = parsedData.latitude
longitude = parsedData.longitude
} else if (Array.isArray(parsedData) && parsedData.length) {
const computed = this.calculateExtremesAndPushPoints(
parsedData,
preparedDataPoints,
accentColor
)
latitude = computed.latitude
longitude = computed.longitude
} else {
if (
parsedData.type === SCATTERPLOT_LAYER &&
parsedData.data.length
) {
const computed = this.calculateExtremesAndPushPoints(
parsedData.data,
preparedDataPoints,
accentColor
)
latitude = computed.latitude
longitude = computed.longitude
} else if (parsedData.type === GEO_MAP && ok(parsedData.layers)) {
parsedData.layers.forEach((layer) => {
if (layer.type === SCATTERPLOT_LAYER) {
let dataPoints = layer.data || []
const computed = this.calculateExtremesAndPushPoints(
dataPoints,
preparedDataPoints,
accentColor
)
latitude = computed.latitude
longitude = computed.longitude
} else {
console.warn(
'Geo_Map: Currently unsupported deck.gl layer.'
)
}
})
}
}
return { latitude, longitude }
centerPoint() {
const { x, y } = calculateCenterPoint(this.dataPoints)
return { latitude: y, longitude: x }
}

/**
* Helper for prepareDataPoints, pushes `GEO_POINT`'s to the list, and calculates central point.
* @returns {{latitude: number, longitude: number}} - center.
* Sets size of the visualization.
* @param size - new size, list of two numbers containing width and height respectively.
*/
calculateExtremesAndPushPoints(
dataPoints,
preparedDataPoints,
accentColor
) {
let latitudes = []
let longitudes = []
dataPoints.forEach((e) => {
if (e.type === GEO_POINT) {
this.pushGeoPoint(preparedDataPoints, e, accentColor)
latitudes.push(e.latitude)
longitudes.push(e.longitude)
setSize(size) {
this.dom.setAttributeNS(null, 'width', size[0])
this.dom.setAttributeNS(null, 'height', size[1])
this.mapElem.setAttributeNS(
null,
'style',
'width:' + size[0] + 'px;height: ' + size[1] + 'px;'
)
}
}

/**
* Extract the visualisation data from a full configuration object.
*/
function extractVisualizationDataFromFullConfig(
parsedData,
preparedDataPoints,
accentColor
) {
if (parsedData.type === SCATTERPLOT_LAYER && parsedData.data.length) {
pushPoints(parsedData.data, preparedDataPoints, accentColor)
} else if (ok(parsedData.layers)) {
parsedData.layers.forEach((layer) => {
if (layer.type === SCATTERPLOT_LAYER) {
let dataPoints = layer.data || []
pushPoints(dataPoints, preparedDataPoints, accentColor)
} else {
console.warn('Geo_Map: Currently unsupported deck.gl layer.')
}
})
let latitude = 0.0
let longitude = 0.0
if (latitudes.length && longitudes.length) {
let minLat = Math.min.apply(null, latitudes)
let maxLat = Math.max.apply(null, latitudes)
latitude = (minLat + maxLat) / 2
let minLon = Math.min.apply(null, longitudes)
let maxLon = Math.max.apply(null, longitudes)
longitude = (minLon + maxLon) / 2
}
return { latitude, longitude }
}
}

/**
* Pushes a new deck.gl-compatible point made out of `GEO_POINT`
*
* @param preparedDataPoints - List holding geoPoints to push the new element into.
* @param geoPoint - `GEO_POINT` to create new deck.gl point from.
* @param accentColor - accent color of IDE if `GEO_POINT` doesn't specify one.
*/
pushGeoPoint(preparedDataPoints, geoPoint, accentColor) {
/**
* Extract the visualisation data from a dataframe.
*/
function extractVisualizationDataFromDataFrame(
parsedData,
preparedDataPoints,
accentColor
) {
const geoPoints = parsedData.df_latitude.map(function (lat, i) {
const lon = parsedData.df_longitude[i]
let label = ok(parsedData.df_label) ? parsedData.df_label[i] : undefined
return { latitude: lat, longitude: lon, label }
})
pushPoints(geoPoints, preparedDataPoints, accentColor)
}

function isDataFrame(data) {
return data.df_latitude !== undefined && data.df_longitude !== undefined
}

/**
* Extracts the data form the given `parsedData`. Checks the type of input data and prepares our
* internal data (`GeoPoints') for consumption in deck.gl.
*
* @param parsedData - All the parsed data to create points from.
* @param preparedDataPoints - List holding data points to push the GeoPoints into.
* @param accentColor - accent color of IDE if element doesn't specify one.
*/
function extractDataPoints(parsedData, preparedDataPoints, accentColor) {
if (isDataFrame(parsedData)) {
extractVisualizationDataFromDataFrame(
parsedData,
preparedDataPoints,
accentColor
)
} else {
extractVisualizationDataFromFullConfig(
parsedData,
preparedDataPoints,
accentColor
)
}
}

/**
* Transforms the `dataPoints` to the internal data format and appends them to the `targetList`.
* Also adds the `accentColor` for each point.
*
* Expects the `dataPoints` to be a list of objects that have a `longitude` and `latitude` and
* optionally `radius`, `color` and `label`.
*/
function pushPoints(dataPoints, targetList, accentColor) {
dataPoints.forEach((geoPoint) => {
let position = [geoPoint.longitude, geoPoint.latitude]
let radius = isNaN(geoPoint.radius)
? DEFAULT_POINT_RADIUS
: geoPoint.radius
let color = ok(geoPoint.color) ? geoPoint.color : accentColor
let label = ok(geoPoint.label) ? geoPoint.label : ''
preparedDataPoints.push({ position, color, radius, label })
}
targetList.push({ position, color, radius, label })
})
}

/**
* Sets size of the visualization.
* @param size - new size, list of two numbers containing width and height respectively.
*/
setSize(size) {
this.dom.setAttributeNS(null, 'width', size[0])
this.dom.setAttributeNS(null, 'height', size[1])
this.mapElem.setAttributeNS(
null,
'style',
'width:' + size[0] + 'px;height: ' + size[1] + 'px;'
)
/**
* Calculate the center of the bounding box of the given list of objects. The objects need to have
* a `position` attribute with two coordinates.
* @returns {{x: number, y: number}}
*/
function calculateCenterPoint(dataPoints) {
const xs = []
const ys = []
dataPoints.forEach((e) => {
xs.push(e.position[0])
ys.push(e.position[1])
})
let x = 0.0
let y = 0.0
if (xs.length && ys.length) {
let minX = Math.min(...xs)
let maxX = Math.max(...xs)
x = (minX + maxX) / 2
let minY = Math.min(...ys)
let maxY = Math.max(...ys)
y = (minY + maxY) / 2
}
return { x, y }
}

/**
Expand Down

0 comments on commit 1ec89ee

Please sign in to comment.