
File name
Commit message
Commit date
2023-06-14
import os
import io
from PIL import Image
import numpy as np
import plotly.express as px
import plotly.graph_objects as go
import cv2
from algorithms.voronoi import get_lecc
# import matplotlib.pyplot as plt # debugging
sep = os.path.sep
def extract(lst, i):
return [item[i] for item in lst]
def plotly_fig2array(fig, scale):
# convert Plotly fig to an array
fig_bytes = fig.to_image(format="png", width=2000, height=800, scale=scale)
buf = io.BytesIO(fig_bytes)
img = Image.open(buf)
return np.asarray(img)
def choropleth_chart(shp, df, title, save_dir, colorscheme="BuGn", show_legend=True, unit='',
adaptive_legend_font_size=False, geo_annot_scale = 1500,
font="Open Sans", scale=4, save=True):
f"""
shp : geopandas datastream from .shp, .shx and .dbf must exist in the same folder
df : single column of pandas DataFrame or any other iterable, height must be the number of object of shp
title : title of the chart
save_dir : save directory from 'figure/save_dir.png'
colorscheme : either string of predefined plotly colorscheme or your colorscheme that is iterate of colors
show_legend : Boolean, self explainatory
adaptive_legend_font_size : legend of regions become adaptive using voronoi algorithm,
asserting the biggest annotation adaptive to the size of the region
If false, there will be no annotation over region
This, takes some time and could be improved if you could simplify polygons,
which is not implemented.
geo_annot_scale : set the scale of font size, default value was found with trial and error.
font : Defaults to the default plotly font(Open Sans) if None.
Type font name string if you need.
Execute font_names.py if you want to see the names of the font that python env recognizes.
scale : the larger the better
save : Boolean, default is True. To save or not to save.
"""
# TODO add more settings
num_area = len(shp)
cent_pnt = [None] * num_area
for i, cent in enumerate(shp.centroid):
cent_pnt[i] = [cent.x, cent.y]
cent_map_x = sum(extract(cent_pnt, 0)) / num_area
cent_map_y = sum(extract(cent_pnt, 1)) / num_area
lecc = None
lecc_dist = None
if adaptive_legend_font_size:
lecc, lecc_dist = get_lecc(shp)
# draw the choropleth
fig = px.choropleth(
# draw shp map
shp.set_index("EMD_KOR_NM"),
locations=shp.index, # names
geojson=shp.geometry, # geojson shape
color=df, # a row vector of data
color_continuous_scale=colorscheme,
center=dict(lat=cent_map_y, lon=cent_map_x),
range_color=[0, max(df)],
)
fig.update_coloraxes(
colorbar=dict(
title=dict(
text="",
font=dict(
size=30
),
),
xanchor="left",
x=0.235,
tickfont=dict(
size=20
),
# tickformat=".0%"
# put tickformat for later uses?
),
)
if show_legend:
if adaptive_legend_font_size:
annotation_text_size = np.multiply(lecc_dist, geo_annot_scale)
fig.add_trace(
go.Scattergeo(
# draw region names based on lat and lon
lat=extract(lecc, 1), lon=extract(lecc, 0),
# lat=cent_pnt_y, lon=cent_pnt_x,
marker={
"size": [0] * num_area,
},
mode="text",
name="",
text=shp["EMD_KOR_NM"].values,
textposition=["middle center"] * num_area,
textfont={
# "color": ["Black"] * num_area,
"family": [f"{font}"] * num_area,
"size": annotation_text_size,
}
)
)
else:
annotation_text_size = [40] * num_area
fig.add_trace(
go.Scattergeo(
# draw region names based on lat and lon
lat=extract(cent_pnt, 1), lon=extract(cent_pnt, 0),
# lat=cent_pnt_y, lon=cent_pnt_x,
marker={
"size": [0] * num_area,
},
mode="text",
name="",
text=shp["EMD_KOR_NM"].values,
textposition=["middle center"] * num_area,
textfont={
# "color": ["Black"] * num_area,
"family": [f"{font}"] * num_area,
"size": annotation_text_size,
}
)
)
# testing with more annotation, such as putting numbers.
# fig.add_trace(go.Scattergeo(
# # draw region names based on lat and lon
# lat=extract(lecc, 1), lon=extract(lecc, 0),
# # lat=cent_pnt_y, lon=cent_pnt_x,
# marker={
# "size": [0] * num_area,
# },
# mode="text",
# name="",
# text=df,
# textposition=["bottom center"] * num_area,
# textfont={
# "color": ["Black"] * num_area,
# "family": ["Open Sans"] * num_area,
# "size": annotation_text_size*0.6,
# }
# ))
fig.update_traces(marker_line_width=3,
marker_line_color='white')
fig.update_layout(margin={"r": 0, "t": 0, "l": 0, "b": 0})
fig.update_annotations(showarrow=False, visible=True)
fig.update_geos(fitbounds="locations", visible=False)
fig.update_layout(
legend=dict(
yanchor="top",
y=0.99,
xanchor="left",
x=0.01
)
)
fig.update_layout(
title_text=f"{title}",
)
fig.update_layout(
font=dict({'family': f'{font}'})
)
fig.update_layout(
coloraxis_colorbar=dict(
title=f"단위 : {unit}",
)
)
# fig.show() # debug
if save:
# because plotly does not have any methods to adjust geojson object.
fig_out = fig
fig_out.update_layout(
title=dict(
yanchor="top",
y=0.98,
xanchor="left",
x=0.32,
font_size=40,
font_color="Black"
),
)
# slicing static image because there is no way to adjust geojson object there.
img = plotly_fig2array(fig_out, scale)
img = img[:, 460*scale : 1500*scale]
p = f"{save_dir}"
cv2.imwrite(p, cv2.cvtColor(img, cv2.COLOR_RGB2BGR))
print(f"saved at : {p}")
return fig