Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add/chart to popup #73

Merged
merged 30 commits into from
Oct 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
212421b
Add basic placeholder chart
4lm Sep 26, 2022
50b0d5f
Merge branch 'dev' into add/chart-to-popup
4lm Sep 27, 2022
fc02664
Merge branch 'dev' into add/chart-to-popup
4lm Oct 4, 2022
98bec1b
Merge branch 'dev' into add/chart-to-popup
4lm Oct 4, 2022
a09766e
Merge branch 'dev' into add/chart-to-popup
4lm Oct 12, 2022
8ec3351
Merge branch 'add-metadata-schema-and-tests' into add/chart-to-popup
4lm Oct 12, 2022
22fef6b
Add layer id in debug mode in console log on map click
4lm Oct 12, 2022
8fdec99
Fetch popup chart data dynamically from static API test endpoint
4lm Oct 12, 2022
d09e23f
Construct coordinates from event
4lm Oct 12, 2022
64b583b
Add all popup segments (fields) to popup and make them configurable v…
4lm Oct 12, 2022
7a6a4f1
Add "js-" prefix to all ids used in popup templates and calling code
4lm Oct 12, 2022
19bc74e
Merge #69 and update API endpoint placeholder JSON to reflect updates…
4lm Oct 13, 2022
957fbc8
Merge branch 'dev' into add/chart-to-popup
4lm Oct 18, 2022
216ea1f
Init working with SCSS in popups
4lm Oct 18, 2022
391ac4c
Update test API JSON and dependent JS implementation to reflect chang…
4lm Oct 18, 2022
8c40b03
start popup styling
bmlancien Oct 18, 2022
29acb93
merge dev and solve conflicts
bmlancien Oct 18, 2022
3d4ce8f
style popup chart
bmlancien Oct 18, 2022
864758d
style popup sources
bmlancien Oct 18, 2022
e697291
update popup text to roboto family
bmlancien Oct 18, 2022
bda8bf0
finish styling popup for desktop
bmlancien Oct 18, 2022
bc13ff6
merge from dev
bmlancien Oct 19, 2022
918472d
Merge branch 'dev' into add/chart-to-popup
bmlancien Oct 19, 2022
bd53070
dynamically populate chart title
bmlancien Oct 19, 2022
bffbdf4
add rounded corners to region value
bmlancien Oct 19, 2022
18e3fe1
solve conflict in changelog
bmlancien Oct 19, 2022
de5e478
add user select none to popup header
bmlancien Oct 19, 2022
1f0cb0e
Add locale numeric rendering to key values components in popups
4lm Oct 20, 2022
db58b70
Merge branch 'dev' into add/chart-to-popup
4lm Oct 20, 2022
42ed4ac
YAGNI, remove unneeded CSS compress block in map HTML template
4lm Oct 20, 2022
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project tries to adhere to [Semantic Versioning](https://semver.org/spe
- results layer with customizable result views

### Changed
- update test API JSON and dependent JS implementation to reflect changes made in #77
- make data in legend dynamically and add legend schema and example
- extend popup schema with key values component
- load satellite layer beneath symbol layers #43
Expand Down
3 changes: 2 additions & 1 deletion digiplan/map/layers.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ class RasterLayerData:
name="Ergebnisse",
name_singular="Ergebnis",
description="",
popup_fields=["name"],
popup_fields=("title", "municipality", "key-values", "chart", "description", "sources"),
# order matters
)
]
}
Expand Down
7 changes: 7 additions & 0 deletions digiplan/static/js/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ function logMessage(msg) {
}
}

function getLanguage() {
// TODO: implement dynamic language determination
// In the future this will properly be implement.
// For now, this function always returns "en"
return "en-US";
}

