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

Update geomaps to allow dataframes as input. #1187

Merged
merged 21 commits into from
Feb 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
78f2551
feat: Update geomaps to allow dataframes as input.
MichaelMauderer Feb 9, 2021
f22cc7c
doc: Update CHANGELOG.md.
MichaelMauderer Feb 9, 2021
d9f6444
doc: Update visualisation doc string.
MichaelMauderer Feb 9, 2021
27eb6d9
feat: Allow labels to be undefined.
MichaelMauderer Feb 10, 2021
0b1935e
lint: Reformat file.
MichaelMauderer Feb 10, 2021
4015bb6
Fix typo in CHANGELOG.md
MichaelMauderer Feb 11, 2021
655410a
Merge branch 'develop' into wip/mm/ide-1055
MichaelMauderer Feb 11, 2021
8fa39bf
refactor: Update enso preprocessor to match on type.
MichaelMauderer Feb 11, 2021
39e4923
refactor: Update enso preprocessor to match on type. Refactor based o…
MichaelMauderer Feb 12, 2021
592f3af
doc: Update CHANGELOG.md.
MichaelMauderer Feb 12, 2021
fddbe2c
doc: Update CHANGELOG.md.
MichaelMauderer Feb 12, 2021
52e434a
Merge branch 'develop' into wip/mm/ide-1055
MichaelMauderer Feb 12, 2021
cfdc548
style: Prettier.
MichaelMauderer Feb 12, 2021
956a563
fix: Update example and center point calculation.
MichaelMauderer Feb 15, 2021
a159cf1
refactor: Expand function names.
MichaelMauderer Feb 15, 2021
3e0958b
refactor: Remove Geo_Map.
MichaelMauderer Feb 15, 2021
eece107
style: Prettier.
MichaelMauderer Feb 15, 2021
39f9838
revert: Debug point size.
MichaelMauderer Feb 15, 2021
aba0eab
fix: Remove console logging.
MichaelMauderer Feb 16, 2021
5382e88
Merge branch 'develop' into wip/mm/ide-1055
MichaelMauderer Feb 16, 2021
7fc9be2
Merge branch 'develop' into wip/mm/ide-1055
MichaelMauderer Feb 16, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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