Skip to content

Commit

Permalink
Merge branch 'refs/heads/dev'
Browse files Browse the repository at this point in the history
  • Loading branch information
henhuy committed May 23, 2024
2 parents d8238ac + 96aa5b2 commit 994bccd
Show file tree
Hide file tree
Showing 11 changed files with 148 additions and 56 deletions.
2 changes: 1 addition & 1 deletion .bumpversion.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tool.bumpversion]
current_version = "1.7.0"
current_version = "1.8.0"
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
serialize = ["{major}.{minor}.{patch}"]
search = "{current_version}"
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ and the versioning aim to respect [Semantic Versioning](http://semver.org/spec/v

Here is a template for new release sections

## [1.8.0] - 2024-05-23
### Added
- docs for popups

### Changed
- step size calculation rounds to 10, 100, 1000 etc.
- default choropleth legend entries to 5

### Fixed
- zero item in choropleth legend
- error in choropleths if value equals "1"

## [1.7.0] - 2024-05-13
### Added
- Middleware to prevent 404 errors for missing MVTs
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,6 @@ Maplibre must be installed (i.e. via npm) and provided as JS framework

# User Guides

- [Layers](docs/LAYERS.md)
- [How to define layers](docs/LAYERS.md)
- [How to enable popups](docs/POPUPS.md)
- [How to set up clusters](docs/CLUSTERS.md)
2 changes: 1 addition & 1 deletion django_mapengine/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Map Engine init, holds version"""

__version__ = "1.7.0"
__version__ = "1.8.0"
31 changes: 20 additions & 11 deletions django_mapengine/choropleth.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

MAX_COLORBREWER_STEPS = 9

DEFAULT_CHOROPLETH_CONFIG = {"color_palette": "YlGnBu", "num_colors": 6}
DEFAULT_CHOROPLETH_CONFIG = {"color_palette": "YlGnBu", "num_colors": 5}


class ChoroplethError(Exception):
Expand Down Expand Up @@ -101,8 +101,8 @@ def __calculate_steps(self, choropleth_config: dict, values: Optional[list] = No
num = choropleth_config["num_colors"]
else:
num = 6
step = (max_value - min_value) / (num - 1)
return [min_value + i * step for i in range(num - 1)] + [max_value]
step_size = self.__calculate_step_size(min_value, max_value, num)
return [min_value + i * step_size for i in range(num)] + [max_value]

if "values" not in choropleth_config:
error_msg = "Values have to be set in style file in order to composite choropleth colors."
Expand Down Expand Up @@ -139,7 +139,7 @@ def get_fill_color(self, name: str, values: Optional[list] = None) -> list:
if len(steps) > MAX_COLORBREWER_STEPS:
error_msg = f"Too many choropleth values given for {name=}."
raise IndexError(error_msg)
colors = colorbrewer.sequential["multihue"][choropleth_config["color_palette"]][len(steps)]
colors = colorbrewer.sequential["multihue"][choropleth_config["color_palette"]][len(steps) - 1]
fill_color = [
"interpolate",
["linear"],
Expand All @@ -151,6 +151,15 @@ def get_fill_color(self, name: str, values: Optional[list] = None) -> list:
fill_color.append(rgb_color)
return fill_color

@staticmethod
def __calculate_step_size(min_value: float, max_value: float, num: int) -> float:
"""
Calculate step size
Algorithm tries to find nice step sizes instead of simply dividing range by number of steps.
"""
return (max_value - min_value) / num

@staticmethod
def __calculate_lower_limit(number: float) -> int:
"""
Expand All @@ -164,20 +173,20 @@ def __calculate_lower_limit(number: float) -> int:
Returns
-------
int
rounded down value by meaningful amount, depending on the size of mini
rounded down value by meaningful amount
Raises
------
ValueError
if lower limit cannot be found
"""
if number == 0:
return number
return int(number)
if number < 1:
return int((number * 10) / 10)
if number > 1:
digits = int(math.log10(number))
return int(number / pow(10, digits)) * 10**digits
if number >= 1:
digits = int(math.log10(number)) + 1
return int(number / 10**digits) * 10**digits
raise ValueError(f"Cannot find lower limit for {number=}")

@staticmethod
Expand All @@ -199,9 +208,9 @@ def __calculate_upper_limit(number: float) -> int:
ValueError
if upper limit cannot be found
"""
if number < 1:
if number <= 1:
return math.ceil((number * 10) / 10)
if number > 1:
digits = int(math.log10(number))
digits = int(math.log10(number)) + 1
return math.ceil(number / 10**digits) * 10**digits
raise ValueError(f"Cannot find upper limit for {number=}")
46 changes: 18 additions & 28 deletions django_mapengine/static/django_mapengine/js/legend.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ const createLegend = (title, unit, colors, valueRanges, nextColumnStartIndex = 3
</div>
<div class="legend__wrap">
<div class="legend__column">
${valueRanges.filter((value, idx) => idx < nextColumnStartIndex).map((value, idx) => `<div class="legend__item" id="legend__item__color-${idx}">${value}</div>`).join(' ')}
${valueRanges.filter((value, idx) => idx <= nextColumnStartIndex).map((value, idx) => `<div class="legend__item" id="legend__item__color-${idx}">${value}</div>`).join(' ')}
</div>
<div class="legend__column">
${valueRanges.filter((value, idx) => idx >= nextColumnStartIndex).map((value, idx) => `<div class="legend__item" id="legend__item__color-${idx + nextColumnStartIndex}">${value}</div>`).join(' ')}
${valueRanges.filter((value, idx) => idx > nextColumnStartIndex).map((value, idx) => `<div class="legend__item" id="legend__item__color-${idx + nextColumnStartIndex}">${value}</div>`).join(' ')}
</div>
</div>
<style>
Expand All @@ -38,44 +38,34 @@ const createLegend = (title, unit, colors, valueRanges, nextColumnStartIndex = 3


function loadLegend(msg, choroplethName){
const title = map_store.cold.choropleths[choroplethName]["title"];
const unit = map_store.cold.choropleths[choroplethName]["unit"];
const title = map_store.cold.choropleths[choroplethName].title;
const unit = map_store.cold.choropleths[choroplethName].unit;
const paintPropertiesPerLayer = map_store.cold.storedChoroplethPaintProperties[choroplethName];

/* Find active layer */
let paintProperties = null;
for (const layerID in paintPropertiesPerLayer) {
const layer = map.getLayer(layerID);
if (map.getZoom() > layer.minzoom && map.getZoom() < layer.maxzoom){
if (layer.visibility === "visible"){
paintProperties = paintPropertiesPerLayer[layerID];
break
break;
}
}
if (paintProperties === null) {
return logMessage(msg);
}

let colors = [];
let values = [];

for (const element in paintProperties["fill-color"]) {
let current = paintProperties["fill-color"][element];

if (typeof(current) == "number") {
if (Number.isInteger(current) === false){
current = current.toFixed(2);
}
const colors = paintProperties["fill-color"].slice(3).filter((_, index) => index % 2 !== 0);
const values = paintProperties["fill-color"].slice(3).filter((_, index) => (index + 1) % 2 !== 0).map(value => value < 100 ? value.toFixed(2) : Math.round(value));

if (values.length === 0) {
values.push("0 - " + String(current));
}
else {
values.push(values[values.length-1].split(" ").slice(-1)[0] + " - " + String(current));
}
}

if (typeof(current) == "string" && current.slice(0,3) === "rgb") {
colors.push(current);
}
let valueRanges = [];
const step_size = parseFloat(values[1]) - parseFloat(values[0]);
for (let i = 0; i < values.length; i++) {
const nextValue = i === values.length - 1 ? parseFloat(values[i]) + step_size : values[i + 1];
valueRanges.push(`${values[i]} - ${nextValue}`);
}

const entriesPerColumn = Math.floor(values.length / 2);
legendElement.innerHTML = createLegend(title, unit, colors, values, entriesPerColumn);
legendElement.innerHTML = createLegend(title, unit, colors, valueRanges, entriesPerColumn);
return logMessage(msg);
}
Empty file added docs/CHOROPLETHS.md
Empty file.
18 changes: 18 additions & 0 deletions docs/POPUPS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
In order to enable popups for your layers you have to define them in the settings using `MAP_ENGINE_POPUPS` parameter.
For each layer in your map you can add a popup which is shown when clicking on the layer.
To set up a popup for a layer you must use `Popup` dataclass from `django_mapengine.setup`.
There you define to which layer the popup belongs.
In order to enable popups for choropleths, which are layered over an existing layer (see section about [choropleths](./CHOROPLETHS.md)),
you can use attribute `choropleths` to give a list of existing choropleths which should have popups.
In order to deactivate popup for layer itself (enabled by default when setting up a popup), you must set `popup_at_default_layer=False`.

NOTE: Order of popups defined under `MAP_ENGINE_POPUPS` matters! If two layers are overlapping, only first popup in list will show up.

Once, popups are defined in settings, each time a user clicks on a layer and a popup event is fired, a backend call is made to
`popup/<str:lookup>/<int:region>` holding the layer ID (or choropleth ID respectively) as `lookup` and clicked feature ID as `region`.
Additionally, all parameters stored in frontend JS in variable `map_store.cold.state` are transferred to backend as well and can be received via `map_state = request.GET.dict()`.
You can make use of this map state variable in your project in order to send additional information to backend, which can be used to customize popups depending on that information.

From there, you can handle popup creation as you like, you only must return HTML at the end, which will be rendered within a popup modal.
Nevertheless, you could use base classes from `django_mapengine.popups` as a starting point for your popups, which are holding basic functionality for
initializing parameters and are offering a simple template system.
66 changes: 64 additions & 2 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "django-mapengine"
version = "1.7.0"
version = "1.8.0"
description = "Map engine for maplibre in django"
authors = ["Hendrik Huyskens <[email protected]>"]
readme = "README.md"
Expand Down Expand Up @@ -29,6 +29,7 @@ isort = "^5.12.0"
flake8 = "^7.0.0"
pylint = "^3.0.3"
pylint-django = "^2.5.5"
pytest = "^8.2.1"

[build-system]
requires = ["poetry-core"]
Expand Down
20 changes: 9 additions & 11 deletions tests/test_choropleths.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,11 @@ def test_choropleths_with_values() -> None:
["linear"],
["feature-state", "with_values"],
0.3,
"rgb(240, 249, 232)",
"rgb(224, 243, 219)",
0.6,
"rgb(186, 228, 188)",
"rgb(168, 221, 181)",
0.8,
"rgb(123, 204, 196)",
1.0,
"rgb(43, 140, 190)",
"rgb(67, 162, 202)",
]


Expand All @@ -35,16 +33,16 @@ def test_choropleths_without_values() -> None:
"interpolate",
["linear"],
["feature-state", "without_values"],
10.0,
0.0,
"rgb(255, 255, 204)",
70.0,
166.66666666666666,
"rgb(199, 233, 180)",
130.0,
333.3333333333333,
"rgb(127, 205, 187)",
190.0,
500.0,
"rgb(65, 182, 196)",
250.0,
666.6666666666666,
"rgb(44, 127, 184)",
310.0,
833.3333333333333,
"rgb(37, 52, 148)",
]

0 comments on commit 994bccd

Please sign in to comment.