// TODO: there is another unmerged branch with this util function (popup.js).
// Remove duplicate entry in popup.js after merge.
async function fetchGetJson(url) {
Expand Down
222 changes: 198 additions & 24 deletions digiplan/static/js/popup.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,207 @@
"use strict";

function add_popup(layer_id, fields, template_id="default") {
map.on("click", layer_id, function (e) {
let coordinates;
// Check if popup already exists:
if ($('.mapboxgl-popup').length > 0) {return;}

if ("lat" in e.features[0].properties) {
// Get coordinates from lat/lon:
coordinates = [e.features[0].properties.lat, e.features[0].properties.lon];
} else {
// Get coordinates from geometry:
coordinates = e.lngLat;
async function fetchGetJson(url) {
try {
// Default options are marked with *
const response = await fetch(url, {
method: "GET", // *GET, POST, PUT, DELETE, etc.
mode: "cors", // no-cors, *cors, same-origin
cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
credentials: "same-origin", // include, *same-origin, omit
headers: {
"Content-Type": "application/json",
}, redirect: "follow", // manual, *follow, error
referrerPolicy: "no-referrer", // no-referrer, *client
});
return await response.json(); // parses JSON response into native JavaScript objects
} catch (err) {
if (err instanceof Error) {
throw new Error(err.message);
}
var template = $($("#" + template_id + "_popup").prop("content")).find("div");
var clone = template.clone();
var table = clone.find("table");
for (var label in fields) {
const key = fields[label];
const value = e.features[0].properties[key];
let row = $("<tr>");
row.append($("<td>").text(label));
row.append($("<td>").text(value));
table.append(row);
throw err;
}
}

function createCoordinates(event) {
if ("lat" in event.features[0].properties) {
return [event.features[0].properties.lat, event.features[0].properties.lon];
}
return event.lngLat;
}

function createListByName(name, series) {
let list = [];
for (const item in series) {
list.push(series[item][name]);
}
return list;
}

function add_popup(layer_id, fields, template_id = "default") {
map.on("click", layer_id, function (event) {
/*
Check if popup already exists
*/
if ($('.mapboxgl-popup').length > 0) {
return;
}
new maplibregl.Popup().setLngLat(coordinates).setHTML(clone.html()).addTo(map);

/*
Construct Coordinates From Event
*/
const coordinates = createCoordinates(event);

// TODO: construct dynamically via emitted id by event
const url = "/static/tests/api/popup.json??lookup=population&municipality=12lang=en";

fetchGetJson(url).then(// TODO: for now we assume response has chart. Later determine dynamically.
(response) => {
/*
Construct Popup From Event And Params
*/
const template = document.getElementById("js-" + template_id + "_popup");
const clone = template.content.cloneNode(true);
const html = clone.getElementById("js-" + "popup");
for (const field in fields) {
if (field === "title") {
const titleElement = html.querySelector("#js-popup__title");
const {title} = response;
titleElement.innerHTML = title;
}
if (field === "municipality") {
const municipalityElement = html.querySelector("#js-popup__municipality");
const {municipality} = response;
municipalityElement.innerHTML = `(${municipality})`;
}
if (field === "key-values") {
const lang = getLanguage();
const keyValuesElement = html.querySelector("#js-popup__key-values");
const {
keyValues: {
unit,
year,
municipalityValue,
regionTitle,
regionValue,
}
} = response;
keyValuesElement.innerHTML = `
<span class="key-values__municipality">
<span class="key-values__unit">${unit}</span>
<span class="key-values__year">${year}</span>
<span class="key-values__region-value">${municipalityValue.toLocaleString(lang)}</span>
</span>
<span class="key-values__region">
<span class="key-values__municipality-title">${regionTitle}</span>:
<span class="key-values__municipality-value">${regionValue.toLocaleString(lang)}</span>
</span>
`;
}
if (field === "description") {
Copy link
Collaborator

Choose a reason for hiding this comment

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

If clauses for title, municipality and description almost look the same - maybe refactor to DRY?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I also suggest postponing this to a different PR. Because as you can see in the many TODO comments, this code implementation will change a lot in the future. No stone will be left untouched. But for now, I would leave it like this because it makes IMO little sense to change something that will totally be changed in the future. If I keep the if-clauses, work with switch-cases or some function calls is TBD and would be determined in the future. Is that OK for you?

const descriptionElement = html.querySelector("#js-popup__description");
const {description} = response;
descriptionElement.innerHTML = description;
}
if (field === "chart") {

// Chart Title
const {chart: {title}} = response;

// Chart
const chartElement = html.querySelector("#js-popup__chart");
const chart = echarts.init(chartElement, null, {renderer: 'svg'});
// TODO: use chartType in payload to construct chart dynamically. For now we assume bar chart type.
// TODO: In this fetch we always expect one payload item. Make failsafe.
const {chart: {data: {series}}} = response;
const xAxisData = createListByName("key", series);
const yAxisData = createListByName("value", series);
const option = {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I would suggest loading a chart option style file with named options for different charts from outside.
And afterwards fill remaining fields with data from visualization request.
What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, of course, but not in this PR - I would suggest. This version lacks a proper API and only has one fixed chart type. The code in this PR is a first step, a MacGyver Implementation, held together by a thread, a piece of chewing gum, and some Gaffa tape. I would suggest implementing your suggestion in another PR when we have more data to play with and, therefore, different chart types. As said before, I want to make smaller PRs, to move faster. What do you think? Is this OK for you? I do not insist on my points if they are not absolutely important to me, and this one is NOT absolutely important to me!

title: {
text: title,
textStyle: {
color: '#002E50',
fontSize: 14,
fontWeight: 400,
lineHeight: 16
},
left: 'center'
},
animation: false,
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: 16,
right: 0,
bottom: 32,
top: 48,
containLabel: true
},
textStyle: {
color: '#002E50'
},
xAxis: [{
type: 'category',
data: xAxisData,
axisTick: {
show: false
},
axisLine: {
show: true,
lineStyle: {
color: '#ECF2F6'
}
},
}],
yAxis: [{
type: 'value',
splitLine: {
show: true,
lineStyle: {
color: '#ECF2F6'
}
}
}],
series: [{
name: 'Direct',
type: 'line',
symbol: 'circle',
symbolSize: 6,
data: yAxisData,
lineStyle: {
color: '#002E50'
},
itemStyle: {
color: '#002E50'
}
}]
};
chart.setOption(option);
requestAnimationFrame(() => {
new maplibregl.Popup({
// https://maplibre.org/maplibre-gl-js-docs/api/markers/#popup-parameters
maxWidth: "280px",
}).setLngLat(coordinates).setHTML(html.innerHTML).addTo(map);
});
}
if (field === "sources") {
const sourcesElement = html.querySelector("#js-popup__sources");
let links = [];
for (const index in response.sources) {
const url = response.sources[index].url;
const name = response.sources[index].name;
links.push(`<a href="${url}">${name}</a>`);
}
sourcesElement.innerHTML = `Quellen: ${links.join(", ")}`;
}
}
});
});
}

$(document).ready(function() {
$(document).ready(function () {
$('#js-intro-modal').modal('show');
});
1 change: 1 addition & 0 deletions digiplan/static/scss/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
@import 'components/map_basemaps_control';
@import 'components/modals';
@import 'components/offcanvas';
@import 'components/popup';
@import 'components/tabs';
@import 'components/top-nav';

Expand Down
1 change: 1 addition & 0 deletions digiplan/static/scss/base/_mixins.scss
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
}

// UL
Expand Down
4 changes: 4 additions & 0 deletions digiplan/static/scss/base/_variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ $font-size-xsmall: .75rem; // 12px

$letter-spacing-small: 0.01rem;
$letter-spacing-normal: 0.03rem;
$letter-spacing-large: 0.06rem;

// LEFT PANEL
$panel-width-sm: 22rem;
Expand Down Expand Up @@ -63,5 +64,8 @@ $map-control-position-left: 1rem;
$map-control-z-index: 9;
$map-control-bg-color: rgba(255, 255, 255, 1);

// POPUP
$popup-width: 28rem;

// ICONS
$info-icon-size: 1.25rem;
94 changes: 94 additions & 0 deletions digiplan/static/scss/components/_popup.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
.mapboxgl-popup-content, .maplibregl-popup-content {
@extend .p-0;
width: $popup-width;
font-family: $font-family-sans-serif;

.popup {
&__header {
@extend .bg-light;
@extend .py-2;
@include flex-row-justify-center;
@include user-select-none;
}

&__title,
&__municipality {
@extend .fs-4;
@extend .fw-light;
@extend .mb-0;
letter-spacing: $letter-spacing-large;
}

&__municipality {
@extend .ps-1;
}

&__key-values,
&__chart,
&__description,
&__sources {
padding: $padding-normal;
}

&__key-values {
@extend .d-flex;
@extend .flex-row;
@extend .justify-content-between;
@extend .fs-7;
@extend .border-bottom;
@include user-select-none;

.key-values {
& > span {
@extend .d-block;
}

&__municipality {
@extend .fw-bold;
}

&__region {
color: $gray-600;
}

&__region-value {
@extend .bg-primary;
@extend .text-white;
margin-left: 4px;
}

&__municipality-value {
@extend .bg-light;
}

&__region-value,
&__municipality-value {
padding: 4px 8px;
@extend .rounded;
}
}
}

&__description {
@extend .fw-light;
@extend .fs-7;
}

&__sources {
@extend .border-top;
@extend .lh-sm;

a {
@extend .fw-bold;
}
}
}

.maplibregl-popup-close-button,
.mapboxgl-popup-close-button {
@extend .fs-3;
@extend .fw-light;
@extend .px-2;
@extend .py-1;
}
}
Loading