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 5 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
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,12 @@ 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 `label` columns can now be shown in a Gei Map visualisation where each row is mapped to a
point of the map with the given label.
MichaelMauderer marked this conversation as resolved.
Show resolved Hide resolved

[1096]: https://github.com/enso-org/ide/pull/1172
[1181]: https://github.com/enso-org/ide/pull/1181
[1187]: https://github.com/enso-org/ide/pull/1187
<br/>


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ const makeId = makeGenerator()
* }]
* }]
* }
*
* Can also consume an array of `Geo_Point` structures, or a dataframe that has the columns
* `latitude`, `longitude` and `label`.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have a Geo_Point structure in Enso stdlib? Some time ago we didn't.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we don't It has to be user defined. This is not new, just explicitly stating the current behaviour.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, so let's put the description how the Geo_Point should look like.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should remove Geo_Point support in this PR. It was a hack before maps support for tables. Please confirm it with Sylwia, but it should be dropped already

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is removed.

*/
class GeoMapVisualization extends Visualization {
static inputType = 'Any'
Expand Down Expand Up @@ -158,6 +161,20 @@ class GeoMapVisualization extends Visualization {
}

onDataReceived(data) {
if (!this.isInit) {
this.setPreprocessor(
'df ->\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'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How it is working for "old" datatypes like array of GeoPoints, or just json string?; As I see it assumes that df has select and map method.

I miss checking if type of df is a Table.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now if the expression fails, we get the original data. Once we want to support multiple data types we can use case to transform depending on the input type.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, but that is an obscure feature I would say, and I feel it could change easily. So I would put here the case for Table explicitly and make df.to_json.to_text in other cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is refactored.

)
this.isInit = true
// We discard this data as it is in the wrong format. We will get another update with
// the correct data that has been transformed by the preprocessor. (Which will be the
// same data format if it was not a data frame.
return
}

let parsedData = data
if (typeof data === 'string') {
parsedData = JSON.parse(data)
Expand All @@ -171,12 +188,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
)
updateDataPoints(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 +263,135 @@ 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: x, longitude: y }
}

/**
* 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;'
)
}
}

function prepareArray(parsedData, preparedDataPoints, accentColor) {
MichaelMauderer marked this conversation as resolved.
Show resolved Hide resolved
pushGeoPoints(parsedData, preparedDataPoints, accentColor)
}

function prepareGeoPoints(parsedData, preparedDataPoints, accentColor) {
MichaelMauderer marked this conversation as resolved.
Show resolved Hide resolved
pushGeoPoints(preparedDataPoints, parsedData, accentColor)
const latitude = parsedData.latitude
const longitude = parsedData.longitude
return { latitude, longitude }
}

function prepareDeckGLLayer(parsedData, preparedDataPoints, accentColor) {
MichaelMauderer marked this conversation as resolved.
Show resolved Hide resolved
if (parsedData.type === SCATTERPLOT_LAYER && parsedData.data.length) {
pushGeoPoints(parsedData.data, preparedDataPoints, accentColor)
} else if (parsedData.type === GEO_MAP && ok(parsedData.layers)) {
parsedData.layers.forEach((layer) => {
if (layer.type === SCATTERPLOT_LAYER) {
let dataPoints = layer.data || []
pushGeoPoints(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) {
function prepareDataFrame(parsedData, preparedDataPoints, accentColor) {
MichaelMauderer marked this conversation as resolved.
Show resolved Hide resolved
console.log('prepareDataFrame')
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 }
})
pushGeoPoints(geoPoints, preparedDataPoints, accentColor)
}

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

function isArray(data) {
return Array.isArray(data) && data.length
}

function isGeoPointData(data) {
return data.type === GEO_POINT
}

/**
* Prepares data points to be shown on the map.
*
* It 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 updateDataPoints(parsedData, preparedDataPoints, accentColor) {
if (isDataFrame(parsedData)) {
prepareDataFrame(parsedData, preparedDataPoints, accentColor)
} else if (isGeoPointData(parsedData)) {
prepareGeoPoints(parsedData, preparedDataPoints, accentColor)
} else if (isArray(parsedData)) {
prepareArray(parsedData, preparedDataPoints, accentColor)
} else {
prepareDeckGLLayer(parsedData, preparedDataPoints, accentColor)
}
}

/**
* Transforms the `dataPoints` to the internal data format and appends them to the `targetList`.
* Also adds the `accentColor` for each point.
*/
function pushGeoPoints(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