"""Utility functions for the creation of interactive maps using BDAP interactive library."""
# Author(s): Davide.De-Marchi@ec.europa.eu
# Copyright © European Union 2022-2023
#
# Licensed under the EUPL, Version 1.2 or as soon they will be approved by
# the European Commission subsequent versions of the EUPL (the "Licence");
#
# You may not use this work except in compliance with the Licence.
#
# You may obtain a copy of the Licence at:
# https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
# Unless required by applicable law or agreed to in writing, software
# distributed under the Licence is distributed on an "AS IS"
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
# express or implied.
#
# See the Licence for the specific language governing permissions and
# limitations under the Licence.
import pandas as pd
import numpy as np
import math
from ipywidgets import widgets, Layout
from ipyleaflet import SearchControl, WidgetControl, LegendControl, ScaleControl, FullScreenControl
try:
from jeodpp import inter, imap
except:
pass
try:
from . import colors
from . import geojsonUtils
except:
import colors
import geojsonUtils
# Custom identify
def CustomIdentifyPopup(map,p):
from ipywidgets import widgets, HTML, CallbackDispatcher
from ipyleaflet import Popup
from IPython.display import display
def handle_interaction_popup(**kwargs):
if kwargs.get('type') == 'click':
pos = [kwargs.get('coordinates')[0],kwargs.get('coordinates')[1]]
message = widgets.HTML()
sid = inter.identifyPointEx(p,pos[1],pos[0],4326,int(map.zoom))
values = sid.split(': ')
if len(values) == 2:
values[1] = str(int(float(values[1])*100) / 100.0)
sid = values[0] + ': ' + values[1]
if len(sid) > 0:
message.value = "<style> p.small {line-height: 1.2; }</style><p class=\"small\">" + sid.replace(",","<br />") + "</p>"
popup = Popup(location=pos,child=message, close_button=True,auto_close=True,close_on_escape_key=True)
map.add_layer(popup)
map._interaction_callbacks = CallbackDispatcher()
map.on_interaction(handle_interaction_popup)
###########################################################################################################################################################################
# Simplified way to create a vector layer displaying the countries of the world.
# Vector data is taken from inter.collections.BaseData.AdministrativeUnits.Global.VirtualEarth.Countries
# Input values to assign to countries come from a Pandas Dataframe df containing a column with iso2code and a column with values
# Returns a Map instance
###########################################################################################################################################################################
[docs]def countriesMap(df, # Pandas dataframe indexed on iso2_code and containing 'value' and 'label' columns
code_column=None, # Name of the column containing the code of the country (None = the country code is the index of the dataframe)
value_column='value', # Name of the column containing the value
codes_selected=[], # codes of the countries selected
center=None, # [lat,lon] to center the map
zoom=None, # initial zoom level for the map
width ='99%', # width of the map
height='400px', # height of the map
min_width=None, # min_width of the map
basemap=1, # Basemap to use
colorlist=['#0d0887', '#46039f', '#7201a8', '#9c179e', '#bd3786', '#d8576b', '#ed7953', '#fb9f3a', '#fdca26', '#f0f921'], # default color scale
stdevnumber=2.0, # Number of stddev to calculate (minvalue,maxvalue) range
stroke='#232323', # stroke color for countries border
stroke_selected='#00ffff', # stroke color for border of selected country
stroke_width=1.0, # border width for countries polygons
decimals=2, # Number of decimals for the legend number display
minallowed_value=None, # Minimum value allowed
maxallowed_value=None): # Maximum value allowed
"""
Creation of an interactive map to display the countries of the world. An input Pandas DataFrame df is used to join a column of numeric values to the countries, using the iso2code (ISO 3166-2) as internal key attribute. Once the values are assigned to the countries, a graduated legend is calculated based on mean and standard deviation of the assigned values. A input list of colors is used to represent the countries given their assigned value.
Note
----
This function is built on top of the BDAP interapro library to display dynamic geospatial dataset. For this reason it is not portable in other environments! Please refer to the module :py:mod:`leafletMap` for geospatial function not related to BDAP.
Parameters
----------
df : Pandas DataFrame
Pandas DataFrame to use for assigning values to the countries. It has to contain at least a column with numeric values.
code_column : str, optional
Name of the column of the Pandas DataFrame containing the unique code of the countries in the ISO-3166-2 standard. This column is used to perform the join with the internal attribute of the countries vector dataset that contains the country code. If the code_column is None, the code is taken from the index of the DataFrame, (default is None)
value_column : str, optional
Name of the column of the Pandas DataFrame containing the values to be assigned to the countries using the join on the ISO-3166-2 codes (default is 'value')
codes_selected : list of strings, optional
List of codes of countries to display as selected (default is [])
center : tuple of (lat,lon), optional
Geographical coordinates of the initial center of the interactive map visualization (default is None)
zoom : int, optional
Initial zoom level of the interactive map (default is None)
width : str, optional
Width of the map widget to create (default is '99%')
height : str, optional
Height of the map widget to create (default is '400px')
min_width : str, optional
Minimum width of the layout of the map widget (default is None)
basemap : int, optional
Basemap to use as background in the map visualization (default is 1). Valid values are in [1,39], see https://jeodpp.jrc.ec.europa.eu/services/processing/interhelp/3.2_map.html?highlight=basemap#inter.Map.printAvailableBasemaps for details
colorlist : list of colors, optional
List of colors to assign to the country polygons (default is the Plotly px.colors.sequential.Plasma, see `Plotly sequential color scales <https://plotly.com/python/builtin-colorscales/#builtin-sequential-color-scales>`_ and `Plotly qualitative color sequences <https://plotly.com/python/discrete-color/#color-sequences-in-plotly-express>`_ )
stdevnumber : float, optional
The correspondance between the values assigned to country polygons and the colors list is done by calculating a range of values [min,max] to linearly map the values to the colors. This range is defined by calculating the mean and standard deviation of the country values and applying this formula [mean - stdevnumber*stddev, mean + stdevnumber*stddev]. Default is 2.0
stroke : str, optional
Color to use for the border of countries (default is '#232323')
stroke_selected : str, optional
Color to use for the border of the selected countries (default is '#00ffff')
stroke_width: float, optional
Width of the border of the country polygons in pixels (default is 1.0)
decimals : int, optional
Number of decimals for the legend numbers display (default is 2)
minallowed_value : float, optional
Minimum value allowed, to force the calculation of the [min,max] range to map the values to the colors
maxallowed_value : float, optional
Maximum value allowed, to force the calculation of the [min,max] range to map the values to the colors
Returns
-------
a jeodpp.imap instance (a Map object derived from the ipyleaflet Map)
Example
-------
Creation of a map displaying a random variable on 4 european countries. The numerical values assigned to each of the countries are randomly generated using numpy.random.uniform and saved into a dictionary having the country code as the key. This dict is transformed to a Pandas DataFrame with 4 rows and having 'iso2code' and 'value' as columns. The graduated legend is build using the 'inverted' Reds Plotly colorscale (low values are dark red, intermediate values are red, high values are white)::
import numpy as np
import pandas as pd
import plotly.express as px
from vois import interMap
countries = ['DE', 'ES', 'FR', 'IT']
# Generate random values and create a dictionary: key=countrycode, value=random in [0.0,100.0]
d = dict(zip(countries, list(np.random.uniform(size=len(countries),low=0.0,high=100.0))))
# Create a pandas dataframe from the dictionary
df = pd.DataFrame(d.items(), columns=['iso2code', 'value'])
m = interMap.countriesMap(df,
code_column='iso2code',
height='400px',
stroke_width=1.5,
stroke_selected='yellow',
colorlist=px.colors.sequential.Reds[::-1],
codes_selected=['IT'])
display(m)
.. figure:: figures/countriesMap.png
:scale: 100 %
:alt: countriesMap example
Example of an interactive map displaying 4 european countries.
"""
if df.shape[0] <= 0:
minvalue = 1.0
maxvalue = 2.0
else:
mean = df[value_column].mean()
if df.shape[0] <= 1:
minvalue = mean
maxvalue = mean
else:
stddev = df[value_column].std()
valuemin = df[value_column].min()
valuemax = df[value_column].max()
minvalue = mean - stdevnumber*stddev
maxvalue = mean + stdevnumber*stddev
if minvalue < valuemin: minvalue = valuemin
if maxvalue > valuemax: maxvalue = valuemax
if not minallowed_value is None:
if minvalue < minallowed_value: minvalue = minallowed_value
if not maxallowed_value is None:
if maxvalue > maxallowed_value: maxvalue = maxallowed_value
#print(mean,stddev, minvalue, maxvalue)
if minvalue >= maxvalue: maxvalue = minvalue + 1
ci = colors.colorInterpolator(colorlist,minvalue,maxvalue)
# Creation of the Map
m = imap.Map(layout=Layout(width=width, height=height), basemap=basemap)
if not min_width is None:
m.layout.min_width = min_width
if not center is None:
m.center = center
if not zoom is None:
m.zoom = zoom
# Add map controls
m.add_control(FullScreenControl(position="topleft"))
m.add_control(SearchControl(position="topleft",url='https://nominatim.openstreetmap.org/search?format=json&q={s}',zoom=12))
m.add_control(ScaleControl(position='bottomright'))
# Seems not to work: to check!
coordlabel = widgets.HTML(value='')
widget_coordinate = WidgetControl(widget=coordlabel, position='bottomleft')
m.add_control(widget_coordinate)
inter.mapInteractGeneric(m, labelCoordinates=coordlabel)
# Layer Countries
v = inter.Collection(inter.collections.BaseData.AdministrativeUnits.Global.VirtualEarth.Countries)
# Join
countries = [str(x) for x in list(df[code_column])]
values = list(df[value_column])
value_ranges = np.linspace(minvalue, maxvalue, 100)
breaks = list(value_ranges) # [1:]
breaks.append(9999999999999)
v.joinAdd('iso_a2', 'value', countries, values) # Doesn't work in Voila!!!
# Define legend
extendedcolors = [ci.GetColor(i) for i in breaks]
v.colorCustom(extendedcolors)
v.legendSet('line', 'stroke-width', str(stroke_width))
v.legendSet('line', 'stroke', str(stroke))
v.legendGraduated('value', 'custom', len(breaks), 2, minvalue, maxvalue, breaks)
v.opacity(255)
# Define the identify
v.parameter('identifyField','admin value')
v.parameter('identifyseparator', ': ')
CustomIdentifyPopup(m, v)
# Add layer to the map
layer = m.addLayer(v.toLayer())
# Add selected countries to the map
if len(codes_selected) > 0:
geoms = v.all('GEOMETRY')
codes = v.all('iso_a2')
for c in codes_selected:
if c in codes:
index = codes.index(c)
geom = geoms[index]
if not geom is None:
s = inter.VectorLayer("wkt")
s.geomAdd(geom)
s.remove('default','all')
s.set('line','stroke', str(stroke_selected))
s.set('line','stroke-width','3')
layersel = m.addLayer(s.toLayer(), name=c)
return m
###########################################################################################################################################################################
# Load a geojson string and returns a inter.VectorLayer object
###########################################################################################################################################################################
[docs]def interGeojsonToVector(geojson):
"""
Load a geojson string and returns a inter.VectorLayer object (see https://jeodpp.jrc.ec.europa.eu/services/processing/interhelp/3.5_vectorlayer.html)
Parameters
----------
geojson : str
String containing data in geojson format
Returns
-------
An instance of the inter.Vector class of the interapro library
Note
----
This function is built on top of the BDAP interapro library to display dynamic geospatial dataset. For this reason it in not portable in other environments!
"""
vector = inter.Collection(inter.collections.Vector)
vector.fileAdd(geojson)
return vector
###########################################################################################################################################################################
# Simplified way to create a vector layer displaying a custom geojson.
# Input values to assign to features come from a Pandas Dataframe df containing a column with unique code and a column with values
# Returns a Map instance
###########################################################################################################################################################################
[docs]def geojsonMap(df, # Pandas dataframe containing 'value' and 'label' columns
geojson_path, # Path of the geojson containing the geographic features
geojson_attribute, # Name of the attribute of the geojson containing the unique code
code_column=None, # Name of the column containing the code of the features (None = the feature code is the index of the dataframe)
value_column='value', # Name of the column containing the value
codes_selected=[], # codes of the features selected
center=None, # [lat,lon] to center the map
zoom=None, # initial zoom level for the map
width ='99%', # width of the map
height='400px', # height of the map
min_width=None, # min_width of the map
basemap=1, # Basemap to use
colorlist=['#0d0887', '#46039f', '#7201a8', '#9c179e', '#bd3786', '#d8576b', '#ed7953', '#fb9f3a', '#fdca26', '#f0f921'], # default color scale
stdevnumber=2.0, # Number of stddev to calculate (minvalue,maxvalue) range
stroke='#232323', # stroke color for polygons border
stroke_selected='#00ffff', # stroke color for border of selected polygons
stroke_width=1.0, # border width for polygons
decimals=2, # Number of decimals for the legend number display
minallowed_value=None, # Minimum value allowed
maxallowed_value=None): # Maximum value allowed
"""
Creation of an interactive map to display a custom geojson dataset. An input Pandas DataFrame df is used to join a column of numeric values to the geojson features, using the <geojson_attribute> as the internal key attribute. Once the values are assigned to the features, a graduated legend is calculated based on mean and standard deviation of the assigned values. A input list of colors is used to represent the featuress given their assigned value.
Note
----
This function is built on top of the BDAP interapro library to display dynamic geospatial dataset. For this reason it is not portable in other environments! Please refer to the module :py:mod:`lefletMap` for geospatial function not related to BDAP.
Parameters
----------
df : Pandas DataFrame
Pandas DataFrame to use for assigning values to features. It has to contain at least a column with numeric values.
geojson_path : str
Path of the geojson file to load that contains the geographic features in geojson format
geojson_attribute : str
Name of the attribute of the geojson dataset that contains the unique codes of the features. This attribute will be use as internal key in the join operation with the df Pandas DataFrame
code_column : str, optional
Name of the column of the df Pandas DataFrame containing the unique code of the features. This column is used to perform the join with the internal attribute of the geojson vector dataset that contains the unique code. If the code_column is None, the code is taken from the index of the DataFrame, (default is None)
value_column : str, optional
Name of the column of the Pandas DataFrame containing the values to be assigned to the features using the join on geojson unique codes (default is 'value')
codes_selected : list of strings, optional
List of codes of features to display as selected (default is [])
center : tuple of (lat,lon), optional
Geographical coordinates of the initial center of the interactive map visualization (default is None)
zoom : int, optional
Initial zoom level of the interactive map (default is None)
width : str, optional
Width of the map widget to create (default is '99%')
height : str, optional
Height of the map widget to create (default is '400px')
min_width : str, optional
Minimum width of the layout of the map widget (default is None)
basemap : int, optional
Basemap to use as background in the map visualization (default is 1). Valid values are in [1,39], see https://jeodpp.jrc.ec.europa.eu/services/processing/interhelp/3.2_map.html?highlight=basemap#inter.Map.printAvailableBasemaps for details
colorlist : list of colors, optional
List of colors to assign to the country polygons (default is the Plotly px.colors.sequential.Plasma, see `Plotly sequential color scales <https://plotly.com/python/builtin-colorscales/#builtin-sequential-color-scales>`_ and `Plotly qualitative color sequences <https://plotly.com/python/discrete-color/#color-sequences-in-plotly-express>`_ )
stdevnumber : float, optional
The correspondance between the values assigned to features and the colors list is done by calculating a range of values [min,max] to linearly map the values to the colors. This range is defined by calculating the mean and standard deviation of the country values and applying this formula [mean - stdevnumber*stddev, mean + stdevnumber*stddev]. Default is 2.0
stroke : str, optional
Color to use for the border of countries (default is '#232323')
stroke_selected : str, optional
Color to use for the border of the selected countries (default is '#00ffff')
stroke_width: float, optional
Width of the border of the country polygons in pixels (default is 1.0)
decimals : int, optional
Number of decimals for the legend numbers display (default is 2)
minallowed_value : float, optional
Minimum value allowed, to force the calculation of the [min,max] range to map the values to the colors
maxallowed_value : float, optional
Maximum value allowed, to force the calculation of the [min,max] range to map the values to the colors
Returns
-------
a jeodpp.imap instance (a Map object derived from the ipyleaflet Map)
Example
-------
Creation of a map displaying a custom geojson. The numerical values assigned to each of the countries are randomly generated using numpy.random.uniform and saved into a dictionary having the country code as the key. This dict is transformed to a Pandas DataFrame with 4 rows and having 'iso2code' and 'value' as columns. The graduated legend is build using the 'inverted' Reds Plotly colorscale (low values are dark red, intermediate values are red, high values are white)::
import numpy as np
import pandas as pd
import plotly.express as px
from vois import interMap
countries = ['DE', 'ES', 'FR', 'IT']
# Generate random values and create a dictionary: key=countrycode, value=random in [0.0,100.0]
d = dict(zip(countries, list(np.random.uniform(size=len(countries),low=0.0,high=100.0))))
# Create a pandas dataframe from the dictionary
df = pd.DataFrame(d.items(), columns=['iso2code', 'value'])
m = interMap.geojsonMap(df,
'./data/ne_50m_admin_0_countries.geojson',
'ISO_A2_EH', # Internal attribute used as key
code_column='iso2code',
height='400px',
stroke_width=1.5,
stroke_selected='yellow',
colorlist=px.colors.sequential.Reds[::-1],
codes_selected=['IT'])
display(m)
.. figure:: figures/geojsonMap.png
:scale: 100 %
:alt: geojsonMap example
Example of an interactive map displaying 4 european countries from a custom geojson file.
"""
if df.shape[0] <= 0:
minvalue = 1.0
maxvalue = 2.0
else:
mean = df[value_column].mean()
if df.shape[0] <= 1:
minvalue = mean
maxvalue = mean
else:
stddev = df[value_column].std()
valuemin = df[value_column].min()
valuemax = df[value_column].max()
minvalue = mean - stdevnumber*stddev
maxvalue = mean + stdevnumber*stddev
if minvalue < valuemin: minvalue = valuemin
if maxvalue > valuemax: maxvalue = valuemax
if not minallowed_value is None:
if minvalue < minallowed_value: minvalue = minallowed_value
if not maxallowed_value is None:
if maxvalue > maxallowed_value: maxvalue = maxallowed_value
#print(mean,stddev, minvalue, maxvalue)
if minvalue >= maxvalue: maxvalue = minvalue + 1
ci = colors.colorInterpolator(colorlist,minvalue,maxvalue)
# Creation of the Map
m = imap.Map(layout=Layout(width=width, height=height), basemap=basemap)
if not min_width is None:
m.layout.min_width = min_width
if not center is None:
m.center = center
if not zoom is None:
m.zoom = zoom
# Add map controls
m.add_control(FullScreenControl(position="topleft"))
m.add_control(SearchControl(position="topleft",url='https://nominatim.openstreetmap.org/search?format=json&q={s}',zoom=12))
m.add_control(ScaleControl(position='bottomright'))
# Seems not to work: to check!
coordlabel = widgets.HTML(value='')
widget_coordinate = WidgetControl(widget=coordlabel, position='bottomleft')
m.add_control(widget_coordinate)
inter.mapInteractGeneric(m, labelCoordinates=coordlabel)
# Layer
geojson = geojsonUtils.geojsonLoadFile(geojson_path)
# Join
countries = [str(x) for x in list(df[code_column])]
values = list(df[value_column])
value_ranges = np.linspace(minvalue, maxvalue, 100)
breaks = list(value_ranges) # [1:]
breaks.append(9999999999999)
d = dict(zip(countries,values))
geojsonnew = geojsonUtils.geojsonJoin(geojson,geojson_attribute, 'value', d, innerMode=True)
v = interGeojsonToVector(geojsonnew)
# Define legend
extendedcolors = [ci.GetColor(i) for i in breaks]
v.colorCustom(extendedcolors)
v.legendSet('line', 'stroke-width', str(stroke_width))
v.legendSet('line', 'stroke', str(stroke))
v.legendGraduated('value', 'custom', len(breaks), 2, minvalue, maxvalue, breaks)
v.opacity(255)
# Define the identify
v.parameter('identifyField',geojson_attribute + ' value')
v.parameter('identifyseparator', ': ')
CustomIdentifyPopup(m, v)
# Add layer to the map
p = v.process()
layer = m.addLayer(v.toLayer())
# Add selected countries to the map
if len(codes_selected) > 0:
filtered = geojsonUtils.geojsonFilter(geojsonnew, geojson_attribute, codes_selected)
if geojsonUtils.geojsonCount(filtered) > 0:
s = inter.Collection(inter.collections.Vector)
s.fileAdd(filtered)
s.remove('default','all')
s.set('line','stroke', str(stroke_selected))
s.set('line','stroke-width','3')
layersel = m.addLayer(s.toLayer(), name='selected')
return m
###########################################################################################################################################################################
# Sets a bivariate legend for vector layer v
# Returns a legend in SVG string format
###########################################################################################################################################################################
[docs]def bivariateLegend(v,
filters1,
filters2,
colorlist1,
colorlist2,
title='',
title1='',
title2='',
names1=[],
names2=[],
fontsize=14,
fontweight=400,
stroke='#000000',
stroke_width=0.25,
side=100,
resizewidth='',
resizeheight=''):
"""
Creation of a bivariate choropleth legend for a polygon vector layer. See `Bivariate Choropleth Maps: A How-to Guide <https://www.joshuastevens.net/cartography/make-a-bivariate-choropleth-map/>`_ for the idea. The function creates a legend for vector layer v based on two attributes of the layer and returns a string containing the SVG representation of the legend (that can be displayed using display(HTML(svgstring) call)
Note
----
This function is built on top of the BDAP interapro library to display dynamic geospatial dataset. For this reason it is not portable in other environments! Please refer to the module :py:mod:`lefletMap` for geospatial function not related to BDAP.
Parameters
----------
v : instance of inter.VectorLayer class
Vector layer instance for which the bivariate legend has to be built
filters1 : list of strings
List of strings defining the conditions for the classes based on the first attribute
filters2 : list of strings
List of strings defining the conditions for the classes based on the second attribute
colorlist1 : list of colors
List of colors to use for the legend on the first attribute (see `Plotly sequential color scales <https://plotly.com/python/builtin-colorscales/#builtin-sequential-color-scales>`_ and `Plotly qualitative color sequences <https://plotly.com/python/discrete-color/#color-sequences-in-plotly-express>`_ )
colorlist2 : list of colors
List of colors to use for the legend on the second attribute (see `Plotly sequential color scales <https://plotly.com/python/builtin-colorscales/#builtin-sequential-color-scales>`_ and `Plotly qualitative color sequences <https://plotly.com/python/discrete-color/#color-sequences-in-plotly-express>`_ )
title : str, optional
Main title of the legend chart (default is '')
title1 : str, optional
Title for the legend on the first attribute. It will be displayed vertically in the Y axis of the SVG. Default is ''.
title2 : str, optional
Title for the legend on the second attribute. It will be displayed horizontally in the X axis of the SVG. Default is ''.
names1 : list of strings, optional
List containing one string for each of the classes of the legend on the first attribute (default is [])
names2 : list of strings, optional
List containing one string for each of the classes of the legend on the second attribute (default is [])
fontsize : int, optional
Size in pixels of the font used for texts (default is 14)
fontweight : int, optional
Weight of the font used for title texts (default is 400)
stroke : str, optional
Color to use for the border of the polygons (default is '#000000')
stroke_width : float, optional
Width in pixels of the stroke to use for the border of the polygons (default is 0.25)
side : int, optional
Side in pixels of the squares displayed in the SVG legend (default is 100)
resizewidth : str, optional
Width of the resizing container (default is '')
resizeheight : str, optional
height of the resizing container (default is '')
Returns
-------
a string containing SVG text to display the bivariate legend using a call to display(HTML(...))
Example
-------
Creation of a bivariate choropleth legend for the polygons of the Italian provinces. The first attribute is the short name of the province (attribute 'SIGLA'), and the second attribute is the SHAPE_AREA attribute which contains the dimension in squared meters::
from ipywidgets import widgets, Layout, HTML
from IPython.display import display
from jeodpp import inter, imap
from vois import interMap, geojsonUtils
# Load data on italian provinces
geojson = geojsonUtils.geojsonLoadFile('./data/ItalyProvinces.geojson')
vector = interMap.interGeojsonToVector(geojson)
vector = vector.parameter("identifyfield", "SIGLA DEN_PROV SHAPE_AREA")
vector = vector.parameter("identifyseparator", "<br>")
# Create and display a Map instance
m = imap.Map(basemap=1, layout=Layout(height='600px'))
display(m)
# Creation of the bivariate legend
colorlist1 = ['#f3f3f3', '#eac5dd', '#e6a3d0']
colorlist2 = ['#f3f3f3', '#c2f1d5', '#8be2ae']
svg = interMap.bivariateLegend(vector,
["[SIGLA] < 'FE'", "[SIGLA] >= 'FE' and [SIGLA] <= 'PU'", "[SIGLA] > 'PU'"],
["[SHAPE_AREA] < 2500000000", "[SHAPE_AREA] >= 2500000000 and [SHAPE_AREA] <= 4500000000", "[SHAPE_AREA] > 4500000000 and [SHAPE_AREA] <= 7500000000", '[SHAPE_AREA] > 7500000000'],
colorlist1,
colorlist2,
title='Example of Bivariate Choropleth',
title1="Province initials",
names1=['< FE', 'in [FE,PU]', '> PU'],
title2="Province area",
names2=['Small', 'Medium', 'Large', 'XLarge'],
fontsize=24,
fontweight=500)
# Display of the vector layer on the map
p = vector.process()
m.clear()
m.addLayer(p.toLayer())
m.zoomToImageExtent(p)
inter.identifyPopup(m,p)
# Display the bivariate choropleth legend
display(HTML(svg))
.. figure:: figures/bivariate.png
:scale: 100 %
:alt: Example of bivariate choropleth legend
Example of an interactive map showing polygons colored with a bivariate choropleth legend.
"""
n1 = len(filters1)
n2 = len(filters2)
v.reset()
width = str(stroke_width)
if n1 > 1 and n2 > 1:
ci1 = colors.colorInterpolator(colorlist1, 0.0, n1-1)
ci2 = colors.colorInterpolator(colorlist2, 0.0, n2-1)
displ = 2*fontsize
h = displ + side * n1
w = displ + side * n2
ydispl = 0
if len(title) > 0:
ydispl = 0.8*displ
h += displ
if len(resizewidth) <= 0: resizewidth = '%fpx' % w
if len(resizeheight) <= 0: resizeheight = '%fpx' % h
svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 %d %d" preserveAspectRatio="xMidYMid meet" width="%s" height="calc(%s + 20px)" version="1.1">\n' % (w,h, resizewidth,resizeheight)
if len(title) > 0:
svg += '<text x="%f" y="%f" text-anchor="middle" font-size="%f" fill="black" font-weight="%d" >%s</text>' % (displ+0.5*n2*side, 1.1*fontsize, 1.05*fontsize, fontweight, title)
svg += '<text x="0" y="0" text-anchor="middle" font-size="%f" fill="black" font-weight="%d" transform="translate(%f,%f) rotate(-90)">%s</text>' % (fontsize, fontweight, 0.8*fontsize, ydispl+0.5*n1*side, title1)
svg += '<text x="%f" y="%f" text-anchor="middle" font-size="%f" fill="black" font-weight="%d" >%s</text>' % (displ+0.5*n2*side, ydispl+2.1*fontsize+n1*side, fontsize, fontweight, title2)
for f1 in filters1:
row = filters1.index(f1)
c1 = ci1.GetColor(row)
if row < len(names1):
svg += '<text x="0" y="0" text-anchor="middle" font-size="%f" fill="black" font-weight="%d" transform="translate(%f,%f) rotate(-90)">%s</text>' % (0.7*fontsize, 400, 1.8*fontsize, ydispl+(n1-row-0.5)*side, names1[row])
for f2 in filters2:
col = filters2.index(f2)
c2 = ci2.GetColor(col)
cm = colors.darken(c1,c2)
f = '(' + f1 + ') and ' + '(' + f2 + ')'
#print(f, colors.string2rgb(c1), colors.string2rgb(c2), cm)
v.set(f,'poly','fill',cm)
v.set(f,'line','stroke',stroke)
v.set(f,'line','stroke-width',width)
svg += '<rect style="fill:%s;" x="%d" y="%d" width="%d" height="%d" stroke-width="0"><title>%s</title></rect>\n' % (cm, displ+col*side, ydispl+(n1-row-1)*side, side, side, f)
for f2 in filters2:
col = filters2.index(f2)
if col < len(names2):
svg += '<text x="%f" y="%f" text-anchor="middle" font-size="%f" fill="black" font-weight="%d" >%s</text>' % (displ+(col+0.5)*side, ydispl+0.75*fontsize+n1*side, 0.7*fontsize, 400, names2[col])
svg += '</svg>'
return svg
###########################################################################################################################################################################
# Sets a simple trivariate legend (defined by 7 colors) using 3 attributes of a vector layer v
# Returns a legend in SVG string format
###########################################################################################################################################################################
[docs]def trivariateLegend(v,
filter1,
filter2,
filter3,
color1='#ff60ff',
color2='#ffff60',
color3='#60ffff',
color4='#ffffff',
title='',
title1='',
title2='',
title3='',
fontsize=14,
fontweight=300,
stroke='#000000',
stroke_width=0.25,
radius=100):
"""
Creation of a trivariate choropleth legend for a polygon vector layer. See `Some Thoughts on Multivariate Maps <stamen.com/some-thoughts-on-multivariate-maps-ffe364342415/>`_ for the idea. The function creates a legend for vector layer v based on three attributes of the layer and returns a string containing the SVG representation of the legend (that can be displayed using display(HTML(svgstring) call).
Note
----
This function is built on top of the BDAP interapro library to display dynamic geospatial dataset. For this reason it is not portable in other environments! Please refer to the module :py:mod:`lefletMap` for geospatial function not related to BDAP.
Parameters
----------
v : instance of inter.VectorLayer class
Vector layer instance for which the trivariate legend has to be built
filter1 : str
Condition to filter the polygons on the first attribute
filter2 : str
Condition to filter the polygons on the second attribute
filter3 : str
Condition to filter the polygons on the third attribute
color1 : str, optional
Color to assign to polygons that satisfy the condition on the first attribute (default is '#ff80ff')
color2 : str, optional
Color to assign to polygons that satisfy the condition on the second attribute (default is '#ffff80')
color3 : str, optional
Color to assign to polygons that satisfy the condition on the third attribute (default is '#ff80ff')
color4 : str, optional
Color to assign to polygons that do not satisfy any of the three conditions (default is '#ffffff')
title : str, optional
Main title of the legend chart (default is '')
title1 : str, optional
Title for the legend on the first attribute (default is '')
title2 : str, optional
Title for the legend on the second attribute (default is '')
title3 : str, optional
Title for the legend on the third attribute (default is '')
fontsize : int, optional
Size in pixels of the font used for texts (default is 14)
fontweight : int, optional
Weight of the font used for title texts (default is 400)
stroke : str, optional
Color to use for the border of the polygons (default is '#000000')
stroke_width : float, optional
Width in pixels of the stroke to use for the border of the polygons (default is 0.25)
radius : int, optional
Radius in pixels of the circles displayed in the SVG legend (default is 100)
Returns
-------
a string containing SVG text to display the trivariate legend using a call to display(HTML(...))
Example
-------
Creation of a simple trivariate choropleth legend (with 7 colors) for a polygons layer containing crop data. The three attributes AL_PERC, PC_PERC and PG_PERC contain the percentage presence of the three specific crops inside the polygon::
from ipywidgets import widgets, Layout, HTML
from IPython.display import display
from jeodpp import inter, imap
from vois import interMap
# Load data
vector = inter.loadLocalVector("DEBY_2019_LandCover.shp")
vector = vector.parameter("identifyfield", "LAU_NAME YEAR AL_PERC PC_PERC PG_PERC")
vector = vector.parameter("identifyseparator", "<br>")
# Create and display a Map instance
m = imap.Map(basemap=60, layout=Layout(height='600px'))
display(m)
# Creation of the bivariate legend
svg = interMap.trivariateLegend(vector,
"[AL_PERC] > 60",
"[PC_PERC] > 10",
"[PG_PERC] > 20",
'#ff60ff',
'#ffff60',
'#60ffff',
'#ffffff55',
title='Example of Trivariate Choropleth',
title1="Arable Land",
title2="Perm. Crop",
title3="Permanent Grassland",
fontsize=12,
fontweight=500,
radius=70)
# Display of the vector layer on the map
p = vector.process()
m.clear()
m.addLayer(p.toLayer())
m.zoomToImageExtent(p)
inter.identifyPopup(m,p)
# Display the trivariate choropleth legend
display(HTML(svg))
.. figure:: figures/trivariate.png
:scale: 100 %
:alt: Example of trivariate choropleth legend
Example of an interactive map showing polygons colored with a trivariate choropleth legend.
"""
width = str(stroke_width)
# Set the legend
v.reset()
v.set('all','poly','fill',color4)
v.set(filter2,'poly','fill',color3)
v.set(filter2,'line','stroke',stroke)
v.set(filter2,'line','stroke-width',width)
v.set(filter2,'poly','fill',color2)
v.set(filter2,'line','stroke',stroke)
v.set(filter2,'line','stroke-width',width)
v.set(filter1,'poly','fill',color1)
v.set(filter1,'line','stroke',stroke)
v.set(filter1,'line','stroke-width',width)
f = '(' + filter1 + ') and ' + '(' + filter2 + ')'
c = colors.darken(color1,color2)
v.set(f,'poly','fill',c)
v.set(f,'line','stroke',stroke)
v.set(f,'line','stroke-width',width)
f = '(' + filter1 + ') and ' + '(' + filter3 + ')'
c = colors.darken(color1,color3)
v.set(f,'poly','fill',c)
v.set(f,'line','stroke',stroke)
v.set(f,'line','stroke-width',width)
f = '(' + filter2 + ') and ' + '(' + filter3 + ')'
c = colors.darken(color2,color3)
v.set(f,'poly','fill',c)
v.set(f,'line','stroke',stroke)
v.set(f,'line','stroke-width',width)
f = '(' + filter1 + ') and ' + '(' + filter2 + ') and ' + '(' + filter3 + ')'
c = colors.darken(color1,colors.darken(color2,color3))
v.set(f,'poly','fill',c)
v.set(f,'line','stroke',stroke)
v.set(f,'line','stroke-width',width)
# Create the SVG
displ = 1.5*fontsize
h = displ + 2.6*radius + displ
w = displ + 2.6*radius + displ
ydispl = 0
if len(title) > 0:
ydispl = 0.8*displ
h += displ
svg = '<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" version="1.1">\n' % (w, h)
if len(title) > 0:
svg += '<text x="%f" y="%f" text-anchor="middle" font-size="%f" fill="black" font-weight="%d" >%s</text>' % (0.5*w, 1.1*fontsize, 1.05*fontsize, fontweight, title)
svg += '<circle cx="%f" cy="%f" r="%f" fill="%s"></circle>' % (0.333*w, 0.666*h, radius, color1)
svg += '<circle cx="%f" cy="%f" r="%f" fill="%s" style="mix-blend-mode: darken;"></circle>' % (0.666*w, 0.666*h, radius, color2)
svg += '<circle cx="%f" cy="%f" r="%f" fill="%s" style="mix-blend-mode: darken;"></circle>' % (0.500*w, 0.390*h, radius, color3)
svg += '<text x="%f" y="%f" text-anchor="end" font-size="%f" fill="black" font-weight="%d" >%s</text>' % (0.333*w, 0.666*h+fontsize, fontsize, fontweight, title1)
svg += '<text x="%f" y="%f" text-anchor="start" font-size="%f" fill="black" font-weight="%d" >%s</text>' % (0.666*w, 0.666*h+fontsize, fontsize, fontweight, title2)
svg += '<text x="%f" y="%f" text-anchor="middle" font-size="%f" fill="black" font-weight="%d" >%s</text>' % (0.500*w, 0.390*h-1.5*fontsize, fontsize, fontweight, title3)
svg += '</svg>'
return svg
###########################################################################################################################################################################
# Sets a complex trivariate legend for vector layer v based on three numerical attributes
# Returns a legend in SVG string format
###########################################################################################################################################################################
[docs]def trivariateLegendEx(v,
attribute1,
attribute2,
attribute3,
n=3,
min1=0.0,
max1=100.0,
min2=0.0,
max2=100.0,
min3=0.0,
max3=100.0,
color1='#ff80f7',
color2='#00d1d0',
color3='#cfb000',
color4='#ffffff',
title='',
title1='',
title2='',
title3='',
fontsize=14,
fontweight=400,
stroke='#000000',
stroke_width=0.25,
side=200,
resizewidth='',
resizeheight='',
digits=2,
maxticks=0,
showarrows=True):
"""
Creation of a trivariate choropleth legend for a polygon vector layer. See `Choropleth maps with tricolore <https://cran.r-project.org/web/packages/tricolore/vignettes/choropleth_maps_with_tricolore.html>`_ for the idea. The function creates a legend for vector layer v based on three attributes of the layer and returns a string containing the SVG representation of the legend in the form of a triangle (that can be displayed using display(HTML(svgstring) call)
Note
----
This function is built on top of the BDAP interapro library to display dynamic geospatial dataset. For this reason it is not portable in other environments! Please refer to the module :py:mod:`lefletMap` for geospatial function not related to BDAP.
Parameters
----------
v : instance of inter.VectorLayer class
Vector layer instance for which the trivariate legend has to be built
attribute1 : str
Name of the first numerical attribute
attribute2 : str
Name of the second numerical attribute
attribute3 : str
Name of the third numerical attribute
n : int, optional
Number of intervals for each of the three numerical attributes (default is 3). Accepptable values are those in the range [2,10]
min1 : float, optional
Minimum value for the first attribute (default is 0.0)
max1 : float, optional
Maximum value for the first attribute (default is 100.0)
min2 : float, optional
Minimum value for the second attribute (default is 0.0)
max2 : float, optional
Maximum value for the second attribute (default is 100.0)
min3 : float, optional
Minimum value for the third attribute (default is 0.0)
max3 : float, optional
Maximum value for the third attribute (default is 100.0)
color1 : str, optional
Color to assign to polygons that have the maximum value on the first attribute (default is '#ff80f7')
color2 : str, optional
Color to assign to polygons that have the maximum value on the second attribute (default is '#00d1d0')
color3 : str, optional
Color to assign to polygons that have the maximum value on the third attribute (default is '#cfb000')
color4 : str, optional
Color to assign to polygons that have all the three values of the attributes smaller than the corresponding minimal value (default is '#ffffff'). For this color, the transparency can be set, for instance using '#ffffff88' for partial transparency or '#ffffffff' for full transparency.
title : str, optional
Main title of the legend chart (default is '')
title1 : str, optional
Title for the legend on the first attribute. It will be displayed on the bottom side of the triangle SVG. Default is ''.
title2 : str, optional
Title for the legend on the second attribute. It will be displayed on the right side of the triangle SVG. Default is ''.
title3 : str, optional
Title for the legend on the third attribute. It will be displayed on the left side of the triangle SVG. Default is ''.
fontsize : int, optional
Size in pixels of the font used for texts (default is 14)
fontweight : int, optional
Weight of the font used for title texts (default is 400)
stroke : str, optional
Color to use for the border of the polygons (default is '#000000')
stroke_width : float, optional
Width in pixels of the stroke to use for the border of the polygons (default is 0.25)
side : int, optional
Dimension in pixels of one side of the triangle displayed in the SVG legend (default is 200)
resizewidth : str, optional
Width of the resizing container (default is '')
resizeheight : str, optional
height of the resizing container (default is '')
digits : int, optional
Number of decimal digits to use for displaying numerical values on the axis of the SVG chart (default is 2)
maxticks: int, optional
Maximum number of tick marks to display on each of the triangle sides. If 0 or less, the ticks for all the intervals are shown. Default is 0
showarrows : bool, optional
If True displays small arrows to help identify the three axes (default is True)
Returns
-------
a string containing SVG text to display the trivariate legend using a call to display(HTML(...))
Example
-------
Creation of a complex trivariate choropleth legend for a polygons layer containing crop data. The three attributes AL_PERC, PC_PERC and PG_PERC contain the percentage presence of the three specific crops inside the polygon::
from ipywidgets import widgets, Layout, HTML
from IPython.display import display
from jeodpp import inter, imap
from vois import interMap
# Load data
vector = inter.loadLocalVector("DEBY_2019_LandCover.shp")
vector = vector.parameter("identifyfield", "LAU_NAME YEAR AL_PERC PC_PERC PG_PERC")
vector = vector.parameter("identifyseparator", "<br>")
# Create and display a Map instance
m = imap.Map(basemap=60, layout=Layout(height='600px'))
display(m)
svg = interMap.trivariateLegendEx(vector,
"AL_PERC",
"PC_PERC",
"PG_PERC",
6,
0.0,
100.0,
0.0,
100.0,
0.0,
100.0,
color1='#ff80f7',
color2='#00d1d0',
color3='#cfb000',
color4='#ffffff00',
title='Complex Trivariate Choropleth',
title1="Arable Land",
title2="Perm. Crop",
title3="Perm. Grassl.",
fontsize=18,
fontweight=500,
side=400,
digits=0,
maxticks=5,
showarrows=True)
# Display of the vector layer on the map
p = vector.process()
m.clear()
m.addLayer(p.toLayer())
m.zoomToImageExtent(p)
inter.identifyPopup(m,p)
# Display the trivariate choropleth legend
display(HTML(svg))
.. figure:: figures/trivariateex.png
:scale: 100 %
:alt: Example of complex trivariate choropleth legend
Example of an interactive map showing polygons colored with a complex trivariate choropleth legend.
"""
if n < 2: n = 2
if n > 10: n = 10
# Convert colors to (r,g,b) tuples
rgb1 = colors.string2rgb(color1)
rgb2 = colors.string2rgb(color2)
rgb3 = colors.string2rgb(color3)
# Maximum distance among small triangles centers
maxdistance = side * (n - 1) / float(n)
# Steps on the three attributes ranges
step1 = (max1 - min1) / n
step2 = (max2 - min2) / n
step3 = (max3 - min3) / n
# Interpolate a color from 3 percentage distance values (d1,d2,d3 in [0.0,1.0])
def colorInterpolate(d1, d2, d3):
# Weights of the three colors
w1 = 1.0 - d1
w2 = 1.0 - d2
w3 = 1.0 - d3
wtot = w1 + w2 + w3
if wtot <= 0.000000001:
return color4
w1 /= wtot
w2 /= wtot
w3 /= wtot
r = int(rgb1[0]*w1 + rgb2[0]*w2 + rgb3[0]*w3)
g = int(rgb1[1]*w1 + rgb2[1]*w2 + rgb3[1]*w3)
b = int(rgb1[2]*w1 + rgb2[2]*w2 + rgb3[2]*w3)
return colors.rgb2hex((r,g,b))
# Returns the list of n points at equal distance on the segment (x1,y1)->(x2,y2)
def pointOnSegment(p1,p2, n):
points = [p1]
d = 1.0/float(n)
for i in range(1,n):
k = d*i
x = k*p2[0] + (1.0-k)*p1[0]
y = k*p2[1] + (1.0-k)*p1[1]
points.append((x,y))
points.append(p2)
return points
# Returns the center point of a triangle
def center(p1, p2, p3):
return ((p1[0]+p2[0]+p3[0])/3.0, (p1[1]+p2[1]+p3[1])/3.0)
# Returns the distance between two points
def distance(p1,p2):
return math.hypot(p2[0] - p1[0], p2[1] - p1[1])
# Returns the SVG string to display a filled triangle
def triangle(p1, p2, p3):
c = center(p1,p2,p3)
d1 = distance(c,P1)
d2 = distance(c,P2)
d3 = distance(c,P3)
if d1 <= 0.001*side:
d1 = 0.0
color = color1
else:
if d2 <= 0.001*side:
d2 = 0.0
color = color2
else:
if d3 <= 0.001*side:
d3 = 0.0
color = color3
else:
d1 /= maxdistance
d2 /= maxdistance
d3 /= maxdistance
color = colorInterpolate(d1,d2,d3)
d = side*0.0025
x1 = p1[0]
y1 = p1[1]
x2 = p2[0]
y2 = p2[1]
x3 = p3[0]
y3 = p3[1]
# Slightly enlarge the triangles so they overlap
if y1 > y3:
x1 -= d
x2 += d
y1 += d
y2 += d
y3 -= d
else:
x1 -= d
x2 += d
y1 -= d
y2 -= d
y3 += d
return '<path d="M%f %f L%f %f L%f %f z" stroke-width="0" fill="%s"/>' % (x1,y1, x2,y2, x3,y3, color)
# Maps a value x from [xmin,ymin] to [x1,x2]
def linearMap(x,xmin,xmax, x1,x2):
if x <= xmin: return x1
elif x >= xmax: return x2
else:
p = (x - xmin)/(xmax - xmin)
return (1.0 - p)*x1 + p*x2
# From a step index returns the interval on one of the attributes plus the distance from the corresponding full color
def stepToValues(index, minvalue, maxvalue, step, attribname):
if index == 0:
return "[" + attribname + "] < " + str(minvalue+step), maxdistance
elif index >= n-1:
return "[" + attribname + "] >= " + str(maxvalue-step), 0.0
else:
return "[" + attribname + "] >= " + str(minvalue+index*step) + " and [" + attribname + "] < " + str(minvalue+(index+1)*step), (n-index-1)*maxdistance/float(n)
# Legend creation for the vector layer
width = str(stroke_width)
v.reset()
v.set('all','poly','fill',color4)
v.set('all','line','stroke',stroke)
v.set('all','line','stroke-width',width)
for i1 in range(n):
for i2 in range(n):
for i3 in range(n):
filter1,d1 = stepToValues(i1, min1, max1, step1, attribute1)
filter2,d2 = stepToValues(i2, min2, max2, step2, attribute2)
filter3,d3 = stepToValues(i3, min3, max3, step3, attribute3)
filterall = filter1 + " and " + filter2 + " and " + filter3
if d1 <= 0.001*side:
d1 = 0.0
color = color1
else:
if d2 <= 0.001*side:
d2 = 0.0
color = color2
else:
if d3 <= 0.001*side:
d3 = 0.0
color = color3
else:
d1 /= maxdistance
d2 /= maxdistance
d3 /= maxdistance
color = colorInterpolate(d1,d2,d3)
v.set(filterall,'poly','fill',color)
v.set(filterall,'line','stroke',stroke)
v.set(filterall,'line','stroke-width',width)
displ = 4*fontsize
tside = side
th = side * 0.5 * math.sqrt(3.0)
w = displ + tside + displ
h = 0.5*displ + th + 0.5*displ
if len(resizewidth) <= 0: resizewidth = '%fpx' % w
if len(resizeheight) <= 0: resizeheight = '%fpx' % h
ydispl = 0
if len(title) > 0:
ydispl = 0.8*displ
h += displ
x1 = displ
x2 = x1 + tside
x3 = 0.5*(x1+x2)
y1 = y2 = h - displ
y3 = y1 - th
svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 %d %d" preserveAspectRatio="xMidYMid meet" width="%s" height="calc(%s + 20px)" version="1.1">\n' % (w,h, resizewidth,resizeheight)
if len(title) > 0:
svg += '<text x="%f" y="%f" text-anchor="middle" font-size="%f" fill="black" font-weight="%d" >%s</text>' % (x3, 1.25*fontsize, 1.05*fontsize, fontweight, title)
# Height of the small triangles
sh = th/float(n)
# Returns the third point of a triangle having p1 and p2 as the base points
def third(p1,p2, h):
return (0.5*(p1[0]+p2[0]), p1[1]+h)
# Center points of the three triangles on the three corners
points = pointOnSegment((x1,y1),(x2,y2),n)
pa = points[-2]
pb = points[-1]
pc = third(pa,pb,-sh)
P1 = center(pa,pb,pc)
points = pointOnSegment((x2,y2),(x3,y3),n)
pa = (points[-2][0]-side/n,points[-2][1])
pb = points[-2]
pc = points[-1]
P2 = center(pa,pb,pc)
points = pointOnSegment((x1,y1),(x2,y2),n)
pa = points[0]
pb = points[1]
pc = third(pa,pb,-sh)
P3 = center(pa,pb,pc)
# Creation of the small triangles
p1 = (x1,y1)
p2 = (x2,y2)
for level in range(n):
points = pointOnSegment(p1,p2,n-level)
for i in range(n-level):
p1 = points[i]
p2 = points[i+1]
svg += triangle(p1,p2,third(p1,p2,-sh))
if i < n-level-1:
p3 = points[i+2]
svg += triangle(third(p1,p2,-sh),third(p2,p3,-sh),p2)
p1 = third(points[0], points[1], -sh)
p2 = third(points[-2],points[-1],-sh)
# External border pf the triangle
swidth = side/140.0
d = side*0.005
svg += '<path d="M%f %f L%f %f L%f %f z" stroke="black" stroke-width="%f" fill="none"/>' % (x1-d,y1+d, x2+d,y2+d, x3,y3-d, swidth)
# Text for the three attributes
if len(title1) > 0:
svg += '<text x="%f" y="%f" text-anchor="middle" font-size="%f" fill="black" font-weight="%d" >%s</text>' % (x2, y2+2.5*fontsize, fontsize, fontweight, title1)
if showarrows:
svg += '<line x1="%f" y1="%f" x2="%f" y2="%f" stroke="black" stroke-width="1.0" fill="black"/>' % (x2-2*fontsize, y2+1.45*fontsize, x2, y2+1.45*fontsize)
svg += '<line x1="%f" y1="%f" x2="%f" y2="%f" stroke="black" stroke-width="1.0" fill="black"/>' % (x2, y2+1.45*fontsize, x2-0.2*fontsize, y2+1.25*fontsize)
svg += '<line x1="%f" y1="%f" x2="%f" y2="%f" stroke="black" stroke-width="1.0" fill="black"/>' % (x2, y2+1.45*fontsize, x2-0.2*fontsize, y2+1.65*fontsize)
if len(title2) > 0:
svg += '<text x="%f" y="%f" text-anchor="middle" font-size="%f" fill="black" font-weight="%d" >%s</text>' % (x3, y3-1.1*fontsize, fontsize, fontweight, title2)
if showarrows:
points = pointOnSegment((x3,y3),(x2,y2),10)
ax1 = points[0][0]+3.2*fontsize
ax2 = points[1][0]+3.2*fontsize
ay1 = points[0][1]+fontsize
ay2 = points[1][1]+fontsize
svg += '<line x1="%f" y1="%f" x2="%f" y2="%f" stroke="black" stroke-width="1.0" fill="black"/>' % (ax1,ay1,ax2,ay2)
svg += '<line x1="%f" y1="%f" x2="%f" y2="%f" stroke="black" stroke-width="1.0" fill="black"/>' % (ax1,ay1,ax1+0.3*fontsize,ay1)
svg += '<line x1="%f" y1="%f" x2="%f" y2="%f" stroke="black" stroke-width="1.0" fill="black"/>' % (ax1,ay1,ax1-0.15*fontsize,ay1+0.3*fontsize)
if len(title3) > 0:
svg += '<text x="%f" y="%f" text-anchor="middle" font-size="%f" fill="black" font-weight="%d" >%s</text>' % (x1, y1+2.5*fontsize, fontsize, fontweight, title3)
if showarrows:
points = pointOnSegment((x1,y1),(x3,y3),10)
ax1 = points[0][0]-2.2*fontsize
ax2 = points[1][0]-2.2*fontsize
ay1 = points[0][1]-fontsize
ay2 = points[1][1]-fontsize
svg += '<line x1="%f" y1="%f" x2="%f" y2="%f" stroke="black" stroke-width="1.0" fill="black"/>' % (ax1,ay1,ax2,ay2)
svg += '<line x1="%f" y1="%f" x2="%f" y2="%f" stroke="black" stroke-width="1.0" fill="black"/>' % (ax1,ay1,ax1+0.36*fontsize,ay1-0.05*fontsize)
svg += '<line x1="%f" y1="%f" x2="%f" y2="%f" stroke="black" stroke-width="1.0" fill="black"/>' % (ax1,ay1,ax1-0.15*fontsize,ay1-0.35*fontsize)
# Texts on the three axes
f = "{:.%df}" % digits
nticks = n
if maxticks > 0 and maxticks < nticks: nticks = maxticks
points = pointOnSegment((x1,y1),(x2,y2),nticks)
for index, p in enumerate(points):
v = linearMap(index, 0, nticks, min1, max1)
svg += '<text x="%f" y="%f" text-anchor="middle" font-size="%f" fill="black" font-weight="300" >%s</text>' % (p[0], p[1]+1.1*fontsize, 0.9*fontsize, f.format(v))
points = pointOnSegment((x2,y2),(x3,y3),nticks)
for index, p in enumerate(points):
v = linearMap(index, 0, nticks, min2, max2)
svg += '<text x="%f" y="%f" text-anchor="start" font-size="%f" fill="black" font-weight="300" >%s</text>' % (p[0]+0.3*fontsize, p[1], 0.9*fontsize, f.format(v))
points = pointOnSegment((x3,y3),(x1,y1),nticks)
for index, p in enumerate(points):
v = linearMap(index, 0, nticks, min3, max3)
svg += '<text x="%f" y="%f" text-anchor="end" font-size="%f" fill="black" font-weight="300" >%s</text>' % (p[0]-0.3*fontsize, p[1], 0.9*fontsize, f.format(v))
svg += '</svg>'
return svg