Beginner python user, having some success with heatmaps using Folium. I am experimenting with using the Global Terrorism Database as a source for some visualizations but I really wanted to use ipywidgets to select a specific terrorist group from a list to update my heatmap. I've already constructed a heatmap for ISIS, created an ipyvuetify button containing the groups I want to compare, but am having trouble figuring out the function and widgets.link syntax. I'd really appreciate some help because there don't seem to be any good guides (for my skill level) on how to do what I'm trying to do here.
import ipyvuetify as v
import ipywidgets as widgets
import folium
from folium import plugins
from folium.plugins import HeatMap as hm
map = folium.Map(location = [25,15], tiles = "cartodbdark_matter", zoom_start = 2)
selected = gtd.loc[gtd['Group'] == 'Islamic State of Iraq and the Levant (ISIL)']
coords = selected[['Latitude','Longitude']]
hm(coords).add_to(map)
terrorists = [
'Taliban',
'Shining Path (SL)',
'Islamic State of Iraq and the Levant (ISIL)',
'Farabundo Marti National Liberation Front (FMLN)',
'Al-Shabaab',
'Irish Republican Army (IRA)',
'Revolutionary Armed Forces of Colombia (FARC)',
'New Peoples Army (NPA)',
'Kurdistan Workers Party (PKK)',
'Boko Haram']
# This is where things go off the rails for me, not exactly sure how to arrange this function
def update(x):
if widget_terrorist_selector.selected is not None:
xmin, xmax = widget_terrorist_selector.selected
widget_terrorist_selector.observe(update,'widget_terrorist_selector')
# The selector here works but I can't figure out how to link it to my map so that it pushes a new set of heatmap coordinates
widget_terrorist_selector = v.Select(label='Choose Option', items=terrorists, v_model=terrorists[0])
widget_terrorist_selector
# This bit keeps throwing a "TypeError: Each object must be HasTraits, not <class 'pandas.core.frame.DataFrame'>"
widgets.link((widget_terrorist_selector,'v_model'),(selected, 'Group'))
Thanks in advance!
widgets.link is used for keeping two ipywidgets in sync. I'm not sure that's what you're trying to achieve here, it can probably be achieved with plain old observe.
I think in your update function you need to make the necessary change to your map, something like
def update(x):
new_choice = widget_terrorist_selector.selected
selected = gtd.loc[gtd['Group'] == new_choice]
coords = selected[['Latitude','Longitude']]
hm(coords).add_to(map)
Related
I have a situation, where I decided to distinguish my markers slightly by adding the MarkerCluster to the existing Marker.
The clustering works, although isn't synchronized. It means, that if I switch one layer off, just the first markercluster disappears, whereas the second criterion defined as CircleMarker still appears like shown below.
My code is:
df = pd.read_csv("or_geo.csv")
fo=FeatureGroup(name="OR",overlay = True)
openreach_cluster = MarkerCluster(name="OR").add_to(map)
openreach_status = MarkerCluster(control=False,
visible=True
).add_to(map)
for i,row in df.iterrows():
lat =df.at[i, 'lat']
lng = df.at[i, 'lng']
sp = df.at[i, 'sp']
stat = df.at[i,'status']
popup = df.at[i,'sp'] +'<br>' + str(df.at[i, 'street']) + '<br>' + str(df.at[i, 'post code']) + '<br>{}'.format(style)
or_marker = folium.Marker(location=[lat,lng], tooltip='<strong>Job details</strong>', popup=popup, icon = folium.Icon(
color='blue', icon='glyphicon-calendar'))
or_stat_marker = folium.CircleMarker(
location=[lat,lng],
radius=10,
color=or_color(stat),
fill_color=or_color(stat),
fill_opacity=0.5)
openreach_cluster.add_child(or_marker)
openreach_status.add_child(or_stat_marker)
Is there any way to combine these markerClusters together or sync them?
UPDATE:
The first approach from the answer below, unfortunately, doubles the jobs up and the user can't see them until clicks on any of them shown below:
This option would be fantastic if the behavior could be the same as in the image above.
UPDATE II:
The second approach is still not what I am looking for, because the clusters are doubled again and after clicking the circlemarker falls almost in the opposite direction as presented in the image above.
I need to have the behaviors exactly like those displayed on the top-left image. The circlemarker should be integrated with the point marker.
If I got you right, I think there are two possibilities:
Use the same cluster
Add your markers from or_stat_marker to the openreach_cluster and not to another cluster to de-/activate them at the same time with the same button
# was
openreach_cluster.add_child(or_marker)
openreach_status.add_child(or_stat_marker)
# try this
openreach_cluster.add_child(or_marker)
openreach_cluster.add_child(or_stat_marker)
Use marker subgroups
In this case you will have three checkmarks, one parent and two childs for each markercluster which gives full control to visibility
UPDATE: If you add the control=False option to the subgroup you will only see the parent group in LayerControl which then show/hide both groups. But the matter of markers "spreading for visibility" still is a problem I think
Another UPDATE: You are able to deactivate the clustering according to the map zoom level or even at all by using disableClusteringAtZoom option (use True or an int number for zoom level). See here for reference
# markergroups in layercontrol
mc = folium.plugins.MarkerCluster(name='OR',
overlay=True,
control=True,
show=True,
disableClusteringAtZoom=15) # choose zoom lvl to your needs
mc.add_to(map)
sub1 = folium.plugins.FeatureGroupSubGroup(mc, name='openreach_cluster', control=False, show=True) # False --> deactivated at start
sub1.add_to(map)
sub2 = folium.plugins.FeatureGroupSubGroup(mc, name='openreach_status', control=False)
sub2.add_to(map)
# the layercontrol itself
lc = folium.map.LayerControl(collapsed=False)
lc.add_to(map)
# ...
for i, row in df.iterrows():
# ...
or_marker = folium.Marker(...)
or_marker.add_to(sub1)
or_stat_marker = folium.CircleMarker(...)
or_stat_marker.add_to(sub2)
My Result:
By following the second approach described I get a map shown here. There is one checkmark "Segment Markers" which shows/hides the markers including the circles. They don't move around when clicked and are fully shown when zoomed in to a specific lvl by using disableClusteringAtZoom.
Sorry this didn't work for your problem, I really don't know why ..
I'm using pygal for generate a world map chart in Google Colab.
The problem I'm facing is with the tooltip. I want to show only in the tooltip the name of the country and the number of videos found.
The result so far I've managed to get is as follows:
Worldmap chart current output:
You can see there that the tooltip is composed as follows:
Country name
Number of videos
Country name: 1
Example:
United States
73 videos
Unites States: 1
Using the sample shown in the screenshot and in the sample above, what I want to get is
Example:
United States
73 videos
OR:
Example:
Unites States: 73 videos
Here is the Google Colab notebook sample I've created, so anyone can reproduce this issue.
This is the Worldmap chart settings:
# Save chart as svg:
worldmap_chart.render_to_file('worldmap_result.svg',
show_legend=True,
human_readable=True,
fill=True,
print_labels=True,
no_data_text="No data available",
legend_at_bottom=True,
pretty_print=True,
x_title="Hover the cursor on the country's name bellow or the map above:",
print_values=True,
force_uri_protocol='https')
I've readed the documentation and searched, but, I'm not been able to find the correct configuration (if possible) for generate the expected tooltip.
Does anyone knows what I'm missing?
After testing for a few more hours, I finally decided to follow these steps:
Generate/render the world chart in a file - in this case, a .svg file.
Read the generated file in the previous step, find and replace certain svg tags - those that contains the data I don't want in the final chart.
Download the modified file.
The said svg tags are as follows:
Example:
<desc class="value">Russian Federation: 1</desc>
Thanks to this answer, I was able to replace the svg tags and leave them empty. With that change, I was able to generate the world map chart as I required.
This is the full, modified and working code:
import types
# Library import:
from pygal_maps_world.maps import World
from pygal.style import DefaultStyle
worldmap_chart = World()
worldmap_chart.title = 'Channels distribution per country'
#list - source: https://stackoverflow.com/a/26716774/12511801
# Excludes "N/A" values:
countriesData = df_countries.loc[df_countries['Country (channel)'] != "N/A"].to_dict('split')["data"]
# Loop the "countriesData" and replace the name of the country
# with is ISO code - lowercase - in order to render the world map:
for x in countriesData:
countryName = x[0]
countryName = [x["id"].lower() for x in all_countries if x["name"] == countryName][0]
x[0] = countryName
# Strings to replace:
js_strings = []
# Generate world map chart:
#worldmap_chart.add('Countries', dict(countriesData))
for data in countriesData:
countryFound = [x["name"] for x in all_countries if x["id"].lower() == data[0]]
if (len(countryFound) > 0):
countryFound = countryFound[0]
worldmap_chart.add(countryFound, [{"value": data[0], "label" : (str(data[1]) + " video" + ("s" if data[1] != 1 else ""))}])
js_strings.append('<desc class="value">{0}: 1</desc>'.format(countryFound))
# Save chart as svg - comment this line for print the chart in this notebook:
worldmap_chart.render_to_file('chart_final_4.svg',
show_legend=True,
human_readable=True,
fill=True,
print_labels=True,
no_data_text="No data available",
legend_at_bottom=True,
pretty_print=True,
x_title="Hover the cursor on the country's name bellow or the map above:",
print_values=True)
print("Chart saved in local storage - see file (chart_final_4.svg).")
# Render the chart in the notebook:
HTML_worldChart = str(worldmap_chart.render(show_legend=True, human_readable=True,
fill=True, print_labels=True,
no_data_text="No data available",
legend_at_bottom=True,
print_values=True, style=DefaultStyle(value_font_family='googlefont:Raleway',
value_font_size=30,
value_colors=('white',))))
# Source: https://stackoverflow.com/a/62008698/12511801
from pathlib import Path
file = Path("chart_final_4.svg")
# Replace specific, conflicting countries by name:
file.write_text(file.read_text().replace('<desc class="value">Korea, Republic of: 1</desc>', '<desc class="value"></desc>'))
file.write_text(file.read_text().replace('<desc class="value">Russian Federation: 1</desc>', '<desc class="value"></desc>'))
file.write_text(file.read_text().replace('<desc class="value">Viet Nam: 1</desc>', '<desc class="value"></desc>'))
## This is due those countries are arranged as follows:
##--country (<desc class="value">Russia: 1</desc>) no detected
##--country (<desc class="value">Vietnam: 1</desc>) no detected
##--country (<desc class="value">South Korea: 1</desc>) no detected
# Loop the strings to replace:
for country in js_strings:
file.write_text(file.read_text().replace(country, '<desc class="value"></desc>'))
print("(chart_final_4.svg) has been modified.")
Output: world map chart modified - I'm modified the screenshot for include two tooltips at the same time.
Side note: it might not be the best way, but, for a lack of time and knowledge, I've decided to leave it as is. I'm open to any, better suggestions.
Exactly like the title says, I've used some code that I collected from multiple places to build up this Chord diagram, but unfortunately one last thing that's kind of going to hinder that 100% perfection is this popup that shows up whenever I hover over the label. The pop up remains there even when the mouse is moving elsewhere...
By the way, I'm using Python and Holoviews for plotting the chord.
I'm pretty sure this is due to a bug or something... however, I'd love to find a way to bypass it...
Example here : bug example
Code :
%%opts Chord [height=600 width=600 title="Transactions from 2000 to 2021" ]
#plot
#edit links = moves[moves['Transactions'] > x] // x refers to the number of transactions minimum for the diagram display
moves = moves[moves['Transactions']>5]
links = moves
chord = hv.Chord(links)
chord
nodesl = []
c = []
for i, row in moves.iterrows():
c.append(row[0])
c.append(row[1])
c = pd.DataFrame(c)
c.drop_duplicates(inplace=True)
c.reset_index(inplace=True)
c.drop(columns='index', inplace=True)
c
nodes = []
for i, row in c.iterrows():
nodes.append({'name':row[0]})
nodes = pd.DataFrame(nodes)
# nodes
nodes = hv.Dataset(nodes, 'name')
nodes.data.head()
%%opts Chord [height=800 width=800 bgcolor="black"]
%%opts Chord [title="Transactions from 2000 to 2021 (Countries with over 5 moves)\nTip: Please do not hover over the label as it might produce a bug, else refresh the page" ]
chord = hv.Chord((links, nodes)).select(value=(5, None))
#this function allows text to fit perfectly on the screen
def rotate_label(plot, element):
text_cds = plot.handles['text_1_source']
length = len(text_cds.data['angle'])
text_cds.data['angle'] = [0]*length
xs = text_cds.data['x']
text = np.array(text_cds.data['text'])
xs[xs<0] -= np.array([len(t)*0.019 for t in text[xs<0]])
chord.opts(
opts.Chord(cmap='Category10',
edge_color=dim('Target').str(),
node_color=dim('name').str(),
labels='name',
label_text_color="white",
hooks=[rotate_label]
))
chord
The ??? usually indicates that some field it's trying to display isn't available; not sure why that would be in this case. You can always override the default tools HoloViews uses for Bokeh by setting default_tools=[] and then specify whatever tools you do want, without 'hover', e.g. tools=['save', 'pan', 'wheel_zoom', 'box_zoom', 'reset'].
I am using folium to generate a map.
m = folium.Map(
location=[47.842167, -120.101655],
zoom_start=8,
tiles='Stamen Toner'
)
points = (47.842167, -120.101655), (46.835627, -118.26239)
folium.Rectangle(bounds=points, color='#ff7800', fill=True, fill_color='#ffff00', fill_opacity=0.2).add_to(m)
m
I would like to save just the part that is in the rectangle..
Is it possible to do that with python ?
Thank you in advance.
Using a combination of disabling pan and zoom features and also using folium's fit_bounds() you could do something like this. I think this comes close to your goal
Note: if you don't want to "lock down" the exported file, you can omit the last 3 params in Map() i.e. zoom_control, scrollWheelZoom and dragging
m = folium.Map(
location=[47.842167, -120.101655],
zoom_start=8,
tiles='Stamen Toner',
zoom_control=False,
scrollWheelZoom=False,
dragging=False
)
sw = [46.835627, -120.101655]
ne = [47.842167, -118.26239]
m.fit_bounds([sw, ne])
m.save('mymap.html')
My folium map, marker cluster, layer control, and search bar all work and show up correctly except for actually using the search bar. The layer I have the search plugin point to when searching is my MarkerCluster layer, which the folium documentation says should be searchable. Anything I type into the search bar it returns "Location not found" which makes me think it's not searching through the MarkerCluster layer correctly, or does not know to search through the text of that layer that is included in the markers' popups.
Here is my code as I have it right now:
import os
import folium
from folium import plugins
import pandas as pd
from folium.plugins import MarkerCluster
from folium.plugins import Search
#import data including lat/lon, name of service, address of service
df = pd.read_csv('data.csv')
#Create base map zoomed in to Indiana
map=folium.Map(location=[39.80, -86.12], tiles=None, zoom_start=7)
folium.TileLayer('cartodbpositron', name='COVID-19 Services').add_to(map)
#Make Marker Cluster Group layer
mcg = folium.plugins.MarkerCluster(control=False)
map.add_child(mcg)
#Create layer of markers
#Set marker popups to display name and address of service
for row in df.iterrows():
row_values=row[1]
location=[row_values['latitude'], row_values['longitude']]
popup=popup=(row_values['name']+'<br>'+'<br>'+row_values['address_1']+
'<br>'+'<br>'+row_values['city']+','+' '+row_values['state']+
'<br>'+row_values['zip'])
marker=folium.Marker(location=location, popup=popup, min_width=2000)
marker.add_to(mcg)
#Add search bar
servicesearch = Search(
layer=mcg,
geom_type='Point',
placeholder='Search for a service',
collapsed=False,
).add_to(map)
#Add layer control
folium.LayerControl().add_to(map)
map
How do I get the search plugin to actually search the test of the marker popups? Then, how do I get the map to highlight or zoom to those searched for markers? Any help is GREATLY appreciated, thank you!
This is my solution:
def visualize_locations_with_marker_cluster(df, zoom=4):
f = folium.Figure(width=1000, height=500)
center_lat=34.686567
center_lon=135.52000
m = folium.Map([center_lat,center_lon], zoom_start=zoom).add_to(f)
marker_cluster = MarkerCluster().add_to(m)
for d in df.iterrows():
folium.Marker(location=[d[1]["y"], d[1]["x"]], popup=d[1]["company"], name=d[1]["company"]).add_to(marker_cluster)
servicesearch = Search(
layer=marker_cluster,
search_label="name",
placeholder='Search for a service',
collapsed=False,
).add_to(m)
return m
First create map, create cluster, loop values in pd.dataframe and create Marekers for Cluster with name label.
Next create Search object and add cluster there with search_label="name", label. Finally add it all back to the map
["x", "y"] is longtitude and latitude, company is a search value in my case
If you change a little how you add data to the map, it will be easier to use the search bar. You can change the data in your dataframe into a GeoJSON objet. You first need to create a dictionnary for your GeoJSON and use the function folium.GeoJSON() :
geo_json = {
"type": "FeatureCollection",
"features": [],
}
for d in df.iterrows():
temp_dict = {
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates":[d[1]["longitude"], d[1]["latitude"]],
},"properties": {"name": d[1]["name"]}
}
geo_json["features"].append(temp_dict)
geojson_obj = folium.GeoJson(geo_json).add_to(map)
After that, you just need to change a little your code to add the search bar :
servicesearch = Search(
layer=geojson_obj,
search_label="name",
placeholder='Search for a service',
collapsed=False,
).add_to(map)
I have been struggling a while with the same. I believer this might fix it: add a 'name' or whatever you want to be searched to the marker, e.g.:
marker=folium.Marker(location=location, popup=popup, min_width=2000, name=name)
A little late, but better than never, perhaps.