From 3776c4166923e7b0088eeee79e045e28f759f7b9 Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Mon, 4 Jul 2022 01:25:07 -0400 Subject: [PATCH 1/5] Add `@finos/perspective-viewer-openlayers` plugin --- examples/blocks/src/citibike/index.html | 1 + examples/blocks/src/csv/index.html | 1 + packages/perspective-jupyterlab/package.json | 1 + .../perspective-jupyterlab/src/js/index.js | 1 + .../perspective-viewer-openlayers/build.js | 32 +++++++++++++ .../package.json | 32 +++++++++++++ .../src/themes/material-dark.less | 18 +++++++ scripts/jlab_link.js | 1 + scripts/setup.js | 5 ++ yarn.lock | 48 ++++++++++++++++++- 10 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 packages/perspective-viewer-openlayers/build.js create mode 100644 packages/perspective-viewer-openlayers/package.json diff --git a/examples/blocks/src/citibike/index.html b/examples/blocks/src/citibike/index.html index 67fd349840..7ce5715a99 100644 --- a/examples/blocks/src/citibike/index.html +++ b/examples/blocks/src/citibike/index.html @@ -8,6 +8,7 @@ + diff --git a/examples/blocks/src/csv/index.html b/examples/blocks/src/csv/index.html index d15490b06b..432f56ff19 100644 --- a/examples/blocks/src/csv/index.html +++ b/examples/blocks/src/csv/index.html @@ -7,6 +7,7 @@ + diff --git a/packages/perspective-jupyterlab/package.json b/packages/perspective-jupyterlab/package.json index e0e1a7e97e..f00e30269b 100644 --- a/packages/perspective-jupyterlab/package.json +++ b/packages/perspective-jupyterlab/package.json @@ -35,6 +35,7 @@ "@finos/perspective-viewer": "^1.5.1", "@finos/perspective-viewer-d3fc": "^1.5.1", "@finos/perspective-viewer-datagrid": "^1.5.1", + "@finos/perspective-viewer-openlayers": "^1.5.1", "@jupyter-widgets/base": "^4.1.0", "@jupyterlab/application": "^3.3.2", "@lumino/application": "^1.27.0", diff --git a/packages/perspective-jupyterlab/src/js/index.js b/packages/perspective-jupyterlab/src/js/index.js index ffaff3bd4b..0cd8f039af 100644 --- a/packages/perspective-jupyterlab/src/js/index.js +++ b/packages/perspective-jupyterlab/src/js/index.js @@ -17,6 +17,7 @@ export * from "./widget"; import "../less/index.less"; import "@finos/perspective-viewer-datagrid"; import "@finos/perspective-viewer-d3fc"; +import "@finos/perspective-viewer-openlayers"; import {perspectiveRenderers} from "./renderer"; import {PerspectiveJupyterPlugin} from "./plugin"; diff --git a/packages/perspective-viewer-openlayers/build.js b/packages/perspective-viewer-openlayers/build.js new file mode 100644 index 0000000000..38771f76fc --- /dev/null +++ b/packages/perspective-viewer-openlayers/build.js @@ -0,0 +1,32 @@ +const {NodeModulesExternal} = require("@finos/perspective-build/external"); +const {InlineCSSPlugin} = require("@finos/perspective-build/inline_css"); +const {UMDLoader} = require("@finos/perspective-build/umd"); +const {build} = require("@finos/perspective-build/build"); + +const BUILD = [ + { + entryPoints: ["src/js/plugin/plugin.js"], + plugins: [InlineCSSPlugin(), NodeModulesExternal()], + format: "esm", + outfile: "dist/esm/perspective-viewer-openlayers.js", + }, + { + entryPoints: ["src/js/plugin/plugin.js"], + globalName: "perspective_openlayers", + plugins: [InlineCSSPlugin(), UMDLoader()], + format: "cjs", + outfile: "dist/umd/perspective-viewer-openlayers.js", + }, + { + entryPoints: ["src/js/plugin/plugin.js"], + plugins: [InlineCSSPlugin()], + format: "esm", + outfile: "dist/cdn/perspective-viewer-openlayers.js", + }, +]; + +async function build_all() { + await Promise.all(BUILD.map(build)).catch(() => process.exit(1)); +} + +build_all(); diff --git a/packages/perspective-viewer-openlayers/package.json b/packages/perspective-viewer-openlayers/package.json new file mode 100644 index 0000000000..15b2b694f1 --- /dev/null +++ b/packages/perspective-viewer-openlayers/package.json @@ -0,0 +1,32 @@ +{ + "name": "@finos/perspective-viewer-openlayers", + "version": "1.5.0", + "main": "src/js/plugin/plugin.js", + "files": [ + "dist/**/*" + ], + "author": "", + "license": "Apache-2.0", + "scripts": { + "bench": "npm-run-all bench:build bench:run", + "bench:build": ":", + "bench:run": ":", + "build": "node build.js", + "test:build": ":", + "test:run": ":", + "test": "npm-run-all test:build test:run", + "watch": ":", + "clean": "rimraf dist", + "clean:screenshots": "rimraf \"test/screenshots/**/*.@(failed|diff).png\"" + }, + "dependencies": { + "@finos/perspective": "1.5.0", + "d3": "^7.1.1", + "gradient-parser": "1.0.2", + "less": "^4.1.0", + "ol": "^5.3.2" + }, + "peerDependencies": { + "@finos/perspective-viewer": "1.5.0" + } +} diff --git a/rust/perspective-viewer/src/themes/material-dark.less b/rust/perspective-viewer/src/themes/material-dark.less index d5795e2f36..5f628763cb 100644 --- a/rust/perspective-viewer/src/themes/material-dark.less +++ b/rust/perspective-viewer/src/themes/material-dark.less @@ -23,6 +23,7 @@ perspective-viewer[theme="Material Dark"], .perspective-viewer-material-dark--colors(); .perspective-viewer-material-dark--datagrid(); .perspective-viewer-material-dark--d3fc(); + .perspective-viewer-material-dark--openlayers(); } perspective-copy-menu[theme="Material Dark"], @@ -63,6 +64,23 @@ perspective-expression-editor[theme="Material Dark"], --boolean--column-type--color: @orange50; } +.perspective-viewer-material-dark--openlayers { + --map-element-background: #333333; + --map-category-1: #1f77b4; + --map-category-2: #0366d6; + --map-category-3: #ff7f0e; + --map-category-4: #2ca02c; + --map-category-5: #d62728; + --map-category-6: #9467bd; + --map-category-7: #8c564b; + --map-category-8: #e377c2; + --map-category-9: #7f7f7f; + --map-category-10: #bcbd22; + --map-category-11: #17becf; + --map-gradient: linear-gradient(#4d342f 0%, #e4521b 22.5%, #decb45 42.5%, #a0a0a0 50%, #bccda8 57.5%, #42b3d5 67.5%, #1a237e 100%); + --map-tile-url: "http://{a-c}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png"; +} + .perspective-viewer-material-dark--datagrid { regular-table { --rt-pos-cell--color: @blue50; diff --git a/scripts/jlab_link.js b/scripts/jlab_link.js index 661b8c15eb..8ca972f0fb 100644 --- a/scripts/jlab_link.js +++ b/scripts/jlab_link.js @@ -14,6 +14,7 @@ const packages = [ "./rust/perspective-viewer", "./packages/perspective-viewer-datagrid", "./packages/perspective-viewer-d3fc", + "./packages/perspective-viewer-openlayers", "./packages/perspective-jupyterlab", ]; diff --git a/scripts/setup.js b/scripts/setup.js index 308a77ceaf..fdff5d5c76 100644 --- a/scripts/setup.js +++ b/scripts/setup.js @@ -131,6 +131,11 @@ async function focus_package() { name: "perspective-jupyterlab", value: "perspective-jupyterlab", }, + { + key: "m", + name: "perspective-viewer-openlayers", + value: "perspective-viewer-openlayers", + }, { key: "w", name: "perspective-workspace", diff --git a/yarn.lock b/yarn.lock index 0732ac3752..bace8d9dfe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9283,7 +9283,7 @@ ieee754@1.1.13: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== -ieee754@^1.1.13, ieee754@^1.1.4: +ieee754@^1.1.13, ieee754@^1.1.4, ieee754@^1.1.6: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -12612,6 +12612,15 @@ octokit@^1.7.2: "@octokit/plugin-throttling" "^3.5.1" "@octokit/types" "^6.26.0" +ol@^5.3.2: + version "5.3.3" + resolved "https://registry.yarnpkg.com/ol/-/ol-5.3.3.tgz#ad39b7b485fdbae4b3e1535a0a07cc5d88b0b9b5" + integrity sha512-7eU4x8YMduNcED1D5wI+AMWDRe7/1HmGfsbV+kFFROI9RNABU/6n4osj6Q3trZbxxKnK2DSRIjIRGwRHT/Z+Ww== + dependencies: + pbf "3.1.0" + pixelworks "1.1.0" + rbush "2.0.2" + on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" @@ -13163,6 +13172,14 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +pbf@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/pbf/-/pbf-3.1.0.tgz#f70004badcb281761eabb1e76c92f179f08189e9" + integrity sha512-/hYJmIsTmh7fMkHAWWXJ5b8IKLWdjdlAFb3IHkRBn1XUhIYBChVGfVwmHEAV3UfXTxsP/AKfYTXTS/dCPxJd5w== + dependencies: + ieee754 "^1.1.6" + resolve-protobuf-schema "^2.0.0" + pend@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" @@ -13227,6 +13244,11 @@ pirates@^4.0.0, pirates@^4.0.1: dependencies: node-modules-regexp "^1.0.0" +pixelworks@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/pixelworks/-/pixelworks-1.1.0.tgz#1f095ad48dca8bf8a1c8258e0092031a44f22ca5" + integrity sha512-nDqeyp0pvOvCihLsyc9GHWKP4THUtcfQ+qs61uiVaZdlNv0j7y6PWNyPfnTtuxMJ+MTAqff2QbbM/1DyCcRdOQ== + pkg-dir@4.2.0, pkg-dir@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" @@ -13977,6 +13999,11 @@ proto-list@~1.2.1: resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk= +protocol-buffers-schema@^3.3.1: + version "3.6.0" + resolved "https://registry.yarnpkg.com/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz#77bc75a48b2ff142c1ad5b5b90c94cd0fa2efd03" + integrity sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw== + protocols@^1.1.0, protocols@^1.4.0: version "1.4.8" resolved "https://registry.yarnpkg.com/protocols/-/protocols-1.4.8.tgz#48eea2d8f58d9644a4a32caae5d5db290a075ce8" @@ -14146,6 +14173,11 @@ quick-lru@^4.0.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== +quickselect@^1.0.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-1.1.1.tgz#852e412ce418f237ad5b660d70cffac647ae94c2" + integrity sha512-qN0Gqdw4c4KGPsBOQafj6yj/PA6c/L63f6CaZ/DCF/xF4Esu3jVmKLUDYxghFx8Kb/O7y9tI7x2RjTSXwdK1iQ== + randomatic@^3.0.0: version "3.1.1" resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.1.tgz#b776efc59375984e36c537b2f51a1f0aff0da1ed" @@ -14185,6 +14217,13 @@ raw-body@~1.1.0: bytes "1" string_decoder "0.10" +rbush@2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/rbush/-/rbush-2.0.2.tgz#bb6005c2731b7ba1d5a9a035772927d16a614605" + integrity sha512-XBOuALcTm+O/H8G90b6pzu6nX6v2zCKiFG4BJho8a+bY6AER6t8uQUZdi5bomQc0AprCWhEGa7ncAbbRap0bRA== + dependencies: + quickselect "^1.0.1" + rc4@~0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/rc4/-/rc4-0.1.5.tgz#08c6e04a0168f6eb621c22ab6cb1151bd9f4a64d" @@ -14756,6 +14795,13 @@ resolve-from@^5.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== +resolve-protobuf-schema@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz#9ca9a9e69cf192bbdaf1006ec1973948aa4a3758" + integrity sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ== + dependencies: + protocol-buffers-schema "^3.3.1" + resolve-url@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" From edb4af3e2aff594c307da7a513710f979185df03 Mon Sep 17 00:00:00 2001 From: Andy Lee Date: Mon, 4 Jul 2022 01:25:33 -0400 Subject: [PATCH 2/5] Port from `perspective-viewer-maps` --- .../src/js/data/data.js | 69 +++++ .../src/js/legend/legend.js | 76 +++++ .../src/js/plugin/plugin.js | 113 ++++++++ .../src/js/plugin/template.js | 51 ++++ .../src/js/style/categoryColors.js | 51 ++++ .../src/js/style/categoryShapes.js | 108 ++++++++ .../src/js/style/computed.js | 36 +++ .../src/js/style/linearColors.js | 69 +++++ .../src/js/tooltip/tooltip.js | 262 ++++++++++++++++++ .../src/js/views/base-map.js | 97 +++++++ .../src/js/views/map-view.js | 140 ++++++++++ .../src/js/views/region-view.js | 133 +++++++++ .../src/js/views/views.js | 14 + .../src/less/plugin.less | 86 ++++++ .../src/themes/material.dark.less | 21 ++ 15 files changed, 1326 insertions(+) create mode 100644 packages/perspective-viewer-openlayers/src/js/data/data.js create mode 100644 packages/perspective-viewer-openlayers/src/js/legend/legend.js create mode 100644 packages/perspective-viewer-openlayers/src/js/plugin/plugin.js create mode 100644 packages/perspective-viewer-openlayers/src/js/plugin/template.js create mode 100644 packages/perspective-viewer-openlayers/src/js/style/categoryColors.js create mode 100644 packages/perspective-viewer-openlayers/src/js/style/categoryShapes.js create mode 100644 packages/perspective-viewer-openlayers/src/js/style/computed.js create mode 100644 packages/perspective-viewer-openlayers/src/js/style/linearColors.js create mode 100644 packages/perspective-viewer-openlayers/src/js/tooltip/tooltip.js create mode 100644 packages/perspective-viewer-openlayers/src/js/views/base-map.js create mode 100644 packages/perspective-viewer-openlayers/src/js/views/map-view.js create mode 100644 packages/perspective-viewer-openlayers/src/js/views/region-view.js create mode 100644 packages/perspective-viewer-openlayers/src/js/views/views.js create mode 100644 packages/perspective-viewer-openlayers/src/less/plugin.less create mode 100644 packages/perspective-viewer-openlayers/src/themes/material.dark.less diff --git a/packages/perspective-viewer-openlayers/src/js/data/data.js b/packages/perspective-viewer-openlayers/src/js/data/data.js new file mode 100644 index 0000000000..172684a258 --- /dev/null +++ b/packages/perspective-viewer-openlayers/src/js/data/data.js @@ -0,0 +1,69 @@ +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + +export function getMapData(config) { + const points = []; + + // Enumerate through supplied data + config.data.forEach((row, i) => { + // Exclude "total" rows that don't have all values + const groupCount = row.__ROW_PATH__ ? row.__ROW_PATH__.length : 0; + if (groupCount < config.group_by.length) return; + + // Get the group from the row path + const group = row.__ROW_PATH__ ? row.__ROW_PATH__.join("|") : `${i}`; + const rowPoints = {}; + + // Split the rest of the row into a point for each category + Object.keys(row) + .filter((key) => key !== "__ROW_PATH__" && row[key] !== null) + .forEach((key) => { + const split = key.split("|"); + const category = + split.length > 1 + ? split.slice(0, split.length - 1).join("|") + : "__default__"; + rowPoints[category] = rowPoints[category] || {group, row}; + rowPoints[category][split[split.length - 1]] = row[key]; + }); + + // Add the points for this row to the data set + Object.keys(rowPoints).forEach((key) => { + const rowPoint = rowPoints[key]; + const cols = config.columns.map((c) => rowPoint[c]); + points.push({ + cols, + group: rowPoint.group, + row: rowPoint.row, + category: key, + }); + }); + }); + + return points; +} + +export function getDataExtents(data) { + let extents = null; + data.forEach((point) => { + if (!extents) { + extents = point.cols.map((c) => ({min: c, max: c})); + } else { + extents = point.cols.map((c, i) => + c + ? { + min: Math.min(c, extents[i].min), + max: Math.max(c, extents[i].max), + } + : extents[i] + ); + } + }); + return extents; +} diff --git a/packages/perspective-viewer-openlayers/src/js/legend/legend.js b/packages/perspective-viewer-openlayers/src/js/legend/legend.js new file mode 100644 index 0000000000..d7a379ab41 --- /dev/null +++ b/packages/perspective-viewer-openlayers/src/js/legend/legend.js @@ -0,0 +1,76 @@ +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + +import {select, axisRight, scaleLinear, range} from "d3"; + +const height = 100; + +export function showLegend(container, colorScale, extent) { + const legend = getOrCreateDiv(container); + const domain = [extent.min, extent.max]; + const scale = scaleLinear().domain(domain).range([height, 0]).nice(); + + // axis + const axis = axisRight(scale).tickSize(15).tickArguments([5]); + legend.select(".map-legend-axis").call(axis); + + // color bar + const colorLines = range(1, height + 1, 1); + const lines = legend + .select(".map-legend-color") + .selectAll("line") + .data(colorLines); + lines + .enter() + .append("line") + .attr("x1", 0) + .attr("x2", 15) + .merge(lines) + .attr("y1", (d) => (d * height) / 100) + .attr("y2", (d) => (d * height) / 100) + .attr( + "stroke", + (d) => + colorScale( + ((100 - d) * (extent.max - extent.min)) / 100 + extent.min + ).stroke + ); + + let maxLabelWidth = 0; + legend.selectAll(".map-legend-axis .tick text").each((d, i, node) => { + maxLabelWidth = Math.max(maxLabelWidth, node[i].getBBox().width); + }); + + legend.style("width", `${maxLabelWidth + 37}px`); +} + +export function hideLegend(container) { + select(container).select(".map-legend").remove(); +} + +const getOrCreateDiv = (container) => { + const selection = select(container); + let legend = selection.select(".map-legend"); + + if (legend.size() === 0) { + legend = selection.append("svg").attr("class", "map-legend"); + // color bar + legend + .append("g") + .attr("class", "map-legend-color") + .attr("transform", "translate(10, 10)"); + // axis + legend + .append("g") + .attr("class", "map-legend-axis") + .attr("transform", "translate(10, 10)"); + } + + return legend; +}; diff --git a/packages/perspective-viewer-openlayers/src/js/plugin/plugin.js b/packages/perspective-viewer-openlayers/src/js/plugin/plugin.js new file mode 100644 index 0000000000..9d090c38cc --- /dev/null +++ b/packages/perspective-viewer-openlayers/src/js/plugin/plugin.js @@ -0,0 +1,113 @@ +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + +import mapView from "../views/map-view"; +import views from "../views/views"; +import "./template"; + +views.forEach((plugin) => { + customElements.define( + plugin.plugin.type, + class extends customElements.get("perspective-viewer-plugin") { + async draw(view) { + drawView(plugin).call(this, view); + } + + async resize() { + mapView.resize(this); + } + + get name() { + return plugin.plugin.name; + } + + async restyle(view) { + mapView.restyle(this); + } + + get select_mode() { + return "select"; + } + + get min_config_columns() { + return 2; + } + + get config_column_names() { + return plugin.plugin.initial.names; + } + } + ); + + customElements.get("perspective-viewer").registerPlugin(plugin.plugin.type); +}); + +function drawView(viewEntryPoint) { + return async function (view) { + const table = this.parentElement + .getTable() + .then((table) => table.schema()); + const [tschema, schema, data, config] = await Promise.all([ + table, + view.schema(), + view.to_json(), + view.get_config(), + ]); + + if (!config.group_by) config.group_by = config.group_by; + if (!config.split_by) config.split_by = config.split_by; + if (!config.aggregate) config.aggregate = config.aggregates; + + viewEntryPoint(this, Object.assign({schema, tschema, data}, config)); + }; +} + +function resizeView() { + if (this[PRIVATE] && this[PRIVATE].view) { + this[PRIVATE].view.resize(); + } +} + +// function deleteView() { +// if (this[PRIVATE] && this[PRIVATE].view) { +// this[PRIVATE].view.remove(); +// } +// } + +// function save() { +// if (this[PRIVATE] && this[PRIVATE].chart) { +// const perspective_d3fc_element = this[PRIVATE].chart; +// return perspective_d3fc_element.getSettings(); +// } +// } + +// function restore(settings) { +// const perspective_d3fc_element = getOrCreatePlugin.call(this); +// perspective_d3fc_element.setSettings(settings); +// } + +// function getOrCreatePlugin() { +// this[PRIVATE] = this[PRIVATE] || {}; +// if (!this[PRIVATE].view) { +// this[PRIVATE].view = document.createElement(name); +// } + +// return this[PRIVATE].view; +// } + +// function getElement(div) { +// const perspective_d3fc_element = getOrCreatePlugin.call(this); + +// if (!document.body.contains(perspective_d3fc_element)) { +// div.innerHTML = ""; +// div.appendChild(perspective_d3fc_element); +// } + +// return perspective_d3fc_element; +// } diff --git a/packages/perspective-viewer-openlayers/src/js/plugin/template.js b/packages/perspective-viewer-openlayers/src/js/plugin/template.js new file mode 100644 index 0000000000..6cf44c1f74 --- /dev/null +++ b/packages/perspective-viewer-openlayers/src/js/plugin/template.js @@ -0,0 +1,51 @@ +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + +// import {bindTemplate} from "@finos/perspective-viewer/cjs/js/utils"; +// import style from "../../less/plugin.less"; +// import {name} from "../../../package.json"; + +// const template = ``; + +// @bindTemplate(template, style) // eslint-disable-next-line no-unused-vars +// class TemplateElement extends HTMLElement { +// constructor() { +// super(); + +// // Serialised user settings for this plugin +// this._settings = {}; +// this._view = null; +// } + +// connectedCallback() { +// this._container = this.shadowRoot.querySelector("#container"); +// } + +// render(view, config) { +// this._view = view; +// view(this._container, config, this._settings); +// } + +// resize() { +// // Called by perspective-viewer when the container is resized +// if (this._view && this._view.resize) { +// this._view.resize(this._container); +// } +// } + +// getSettings() { +// // Called when saving user settings +// return this._settings; +// } + +// setSettings(settings) { +// // Called when restoring user settings +// this._settings = settings || {}; +// } +// } diff --git a/packages/perspective-viewer-openlayers/src/js/style/categoryColors.js b/packages/perspective-viewer-openlayers/src/js/style/categoryColors.js new file mode 100644 index 0000000000..cc21a302e9 --- /dev/null +++ b/packages/perspective-viewer-openlayers/src/js/style/categoryColors.js @@ -0,0 +1,51 @@ +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import {computedStyle, toFillAndStroke} from "./computed"; + +const CATEGORY_COLOR_VAR = "--map-category-"; +const defaultColors = [ + "#1f77b4", + "#0366d6", + "#ff7f0e", + "#2ca02c", + "#d62728", + "#9467bd", + "#8c564b", + "#e377c2", + "#7f7f7f", + "#bcbd22", + "#17becf", +]; + +const defaultValueFn = (d) => d.category; +export const categoryColorMap = (container, data, valueFn = defaultValueFn) => { + let colIndex = 0; + const categories = {}; + const categoryColors = getCategoryColors(container); + + data.forEach((point) => { + const category = valueFn(point); + if (!categories[category]) { + const col = categoryColors[colIndex]; + categories[category] = toFillAndStroke(col); + + colIndex++; + if (colIndex >= categoryColors.length) colIndex = 0; + } + }); + + return (point) => categories[valueFn(point)]; +}; + +const getCategoryColors = (container) => { + const computed = computedStyle(container); + return defaultColors.map((defaultColor, i) => + computed(`${CATEGORY_COLOR_VAR}${i + 1}`, defaultColor) + ); +}; diff --git a/packages/perspective-viewer-openlayers/src/js/style/categoryShapes.js b/packages/perspective-viewer-openlayers/src/js/style/categoryShapes.js new file mode 100644 index 0000000000..0070d00207 --- /dev/null +++ b/packages/perspective-viewer-openlayers/src/js/style/categoryShapes.js @@ -0,0 +1,108 @@ +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + +import { + symbol, + symbolCross, + symbolDiamond, + symbolSquare, + symbolStar, + symbolTriangle, + symbolWye, +} from "d3"; + +const {Polygon, Circle} = require("ol/geom"); +const {toContext} = require("ol/render"); +const {Style, Fill, Stroke} = require("ol/style"); + +const shapes = [ + null, + symbolCross, + symbolDiamond, + symbolSquare, + symbolStar, + symbolTriangle, + symbolWye, +]; +let shapePoints = null; + +const defaultValueFn = (d) => d.category; +export const categoryShapeMap = (container, data, valueFn = defaultValueFn) => { + const categoryMap = categoryPointsMap(data, valueFn); + const style = new Style({renderer: createRenderer(categoryMap)}); + return () => style; +}; + +function categoryPointsMap(data, valueFn) { + loadShapes(); + const categories = {}; + let catIndex = 0; + data.forEach((point) => { + const category = valueFn(point); + if (!categories[category]) { + categories[category] = shapePoints[catIndex]; + + catIndex++; + if (catIndex >= shapePoints.length) catIndex = 0; + } + }); + return categories; +} + +function createRenderer(categoryMap) { + return (location, {context, feature}) => { + const {category, style, scale} = feature.getProperties(); + const points = categoryMap[category]; + + var render = toContext(context, {pixelRatio: 1}); + + const fillStyle = new Fill({color: style.fill}); + const strokeStyle = new Stroke({color: style.stroke}); + render.setFillStrokeStyle(fillStyle, strokeStyle); + + if (points.length) { + const sizedPoints = points.map((p) => [ + p[0] * scale + location[0], + p[1] * scale + location[1], + ]); + render.drawPolygon(new Polygon([sizedPoints])); + } else { + render.drawCircle(new Circle(location, scale * 8)); + } + }; +} + +function loadShapes() { + if (!shapePoints) { + shapePoints = shapes.map(shapeToPoints); + } +} + +function shapeToPoints(d3Shape) { + if (d3Shape) { + const shapeSymbol = symbol().type(d3Shape); + const shapePath = shapeSymbol.size(150)(); + const points = shapePath + .substring(1, shapePath.length - 1) + .split("L") + .map((p) => p.split(",").map((c) => parseFloat(c))); + + if (points.length === 1) { + // Square + const l = -points[0][0]; + points.push([l, -l]); + points.push([l, l]); + points.push([-l, l]); + } + + points.push(points[0]); + return points; + } + return []; +} diff --git a/packages/perspective-viewer-openlayers/src/js/style/computed.js b/packages/perspective-viewer-openlayers/src/js/style/computed.js new file mode 100644 index 0000000000..d2368ba5c6 --- /dev/null +++ b/packages/perspective-viewer-openlayers/src/js/style/computed.js @@ -0,0 +1,36 @@ +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import {color, rgb} from "d3-color"; + +export const computedStyle = (container) => { + if (window.ShadyCSS) { + return (name, defaultValue) => + window.ShadyCSS.getComputedStyleValue(container, name) || + defaultValue; + } else { + const containerStyles = getComputedStyle(container); + return (name, defaultValue) => + containerStyles.getPropertyValue(name) || defaultValue; + } +}; + +export const toFillAndStroke = (col) => { + const asColor = color(col); + const stroke = `${asColor}`; + asColor.opacity = 0.5; + const fill = `${asColor}`; + + return {stroke, fill}; +}; + +export const lightenRgb = (col, lighten) => { + const source = Array.isArray(col) ? rgb(col) : color(col); + const target = source.brighter(lighten); + return `${target}`; +}; diff --git a/packages/perspective-viewer-openlayers/src/js/style/linearColors.js b/packages/perspective-viewer-openlayers/src/js/style/linearColors.js new file mode 100644 index 0000000000..8e40931e24 --- /dev/null +++ b/packages/perspective-viewer-openlayers/src/js/style/linearColors.js @@ -0,0 +1,69 @@ +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import * as gparser from "gradient-parser"; +import {interpolate, scaleSequential} from "d3"; + +import {computedStyle, toFillAndStroke} from "./computed"; + +const GRADIENT_COLOR_VAR = "--map-gradient"; +const GRADIENT_DEFAULT = + "linear-gradient(#4d342f 0%, #e4521b 22.5%, #decb45 42.5%, #a0a0a0 50%, #bccda8 57.5%, #42b3d5 67.5%, #1a237e 100%)"; + +export const linearColorScale = (container, extent) => { + const gradient = getGradient(container); + + const interpolator = multiInterpolator(gradient); + + const domain = [ + Math.min(extent.min, -extent.max), + Math.max(-extent.min, extent.max), + ]; + return scaleSequential(interpolator).domain(domain); +}; + +const getGradient = (container) => { + const computed = computedStyle(container); + const gradient = computed(GRADIENT_COLOR_VAR, GRADIENT_DEFAULT); + + return gparser + .parse(gradient)[0] + .colorStops.map((g) => [ + g.length.value / 100, + toFillAndStroke(`#${g.value}`), + ]) + .sort((a, b) => a[0] - b[0]); +}; + +const multiInterpolator = (gradientPairs) => { + // A new interpolator that calls through to a set of + // interpolators between each value/color pair + const interpolators = gradientPairs + .slice(1) + .map((p, i) => interpolate(gradientPairs[i][1], p[1])); + return (value) => { + const index = gradientPairs.findIndex( + (p, i) => + i < gradientPairs.length - 1 && + value <= gradientPairs[i + 1][0] && + value > p[0] + ); + if (index === -1) { + if (value <= gradientPairs[0][0]) { + return gradientPairs[0][1]; + } + return gradientPairs[gradientPairs.length - 1][1]; + } + + const interpolator = interpolators[index]; + const [value1] = gradientPairs[index]; + const [value2] = gradientPairs[index + 1]; + + return interpolator((value - value1) / (value2 - value1)); + }; +}; diff --git a/packages/perspective-viewer-openlayers/src/js/tooltip/tooltip.js b/packages/perspective-viewer-openlayers/src/js/tooltip/tooltip.js new file mode 100644 index 0000000000..9768e204f1 --- /dev/null +++ b/packages/perspective-viewer-openlayers/src/js/tooltip/tooltip.js @@ -0,0 +1,262 @@ +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + +export function createTooltip(container, map) { + let data = null; + let config = null; + let vectorSource = null; + let regions = false; + let onHighlight = null; + + let currentPoint = null; + let currentFeature = null; + + map.on("pointermove", (evt) => { + if (!evt.dragging) { + onMove(evt); + } else { + onLeave(evt); + } + }); + map.on("click", (evt) => onClick(evt)); + + container.addEventListener("mouseleave", (evt) => onLeave(evt)); + + const tooltipDiv = document.createElement("div"); + tooltipDiv.className = "map-tooltip"; + container.appendChild(tooltipDiv); + + const _tooltip = {}; + _tooltip.data = (...args) => { + if (args.length) { + data = args[0]; + return _tooltip; + } + return data; + }; + _tooltip.config = (...args) => { + if (args.length) { + config = args[0]; + return _tooltip; + } + return config; + }; + _tooltip.vectorSource = (...args) => { + if (args.length) { + vectorSource = args[0]; + return _tooltip; + } + return vectorSource; + }; + _tooltip.regions = (...args) => { + if (args.length) { + regions = args[0]; + return _tooltip; + } + return regions; + }; + _tooltip.onHighlight = (...args) => { + if (args.length) { + onHighlight = args[0]; + return _tooltip; + } + return onHighlight; + }; + + const onMove = (evt) => { + // Find the closest point + const {coordinate} = evt; + const hoverFeature = getClosest(coordinate); + const closest = hoverFeature && hoverFeature.get("data"); + if (closest) { + const geometry = hoverFeature.getGeometry(); + const extent = geometry.getExtent(); + + const position = [ + (extent[0] + extent[2]) / 2, + (extent[1] + extent[3]) / 2, + ]; + const screen = map.getPixelFromCoordinate(position); + + if (!regions) { + const mouse = map.getPixelFromCoordinate(coordinate); + if (distanceBetween(screen, mouse) > 50) { + return onLeave(evt); + } + } + + if (currentPoint !== closest) { + currentPoint = closest; + highlighFeature(hoverFeature); + + tooltipDiv.innerHTML = composeHtml(currentPoint); + tooltipDiv.style.left = `${screen[0]}px`; + tooltipDiv.style.top = `${screen[1]}px`; + tooltipDiv.className = "map-tooltip show"; + } + } else { + return onLeave(evt); + } + }; + + const getClosest = (coordinate) => { + if (regions) { + const hitFeatures = + vectorSource.getFeaturesAtCoordinate(coordinate); + return hitFeatures.length ? hitFeatures[0] : null; + } + return vectorSource.getClosestFeatureToCoordinate(coordinate); + }; + + const highlighFeature = (feature) => { + restoreFeature(); + currentFeature = feature; + + if (currentFeature && onHighlight) { + onHighlight(currentFeature, true); + + // if (regions) { + // } else { + // const imageStyle = featureStyle && featureStyle.getImage(); + // if (featureStyle && imageStyle) { + // const color = imageStyle.getStroke().getColor(); + + // const newStyle = new CircleStyle({ + // stroke: new Stroke({color: lightenRgb(color, 0.25)}), + // fill: new Fill({color: lightenRgb(color, 0.5)}), + // radius: imageStyle.getRadius() + // }); + + // currentFeature.setStyle(new Style({image: newStyle, zIndex: 10})); + // } else { + // const color = featureProperties.stroke; + // currentFeature.setProperties({ + // stroke: lightenRgb(color, 0.25), + // fill: lightenRgb(color, 0.5) + // }); + // } + // } + } + }; + + const restoreFeature = () => { + if (currentFeature && onHighlight) { + onHighlight(currentFeature, false); + + // currentFeature.setProperties(featureProperties); + // currentFeature.setStyle(featureStyle); + } + currentFeature = null; + }; + + const onLeave = () => { + tooltipDiv.className = "map-tooltip"; + currentPoint = null; + restoreFeature(); + }; + + const onClick = () => { + if (currentPoint) { + const column_names = config.columns; + const groupFilters = getFilter( + getListFromJoin(currentPoint.group, config.group_by) + ); + const categoryFilters = getFilter( + getListFromJoin(currentPoint.category, config.split_by) + ); + const filters = config.filter + .concat(groupFilters) + .concat(categoryFilters); + + container.dispatchEvent( + new CustomEvent("perspective-click", { + bubbles: true, + composed: true, + detail: { + column_names, + config: {filters}, + row: currentPoint.row, + }, + }) + ); + } + }; + + const composeHtml = (point) => { + const group = composeGroup(point.group); + const aggregates = composeAggregates(point.cols, regions ? 0 : 2); + const category = composeCategory(point.category); + const location = regions ? "" : composeLocation(point.cols); + + return `${group}${aggregates}${category}${location}`; + }; + + const composeAggregates = (cols, fromIndex) => { + if (!cols) return ""; + const list = config.columns.slice(fromIndex).map((c, i) => ({ + name: c, + value: cols[i + fromIndex].toLocaleString(), + })); + return composeList(list); + }; + + const composeGroup = (group) => { + const groupList = getListFromJoin(group, config.group_by); + if (groupList.length === 1) { + return `

${group}

`; + } + return composeList(groupList); + }; + + const composeCategory = (category) => { + return composeList(getListFromJoin(category, config.split_by)); + }; + + const getListFromJoin = (join, pivot) => { + if (join && pivot.length) { + const values = join.split("|"); + return values.map((value, i) => ({name: pivot[i], value})); + } + return []; + }; + + const getFilter = (list) => { + return list.map((item) => [item.name, "==", item.value]); + }; + + const composeList = (items) => { + if (items.length) { + const itemList = items.map( + (item) => + `
  • ${sanitize( + item.name + )}${sanitize(item.value)}
  • ` + ); + return ``; + } + return ""; + }; + + const composeLocation = (cols) => { + return `(${cols[0]}, ${cols[1]})`; + }; + + const sanitize = (text) => { + tooltipDiv.innerText = text; + return tooltipDiv.innerHTML; + }; + + const distanceBetween = (c1, c2) => { + return Math.sqrt( + Math.pow(c1[0] - c2[0], 2) + Math.pow(c1[1] - c2[1], 2) + ); + }; + + return _tooltip; +} diff --git a/packages/perspective-viewer-openlayers/src/js/views/base-map.js b/packages/perspective-viewer-openlayers/src/js/views/base-map.js new file mode 100644 index 0000000000..3fd250c700 --- /dev/null +++ b/packages/perspective-viewer-openlayers/src/js/views/base-map.js @@ -0,0 +1,97 @@ +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + +import {computedStyle} from "../style/computed"; +import {createTooltip} from "../tooltip/tooltip"; + +// const ol = require("ol"); +import {Map, View} from "ol"; +import TileLayer from "ol/layer/Tile"; +import {OSM} from "ol/source"; + +const DEFAULT_TILE_URL = + '"http://{a-c}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png"'; + +const PRIVATE = Symbol("map-view-data"); + +export function baseMap(container) { + // Setup the initial base map + return getOrCreateMap(container); +} + +baseMap.resize = (container) => { + if (container[PRIVATE]) { + container[PRIVATE].map.updateSize(); + } +}; + +baseMap.restyle = (container) => { + if (container[PRIVATE]) { + setTileUrl(container); + } +}; + +baseMap.initialiseView = (container, extent) => { + initialiseView(container, extent); +}; + +function getOrCreateMap(container) { + if (!container[PRIVATE]) { + const tileLayer = new TileLayer(); + + const map = new Map({ + target: container, + layers: [tileLayer], + view: new View({center: [0, 0], zoom: 1}), + }); + + const tooltip = createTooltip(container, map); + container[PRIVATE] = { + map, + tileLayer, + tooltip, + initialisedExtent: false, + }; + } + + removeVectorLayer(container); + setTileUrl(container); + return container[PRIVATE]; +} + +function initialiseView(container, vectorSource) { + if (!container[PRIVATE].initialisedExtent) { + const extents = vectorSource.getExtent(); + const map = container[PRIVATE].map; + map.getView().fit(extents, {size: map.getSize()}); + + container[PRIVATE].initialisedExtent = true; + } +} + +function removeVectorLayer(container) { + const {map} = container[PRIVATE]; + const layers = map.getLayers().getArray(); + for (var n = layers.length - 1; n > 0; n--) { + map.removeLayer(layers[n]); + } +} + +function setTileUrl(container) { + const tileUrl = computedStyle(container)( + "--map-tile-url", + DEFAULT_TILE_URL + ); + const url = tileUrl.trim().substring(1, tileUrl.length - 1); + + if (container[PRIVATE].tileUrl != url) { + container[PRIVATE].tileLayer.setSource(new OSM({wrapX: false, url})); + container[PRIVATE].tileUrl = url; + } +} diff --git a/packages/perspective-viewer-openlayers/src/js/views/map-view.js b/packages/perspective-viewer-openlayers/src/js/views/map-view.js new file mode 100644 index 0000000000..3a06472327 --- /dev/null +++ b/packages/perspective-viewer-openlayers/src/js/views/map-view.js @@ -0,0 +1,140 @@ +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + +import {getMapData, getDataExtents} from "../data/data"; +import {baseMap} from "./base-map"; +import {categoryColorMap} from "../style/categoryColors"; +import {linearColorScale} from "../style/linearColors"; +import {showLegend, hideLegend} from "../legend/legend"; +import {categoryShapeMap} from "../style/categoryShapes"; +import {lightenRgb} from "../style/computed"; + +import VectorLayer from "ol/layer/Vector"; +import VectorSource from "ol/source/Vector"; + +import {Feature} from "ol"; +import {fromLonLat} from "ol/proj"; +import {Point} from "ol/geom"; + +const MIN_SIZE = 1; +const MAX_SIZE = 10; +const DEFAULT_SIZE = 2; + +function mapView(container, config) { + const data = getMapData(config); + const extents = getDataExtents(data); + const map = baseMap(container); + + const useLinearColors = extents.length > 2; + const colorScale = useLinearColors + ? linearColorScale(container, extents[2]) + : null; + const colorMap = useLinearColors + ? (d) => colorScale(d.cols[2]) + : categoryColorMap(container, data); + const sizeMap = sizeMapFromExtents(extents); + const shapeMap = categoryShapeMap(container, data); + + const vectorSource = new VectorSource({ + features: data.map((point) => + featureFromPoint(point, colorMap, sizeMap, shapeMap) + ), + wrapX: false, + }); + baseMap.initialiseView(container, vectorSource); + + const vectorLayer = new VectorLayer({ + source: vectorSource, + updateWhileInteracting: true, + renderMode: "image", + }); + map.map.addLayer(vectorLayer); + + // Update the tooltip component + map.tooltip + .config(config) + .vectorSource(vectorSource) + .regions(false) + .onHighlight(onHighlight) + .data(data); + + if (useLinearColors) { + showLegend(container, colorScale, extents[2]); + } else { + hideLegend(container); + } +} + +mapView.resize = (container) => { + baseMap.resize(container); +}; + +mapView.restyle = (container) => { + baseMap.restyle(container); +}; + +function featureFromPoint(point, colorMap, sizeMap, shapeMap) { + const feature = new Feature(new Point(fromLonLat(point.cols))); + const fillAndStroke = colorMap(point); + if (fillAndStroke) { + feature.setProperties({ + category: point.category, + scale: sizeMap(point) / 4, + style: { + fill: fillAndStroke.fill, + stroke: fillAndStroke.stroke, + }, + data: point, + }); + + // Use custom shapes + feature.setStyle(shapeMap(point)); + } + return feature; +} + +function onHighlight(feature, highlighted) { + const featureProperties = feature.getProperties(); + const oldStyle = featureProperties.oldStyle || featureProperties.style; + + const style = highlighted + ? { + stroke: lightenRgb(oldStyle.stroke, 0.25), + fill: lightenRgb(oldStyle.stroke, 0.5), + } + : oldStyle; + + feature.setProperties({ + oldStyle, + style, + }); +} + +function sizeMapFromExtents(extents) { + if (extents.length > 3) { + // We have the size value + const range = extents[3].max - extents[3].min; + return (point) => + ((point.cols[3] - extents[3].min) / range) * (MAX_SIZE - MIN_SIZE) + + MIN_SIZE; + } + return () => DEFAULT_SIZE; +} + +mapView.plugin = { + type: "perspective-viewer-map-points", + name: "Map Points", + max_size: 25000, + initial: { + type: "number", + count: 2, + names: ["Longitude", "Latitude", "Color", "Size", "Tooltip"], + }, +}; +export default mapView; diff --git a/packages/perspective-viewer-openlayers/src/js/views/region-view.js b/packages/perspective-viewer-openlayers/src/js/views/region-view.js new file mode 100644 index 0000000000..390c94d293 --- /dev/null +++ b/packages/perspective-viewer-openlayers/src/js/views/region-view.js @@ -0,0 +1,133 @@ +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + +import {getMapData, getDataExtents} from "../data/data"; +import {baseMap} from "./base-map"; +import {linearColorScale} from "../style/linearColors"; +import {showLegend, hideLegend} from "../legend/legend"; +import {lightenRgb} from "../style/computed"; + +import VectorLayer from "ol/layer/Vector"; +import VectorSource from "ol/source/Vector"; + +import {KML} from "ol/format"; + +// /const ol = require("ol"); +// const {Vector: VectorLayer} = ol.layer; +// const {Vector: VectorSource} = ol.source; +// const {KML} = ol.format; +import {Style, Fill, Stroke} from "ol/style"; + +const regionSources = {}; +window.registerMapRegions = ({ + name, + url, + key, + format = new KML({extractStyles: false}), +}) => { + const source = new VectorSource({url, format, wrapX: false}); + const nameFn = typeof key == "string" ? (props) => props[key] : key; + regionSources[name] = {source, nameFn}; +}; + +function regionView(container, config) { + const data = getMapData(config); + const extents = getDataExtents(data); + const map = baseMap(container); + + const regionSource = + config.group_by.length && regionSources[config.group_by[0]]; + + if (regionSource) { + const vectorSource = regionSource.source; + const colorScale = linearColorScale(container, extents[0]); + const vectorLayer = new VectorLayer({ + source: vectorSource, + updateWhileInteracting: true, + style: createStyleFunction(regionSource, data, colorScale), + }); + map.map.addLayer(vectorLayer); + + vectorSource.on("change", () => { + baseMap.initialiseView(container, vectorSource); + }); + + // Update the tooltip component + map.tooltip + .config(config) + .vectorSource(vectorSource) + .regions(true) + .onHighlight(onHighlight) + .data(data); + + showLegend(container, colorScale, extents[0]); + } else { + hideLegend(container); + } +} + +function createStyleFunction(regionSource, data, colorScale) { + return (feature) => { + const properties = feature.getProperties(); + const regionName = regionSource.nameFn(properties); + const dataPoint = data.find((d) => d.group == regionName); + if (dataPoint) { + const style = colorScale(dataPoint.cols[0]); + feature.setProperties({data: dataPoint, style}); + + const drawStyle = properties.highlightStyle || style; + return new Style({ + fill: new Fill({color: drawStyle.fill}), + stroke: new Stroke({color: drawStyle.stroke}), + }); + } else { + // Mark it with a name so we can identify it in a tooltip + feature.setProperties({data: {group: regionName}}); + return new Style({ + stroke: new Stroke({color: "rgba(200, 150, 150, 0.2)"}), + }); + } + }; +} + +function onHighlight(feature, highlighted) { + const featureProperties = feature.getProperties(); + + const oldStyle = featureProperties.style; + if (!oldStyle) return; + + const style = highlighted + ? { + stroke: lightenRgb(oldStyle.stroke, 0.25), + fill: lightenRgb(oldStyle.fill, 0.25), + } + : null; + + feature.setProperties({highlightStyle: style}); +} + +regionView.resize = (container) => { + baseMap.resize(container); +}; + +regionView.restyle = (container) => { + baseMap.restyle(container); +}; + +regionView.plugin = { + type: "perspective-viewer-map-regions", + name: "Map Regions", + max_size: 25000, + initial: { + type: "number", + count: 1, + names: ["Color", "Tooltip"], + }, +}; +export default regionView; diff --git a/packages/perspective-viewer-openlayers/src/js/views/views.js b/packages/perspective-viewer-openlayers/src/js/views/views.js new file mode 100644 index 0000000000..d4993ae961 --- /dev/null +++ b/packages/perspective-viewer-openlayers/src/js/views/views.js @@ -0,0 +1,14 @@ +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + +import mapView from "./map-view"; +import regionView from "./region-view"; + +const views = [mapView, regionView]; +export default views; diff --git a/packages/perspective-viewer-openlayers/src/less/plugin.less b/packages/perspective-viewer-openlayers/src/less/plugin.less new file mode 100644 index 0000000000..d081244631 --- /dev/null +++ b/packages/perspective-viewer-openlayers/src/less/plugin.less @@ -0,0 +1,86 @@ +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +@sans-serif-fonts: Arial, sans-serif; +@import (inline) "../../node_modules/ol/ol.css"; + +:host { + #container { + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + overflow: hidden; + + & .ol-zoom { + left: auto; + top: 4em; + left: 0.5em; + } + + & .map-tooltip { + position: absolute; + display: none; + background: var(--map-element-background, #eee); + border: 1px solid #888; + border-radius: 5px; + padding: 5px; + pointer-events: none; + opacity: 0.8; + font-size: 0.8em; + + &.show { + display: block; + } + + & .title { + margin: 0 0 0.5em 0; + font-size: 1.2em; + } + + & ul { + margin: 0 0 0.5em 0; + padding: 0; + list-style: none; + + & li { + margin-bottom: 0.2em; + } + + & .label { + display: inline-block; + font-weight: bold; + margin-right: 1em; + } + } + } + + & .map-legend { + position: absolute; + box-sizing: border-box; + top: 3em; + right: 0.75em; + width: 80px; + height: 122px; + background: var(--map-element-background, #eee); + border-radius: 5px; + pointer-events: none; + opacity: 0.8; + font-size: 0.8em; + + & path.domain { + visibility: hidden; + } + + & .tick line { + color: var(--map-element-background, #eee); + } + } + } +} diff --git a/packages/perspective-viewer-openlayers/src/themes/material.dark.less b/packages/perspective-viewer-openlayers/src/themes/material.dark.less new file mode 100644 index 0000000000..eac2416a71 --- /dev/null +++ b/packages/perspective-viewer-openlayers/src/themes/material.dark.less @@ -0,0 +1,21 @@ +perspective-viewer { + --map-element-background: #333333; + + // colors for categories 1 to 11 + --map-category-1: #1f77b4; + --map-category-2: #0366d6; + --map-category-3: #ff7f0e; + --map-category-4: #2ca02c; + --map-category-5: #d62728; + --map-category-6: #9467bd; + --map-category-7: #8c564b; + --map-category-8: #e377c2; + --map-category-9: #7f7f7f; + --map-category-10: #bcbd22; + --map-category-11: #17becf; + + // color gradient when color value is selected + --map-gradient: linear-gradient(#4d342f 0%, #e4521b 22.5%, #decb45 42.5%, #a0a0a0 50%, #bccda8 57.5%, #42b3d5 67.5%, #1a237e 100%); + + --map-tile-url: "http://{a-c}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png"; +} From 236ebf4f39fe8665f511894f1524c1ae5927e0f9 Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Sun, 10 Jul 2022 02:33:13 -0400 Subject: [PATCH 3/5] Update to latest plugin API --- .../package.json | 8 +- .../src/js/data/data.js | 31 ++---- .../src/js/plugin/plugin.js | 96 ++++++++----------- .../src/js/style/computed.js | 12 +-- .../src/js/tooltip/tooltip.js | 23 +++-- .../src/js/views/base-map.js | 21 ++-- .../src/js/views/map-view.js | 36 +++---- .../src/js/views/region-view.js | 4 +- .../src/js/views/views.js | 5 +- .../src/less/plugin.less | 12 ++- .../src/themes/material-dark.less | 30 +++--- .../src/themes/material.less | 25 +++++ 12 files changed, 150 insertions(+), 153 deletions(-) diff --git a/packages/perspective-viewer-openlayers/package.json b/packages/perspective-viewer-openlayers/package.json index 15b2b694f1..ae1aee9266 100644 --- a/packages/perspective-viewer-openlayers/package.json +++ b/packages/perspective-viewer-openlayers/package.json @@ -1,6 +1,6 @@ { "name": "@finos/perspective-viewer-openlayers", - "version": "1.5.0", + "version": "1.5.1", "main": "src/js/plugin/plugin.js", "files": [ "dist/**/*" @@ -20,13 +20,11 @@ "clean:screenshots": "rimraf \"test/screenshots/**/*.@(failed|diff).png\"" }, "dependencies": { - "@finos/perspective": "1.5.0", + "@finos/perspective": "1.5.1", + "@finos/perspective-viewer": "1.5.1", "d3": "^7.1.1", "gradient-parser": "1.0.2", "less": "^4.1.0", "ol": "^5.3.2" - }, - "peerDependencies": { - "@finos/perspective-viewer": "1.5.0" } } diff --git a/packages/perspective-viewer-openlayers/src/js/data/data.js b/packages/perspective-viewer-openlayers/src/js/data/data.js index 172684a258..6abbf26570 100644 --- a/packages/perspective-viewer-openlayers/src/js/data/data.js +++ b/packages/perspective-viewer-openlayers/src/js/data/data.js @@ -9,12 +9,13 @@ export function getMapData(config) { const points = []; - - // Enumerate through supplied data + // const cols = config.columns.map(x => ) config.data.forEach((row, i) => { // Exclude "total" rows that don't have all values const groupCount = row.__ROW_PATH__ ? row.__ROW_PATH__.length : 0; - if (groupCount < config.group_by.length) return; + if (groupCount < config.group_by.length) { + return; + } // Get the group from the row path const group = row.__ROW_PATH__ ? row.__ROW_PATH__.join("|") : `${i}`; @@ -36,7 +37,10 @@ export function getMapData(config) { // Add the points for this row to the data set Object.keys(rowPoints).forEach((key) => { const rowPoint = rowPoints[key]; - const cols = config.columns.map((c) => rowPoint[c]); + const cols = config.real_columns.map((c) => + c ? rowPoint[c] : null + ); + points.push({ cols, group: rowPoint.group, @@ -48,22 +52,3 @@ export function getMapData(config) { return points; } - -export function getDataExtents(data) { - let extents = null; - data.forEach((point) => { - if (!extents) { - extents = point.cols.map((c) => ({min: c, max: c})); - } else { - extents = point.cols.map((c, i) => - c - ? { - min: Math.min(c, extents[i].min), - max: Math.max(c, extents[i].max), - } - : extents[i] - ); - } - }); - return extents; -} diff --git a/packages/perspective-viewer-openlayers/src/js/plugin/plugin.js b/packages/perspective-viewer-openlayers/src/js/plugin/plugin.js index 9d090c38cc..b8b0b69f40 100644 --- a/packages/perspective-viewer-openlayers/src/js/plugin/plugin.js +++ b/packages/perspective-viewer-openlayers/src/js/plugin/plugin.js @@ -9,30 +9,45 @@ import mapView from "../views/map-view"; import views from "../views/views"; -import "./template"; +import css from "../../less/plugin.less"; views.forEach((plugin) => { customElements.define( plugin.plugin.type, class extends customElements.get("perspective-viewer-plugin") { + constructor() { + super(); + this.attachShadow({mode: "open"}); + const style = document.createElement("style"); + style.textContent = css; + this.shadowRoot.appendChild(style); + const container = document.createElement("div"); + container.setAttribute("id", "container"); + this.shadowRoot.appendChild(container); + } + async draw(view) { - drawView(plugin).call(this, view); + drawView(plugin).call(this.shadowRoot.children[1], view); } async resize() { - mapView.resize(this); + mapView.resize(this.shadowRoot.children[1]); } get name() { return plugin.plugin.name; } + get category() { + return "OpenStreetMap"; + } + async restyle(view) { - mapView.restyle(this); + mapView.restyle(this.shadowRoot.children[1]); } get select_mode() { - return "select"; + return "toggle"; } get min_config_columns() { @@ -50,64 +65,31 @@ views.forEach((plugin) => { function drawView(viewEntryPoint) { return async function (view) { - const table = this.parentElement - .getTable() - .then((table) => table.schema()); + const table = await this.getRootNode().host.parentElement.getTable(); + + // TODO ue faster serialization method const [tschema, schema, data, config] = await Promise.all([ - table, + table.schema(), view.schema(), view.to_json(), - view.get_config(), + this.getRootNode().host.parentElement.save(), ]); - if (!config.group_by) config.group_by = config.group_by; - if (!config.split_by) config.split_by = config.split_by; - if (!config.aggregate) config.aggregate = config.aggregates; + config.real_columns = config.columns; + config.columns = config.columns.filter((x) => !!x); + + // Enrich color info + if (!!config.real_columns[2]) { + const [min, max] = await view.get_min_max(config.real_columns[2]); + config.color_extents = {min, max}; + } + + // Enrich size info + if (!!config.real_columns[3]) { + const [min, max] = await view.get_min_max(config.real_columns[3]); + config.size_extents = {min, max}; + } viewEntryPoint(this, Object.assign({schema, tschema, data}, config)); }; } - -function resizeView() { - if (this[PRIVATE] && this[PRIVATE].view) { - this[PRIVATE].view.resize(); - } -} - -// function deleteView() { -// if (this[PRIVATE] && this[PRIVATE].view) { -// this[PRIVATE].view.remove(); -// } -// } - -// function save() { -// if (this[PRIVATE] && this[PRIVATE].chart) { -// const perspective_d3fc_element = this[PRIVATE].chart; -// return perspective_d3fc_element.getSettings(); -// } -// } - -// function restore(settings) { -// const perspective_d3fc_element = getOrCreatePlugin.call(this); -// perspective_d3fc_element.setSettings(settings); -// } - -// function getOrCreatePlugin() { -// this[PRIVATE] = this[PRIVATE] || {}; -// if (!this[PRIVATE].view) { -// this[PRIVATE].view = document.createElement(name); -// } - -// return this[PRIVATE].view; -// } - -// function getElement(div) { -// const perspective_d3fc_element = getOrCreatePlugin.call(this); - -// if (!document.body.contains(perspective_d3fc_element)) { -// div.innerHTML = ""; -// div.appendChild(perspective_d3fc_element); -// } - -// return perspective_d3fc_element; -// } diff --git a/packages/perspective-viewer-openlayers/src/js/style/computed.js b/packages/perspective-viewer-openlayers/src/js/style/computed.js index d2368ba5c6..80c7003eb2 100644 --- a/packages/perspective-viewer-openlayers/src/js/style/computed.js +++ b/packages/perspective-viewer-openlayers/src/js/style/computed.js @@ -9,15 +9,9 @@ import {color, rgb} from "d3-color"; export const computedStyle = (container) => { - if (window.ShadyCSS) { - return (name, defaultValue) => - window.ShadyCSS.getComputedStyleValue(container, name) || - defaultValue; - } else { - const containerStyles = getComputedStyle(container); - return (name, defaultValue) => - containerStyles.getPropertyValue(name) || defaultValue; - } + const containerStyles = getComputedStyle(container); + return (name, defaultValue) => + containerStyles.getPropertyValue(name) || defaultValue; }; export const toFillAndStroke = (col) => { diff --git a/packages/perspective-viewer-openlayers/src/js/tooltip/tooltip.js b/packages/perspective-viewer-openlayers/src/js/tooltip/tooltip.js index 9768e204f1..25f4dfbb4a 100644 --- a/packages/perspective-viewer-openlayers/src/js/tooltip/tooltip.js +++ b/packages/perspective-viewer-openlayers/src/js/tooltip/tooltip.js @@ -199,10 +199,14 @@ export function createTooltip(container, map) { const composeAggregates = (cols, fromIndex) => { if (!cols) return ""; - const list = config.columns.slice(fromIndex).map((c, i) => ({ - name: c, - value: cols[i + fromIndex].toLocaleString(), - })); + const list = config.real_columns.slice(fromIndex).map((c, i) => + c === null + ? null + : { + name: c, + value: cols[i + fromIndex].toLocaleString(), + } + ); return composeList(list); }; @@ -232,11 +236,12 @@ export function createTooltip(container, map) { const composeList = (items) => { if (items.length) { - const itemList = items.map( - (item) => - `
  • ${sanitize( - item.name - )}${sanitize(item.value)}
  • ` + const itemList = items.map((item) => + item === null + ? `` + : `
  • ${sanitize( + item.name + )}${sanitize(item.value)}
  • ` ); return `
      ${itemList.join("")}
    `; } diff --git a/packages/perspective-viewer-openlayers/src/js/views/base-map.js b/packages/perspective-viewer-openlayers/src/js/views/base-map.js index 3fd250c700..89a6f64de4 100644 --- a/packages/perspective-viewer-openlayers/src/js/views/base-map.js +++ b/packages/perspective-viewer-openlayers/src/js/views/base-map.js @@ -37,14 +37,14 @@ baseMap.restyle = (container) => { } }; -baseMap.initialiseView = (container, extent) => { - initialiseView(container, extent); +baseMap.initializeView = (container, extent) => { + initializeView(container, extent); }; function getOrCreateMap(container) { if (!container[PRIVATE]) { + // console.log const tileLayer = new TileLayer(); - const map = new Map({ target: container, layers: [tileLayer], @@ -59,20 +59,19 @@ function getOrCreateMap(container) { initialisedExtent: false, }; } - removeVectorLayer(container); setTileUrl(container); return container[PRIVATE]; } -function initialiseView(container, vectorSource) { - if (!container[PRIVATE].initialisedExtent) { - const extents = vectorSource.getExtent(); - const map = container[PRIVATE].map; - map.getView().fit(extents, {size: map.getSize()}); +function initializeView(container, vectorSource) { + // if (!container[PRIVATE].initialisedExtent) { + const extents = vectorSource.getExtent(); + const map = container[PRIVATE].map; + map.getView().fit(extents, {size: map.getSize()}); - container[PRIVATE].initialisedExtent = true; - } + // container[PRIVATE].initialisedExtent = true; + // } } function removeVectorLayer(container) { diff --git a/packages/perspective-viewer-openlayers/src/js/views/map-view.js b/packages/perspective-viewer-openlayers/src/js/views/map-view.js index 3a06472327..61776e593e 100644 --- a/packages/perspective-viewer-openlayers/src/js/views/map-view.js +++ b/packages/perspective-viewer-openlayers/src/js/views/map-view.js @@ -7,7 +7,7 @@ * */ -import {getMapData, getDataExtents} from "../data/data"; +import {getMapData} from "../data/data"; import {baseMap} from "./base-map"; import {categoryColorMap} from "../style/categoryColors"; import {linearColorScale} from "../style/linearColors"; @@ -22,41 +22,39 @@ import {Feature} from "ol"; import {fromLonLat} from "ol/proj"; import {Point} from "ol/geom"; -const MIN_SIZE = 1; +const MIN_SIZE = 2; const MAX_SIZE = 10; const DEFAULT_SIZE = 2; function mapView(container, config) { const data = getMapData(config); - const extents = getDataExtents(data); const map = baseMap(container); - - const useLinearColors = extents.length > 2; + const useLinearColors = !!config.color_extents; const colorScale = useLinearColors - ? linearColorScale(container, extents[2]) + ? linearColorScale(container, config.color_extents) : null; + const colorMap = useLinearColors ? (d) => colorScale(d.cols[2]) : categoryColorMap(container, data); - const sizeMap = sizeMapFromExtents(extents); - const shapeMap = categoryShapeMap(container, data); + const sizeMap = sizeMapFromExtents(config); + const shapeMap = categoryShapeMap(container, data); const vectorSource = new VectorSource({ features: data.map((point) => featureFromPoint(point, colorMap, sizeMap, shapeMap) ), wrapX: false, }); - baseMap.initialiseView(container, vectorSource); + baseMap.initializeView(container, vectorSource); const vectorLayer = new VectorLayer({ source: vectorSource, updateWhileInteracting: true, renderMode: "image", }); - map.map.addLayer(vectorLayer); - // Update the tooltip component + map.map.addLayer(vectorLayer); map.tooltip .config(config) .vectorSource(vectorSource) @@ -65,7 +63,7 @@ function mapView(container, config) { .data(data); if (useLinearColors) { - showLegend(container, colorScale, extents[2]); + showLegend(container, colorScale, config.color_extents); } else { hideLegend(container); } @@ -116,20 +114,22 @@ function onHighlight(feature, highlighted) { }); } -function sizeMapFromExtents(extents) { - if (extents.length > 3) { +function sizeMapFromExtents({size_extents}) { + if (!!size_extents) { // We have the size value - const range = extents[3].max - extents[3].min; + const range = size_extents.max - size_extents.min; return (point) => - ((point.cols[3] - extents[3].min) / range) * (MAX_SIZE - MIN_SIZE) + + ((point.cols[3] - size_extents.min) / range) * + (MAX_SIZE - MIN_SIZE) + MIN_SIZE; } + return () => DEFAULT_SIZE; } mapView.plugin = { - type: "perspective-viewer-map-points", - name: "Map Points", + type: "perspective-viewer-openlayers-scatter", + name: "Map Scatter", max_size: 25000, initial: { type: "number", diff --git a/packages/perspective-viewer-openlayers/src/js/views/region-view.js b/packages/perspective-viewer-openlayers/src/js/views/region-view.js index 390c94d293..975ee912bd 100644 --- a/packages/perspective-viewer-openlayers/src/js/views/region-view.js +++ b/packages/perspective-viewer-openlayers/src/js/views/region-view.js @@ -7,7 +7,7 @@ * */ -import {getMapData, getDataExtents} from "../data/data"; +import {getMapData} from "../data/data"; import {baseMap} from "./base-map"; import {linearColorScale} from "../style/linearColors"; import {showLegend, hideLegend} from "../legend/legend"; @@ -55,7 +55,7 @@ function regionView(container, config) { map.map.addLayer(vectorLayer); vectorSource.on("change", () => { - baseMap.initialiseView(container, vectorSource); + baseMap.initializeView(container, vectorSource); }); // Update the tooltip component diff --git a/packages/perspective-viewer-openlayers/src/js/views/views.js b/packages/perspective-viewer-openlayers/src/js/views/views.js index d4993ae961..17bd25cebf 100644 --- a/packages/perspective-viewer-openlayers/src/js/views/views.js +++ b/packages/perspective-viewer-openlayers/src/js/views/views.js @@ -8,7 +8,8 @@ */ import mapView from "./map-view"; -import regionView from "./region-view"; +// import regionView from "./region-view"; -const views = [mapView, regionView]; +// const views = [mapView, regionView]; +const views = [mapView]; export default views; diff --git a/packages/perspective-viewer-openlayers/src/less/plugin.less b/packages/perspective-viewer-openlayers/src/less/plugin.less index d081244631..9768056b22 100644 --- a/packages/perspective-viewer-openlayers/src/less/plugin.less +++ b/packages/perspective-viewer-openlayers/src/less/plugin.less @@ -20,8 +20,12 @@ & .ol-zoom { left: auto; - top: 4em; - left: 0.5em; + top: 0.5em; + right: 0.5em; + } + + & .ol-attribution { + filter: var(--map-attribution--filter, none); } & .map-tooltip { @@ -64,8 +68,8 @@ & .map-legend { position: absolute; box-sizing: border-box; - top: 3em; - right: 0.75em; + top: 0.5em; + right: 4em; width: 80px; height: 122px; background: var(--map-element-background, #eee); diff --git a/rust/perspective-viewer/src/themes/material-dark.less b/rust/perspective-viewer/src/themes/material-dark.less index 5f628763cb..f1e3140064 100644 --- a/rust/perspective-viewer/src/themes/material-dark.less +++ b/rust/perspective-viewer/src/themes/material-dark.less @@ -65,20 +65,24 @@ perspective-expression-editor[theme="Material Dark"], } .perspective-viewer-material-dark--openlayers { - --map-element-background: #333333; - --map-category-1: #1f77b4; - --map-category-2: #0366d6; - --map-category-3: #ff7f0e; - --map-category-4: #2ca02c; - --map-category-5: #d62728; - --map-category-6: #9467bd; - --map-category-7: #8c564b; - --map-category-8: #e377c2; - --map-category-9: #7f7f7f; - --map-category-10: #bcbd22; - --map-category-11: #17becf; - --map-gradient: linear-gradient(#4d342f 0%, #e4521b 22.5%, #decb45 42.5%, #a0a0a0 50%, #bccda8 57.5%, #42b3d5 67.5%, #1a237e 100%); --map-tile-url: "http://{a-c}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png"; + --map-attribution--filter: invert(1) hue-rotate(180deg); + --map-element-background: @grey700; + --map-category-1: rgb(71, 120, 194); + --map-category-2: rgb(204, 120, 48); + --map-category-3: rgb(158, 84, 192); + --map-category-4: rgb(51, 150, 153); + --map-category-5: rgb(102, 114, 143); + --map-category-6: rgb(211, 103, 189); + --map-category-7: rgb(109, 124, 77); + --map-category-8: rgb(221, 99, 103); + --map-category-9: rgb(120, 104, 206); + --map-category-10: rgb(23, 166, 123); + --map-gradient: linear-gradient( + #dd6367 0%, + #242526 50%, + #3289c8 100% + ); } .perspective-viewer-material-dark--datagrid { diff --git a/rust/perspective-viewer/src/themes/material.less b/rust/perspective-viewer/src/themes/material.less index 59fdaa5ce8..6af1a9a4c5 100644 --- a/rust/perspective-viewer/src/themes/material.less +++ b/rust/perspective-viewer/src/themes/material.less @@ -24,6 +24,7 @@ perspective-viewer[theme="Material Light"], .perspective-viewer-material--intl(); .perspective-viewer-material--d3fc(); .perspective-viewer-material--datagrid(); + .perspective-viewer-material--openlayers(); } perspective-copy-menu[theme="Material Light"], @@ -176,6 +177,30 @@ perspective-expression-editor[theme="Material Light"], ); } +.perspective-viewer-material--openlayers { + --map-tile-url: "http://{a-c}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png"; + --map-element-background: #fff; + --map-category-1: #0366d6; + --map-category-2: #ff7f0e; + --map-category-3: #2ca02c; + --map-category-4: #d62728; + --map-category-5: #9467bd; + --map-category-6: #8c564b; + --map-category-7: #e377c2; + --map-category-8: #7f7f7f; + --map-category-9: #bcbd22; + --map-category-10: #17becf; + --map-gradient: linear-gradient( + #4d342f 0%, + #e4521b 22.5%, + #feeb65 42.5%, + #f0f0f0 50%, + #dcedc8 57.5%, + #42b3d5 67.5%, + #1a237e 100% + ); +} + .perspective-viewer-material--datagrid { --rt-pos-cell--color: @blue300; --rt-neg-cell--color: @red300; From 598b85b6c01b1f0717136fde5588263853d5710c Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Thu, 14 Jul 2022 01:14:45 -0400 Subject: [PATCH 4/5] Smoke tests --- .../package.json | 4 +- .../test/html/superstore.html | 55 +++++++++++++++++++ .../test/js/superstore.spec.js | 32 +++++++++++ .../test/results/results.json | 16 ++++++ 4 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 packages/perspective-viewer-openlayers/test/html/superstore.html create mode 100644 packages/perspective-viewer-openlayers/test/js/superstore.spec.js create mode 100644 packages/perspective-viewer-openlayers/test/results/results.json diff --git a/packages/perspective-viewer-openlayers/package.json b/packages/perspective-viewer-openlayers/package.json index ae1aee9266..0481218d4d 100644 --- a/packages/perspective-viewer-openlayers/package.json +++ b/packages/perspective-viewer-openlayers/package.json @@ -12,8 +12,8 @@ "bench:build": ":", "bench:run": ":", "build": "node build.js", - "test:build": ":", - "test:run": ":", + "test:build": "cpy \"test/html/*\" dist/umd", + "test:run": "jest --rootDir=. --config=../../tools/perspective-test/jest.config.js --color", "test": "npm-run-all test:build test:run", "watch": ":", "clean": "rimraf dist", diff --git a/packages/perspective-viewer-openlayers/test/html/superstore.html b/packages/perspective-viewer-openlayers/test/html/superstore.html new file mode 100644 index 0000000000..5cb6710729 --- /dev/null +++ b/packages/perspective-viewer-openlayers/test/html/superstore.html @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + diff --git a/packages/perspective-viewer-openlayers/test/js/superstore.spec.js b/packages/perspective-viewer-openlayers/test/js/superstore.spec.js new file mode 100644 index 0000000000..5fa76d3f32 --- /dev/null +++ b/packages/perspective-viewer-openlayers/test/js/superstore.spec.js @@ -0,0 +1,32 @@ +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + +const utils = require("@finos/perspective-test"); +const path = require("path"); + +const simple_tests = require("@finos/perspective-viewer/test/js/simple_tests.js"); + +async function get_contents(page) { + return await page.evaluate(async () => { + const viewer = document.querySelector( + "perspective-viewer perspective-viewer-openlayers-scatter" + ); + return viewer.innerHTML || "MISSING"; + }); +} + +utils.with_server({}, () => { + describe.page( + "superstore.html", + () => { + simple_tests.default(get_contents); + }, + {root: path.join(__dirname, "..", "..")} + ); +}); diff --git a/packages/perspective-viewer-openlayers/test/results/results.json b/packages/perspective-viewer-openlayers/test/results/results.json new file mode 100644 index 0000000000..089081a48d --- /dev/null +++ b/packages/perspective-viewer-openlayers/test/results/results.json @@ -0,0 +1,16 @@ +{ + "superstore_shows_a_grid_without_any_settings_applied": "10d1208b485425756fcc932229386b02", + "superstore_displays_visible_columns_": "10d1208b485425756fcc932229386b02", + "superstore_pivot_by_a_row": "10d1208b485425756fcc932229386b02", + "superstore_pivot_by_two_rows": "10d1208b485425756fcc932229386b02", + "superstore_pivot_by_a_column": "10d1208b485425756fcc932229386b02", + "superstore_pivot_by_a_row_and_a_column": "10d1208b485425756fcc932229386b02", + "superstore_pivot_by_two_rows_and_two_columns": "10d1208b485425756fcc932229386b02", + "superstore_sort_by_a_hidden_column": "10d1208b485425756fcc932229386b02", + "superstore_sort_by_a_numeric_column": "10d1208b485425756fcc932229386b02", + "superstore_sort_by_an_alpha_column": "10d1208b485425756fcc932229386b02", + "superstore_filters_filters_by_a_numeric_column": "10d1208b485425756fcc932229386b02", + "superstore_filters_filters_by_an_alpha_column": "10d1208b485425756fcc932229386b02", + "superstore_filters_filters_with__in__comparator": "10d1208b485425756fcc932229386b02", + "__GIT_COMMIT__": "fa4d178e2674dfd4b9edd5594b93df307ae7640c" +} \ No newline at end of file From 739c34a3def1828fa666f3933baf8e9108ef187f Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Thu, 14 Jul 2022 00:49:57 -0400 Subject: [PATCH 5/5] Add `evictions` example --- cpp/perspective/src/cpp/arrow_csv.cpp | 7 +- examples/blocks/gists.json | 3 +- examples/blocks/src/evictions/.block | 1 + examples/blocks/src/evictions/README.md | 1 + examples/blocks/src/evictions/index.html | 123 ++++++++++++++++++++++ examples/blocks/src/evictions/layout.json | 18 ++++ 6 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 examples/blocks/src/evictions/.block create mode 100644 examples/blocks/src/evictions/README.md create mode 100644 examples/blocks/src/evictions/index.html create mode 100644 examples/blocks/src/evictions/layout.json diff --git a/cpp/perspective/src/cpp/arrow_csv.cpp b/cpp/perspective/src/cpp/arrow_csv.cpp index 7444ad6b35..4a254f4582 100644 --- a/cpp/perspective/src/cpp/arrow_csv.cpp +++ b/cpp/perspective/src/cpp/arrow_csv.cpp @@ -78,7 +78,8 @@ namespace apachearrow { public: bool operator()(const char* s, size_t length, arrow::TimeUnit::type out_unit, - int64_t* out, bool* out_zone_offset_present = NULLPTR) const override { + int64_t* out, + bool* out_zone_offset_present = NULLPTR) const override { size_t endptr; std::string val(s, s + length); int64_t value @@ -142,7 +143,8 @@ namespace apachearrow { public: bool operator()(const char* s, size_t length, arrow::TimeUnit::type unit, - int64_t* out, bool* out_zone_offset_present = NULLPTR) const override { + int64_t* out, + bool* out_zone_offset_present = NULLPTR) const override { if (!arrow::internal::ParseTimestampISO8601(s, length, unit, out)) { if (s[length - 1] == 'Z') { @@ -247,6 +249,7 @@ namespace apachearrow { auto convert_options = arrow::csv::ConvertOptions::Defaults(); read_options.use_threads = false; + parse_options.newlines_in_values = true; if (is_update) { convert_options.column_types = std::move(schema); diff --git a/examples/blocks/gists.json b/examples/blocks/gists.json index ff4419aa21..daa77ff2cc 100644 --- a/examples/blocks/gists.json +++ b/examples/blocks/gists.json @@ -3,6 +3,7 @@ "olympics": "efd4a857aca9a52ab6cddbb6e1f701c9", "custom": "c42f3189699bd29cf20bbe7dce767b07", "editable": "45b868833c9f456bd39a51e606412c5d", + "evictions": "fc0cdabe27dc121a1a4038545a9e9b23", "streaming": "9bec2f8041471bafc2c56db2272a9381", "csv": "02d8fd10aef21b19d6165cf92e43e668", "iex": "eb151fdd9f98bde987538cbc20e003f6", @@ -10,4 +11,4 @@ "movies": "6b4dcebf65db4ebe4fe53a6de5ea0b48", "fractal": "5485f6b630b08d38218822e507f09f21", "covid": "e074d7d9e5783e680d35f565d2b4b32e" -} \ No newline at end of file +} diff --git a/examples/blocks/src/evictions/.block b/examples/blocks/src/evictions/.block new file mode 100644 index 0000000000..ad08ee86b5 --- /dev/null +++ b/examples/blocks/src/evictions/.block @@ -0,0 +1 @@ +license: apache-2.0 \ No newline at end of file diff --git a/examples/blocks/src/evictions/README.md b/examples/blocks/src/evictions/README.md new file mode 100644 index 0000000000..4c30a1373a --- /dev/null +++ b/examples/blocks/src/evictions/README.md @@ -0,0 +1 @@ +Demo of [Perspective](https://github.com/finos/perspective), using SF eviciton data from 1997-present provided by [DataSF](https://data.sfgov.org/Housing-and-Buildings/Eviction-Notices/5cei-gny5). diff --git a/examples/blocks/src/evictions/index.html b/examples/blocks/src/evictions/index.html new file mode 100644 index 0000000000..713ab88dd1 --- /dev/null +++ b/examples/blocks/src/evictions/index.html @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/blocks/src/evictions/layout.json b/examples/blocks/src/evictions/layout.json new file mode 100644 index 0000000000..4f3eb71b83 --- /dev/null +++ b/examples/blocks/src/evictions/layout.json @@ -0,0 +1,18 @@ +{ + "plugin": "Map Scatter", + "plugin_config": {}, + "settings": true, + "theme": "Material Dark", + "group_by": ["bucket lon", "bucket lat"], + "split_by": ["neighborhood"], + "columns": ["bucket lon", "bucket lat", null, null, null], + "filter": [["lon", "is not null", null]], + "sort": [], + "expressions": [ + "// lon\nvar x[2];\nindexof(\"shape\", ' .+?( )', x);\nvar y := substring(\"shape\", 7, x[1] - 7);\nfloat(y)", + "// lat\nvar x[2];\nindexof(\"shape\", ' .+?( )', x);\nvar y := substring(\"shape\", x[0], length(\"shape\") - x[1]);\nfloat(y)", + "// bucket lon\nvar x[2];\nindexof(\"shape\", ' .+?( )', x);\nvar y := substring(\"shape\", 7, x[1] - 7);\nbucket(float(y), 0.0025) + 0.00125", + "// bucket lat\nvar x[2];\nindexof(\"shape\", ' .+?( )', x);\nvar y := substring(\"shape\", x[0], length(\"shape\") - x[1]);\nbucket(float(y), 0.0025) + 0.00125" + ], + "aggregates": {"lon": "avg", "bucket lon": "avg", "bucket lat": "avg"} +}