Adding date labels to a reportlab LinePlot chart - python

I am using reportlab to generate a LinePlot chart. I can't seem to get non numeric labels for the X axis.
Does anyone have any ideas?
This is my Lineplot chart class (note: im donig some calculations and setup outside this class, but you get the gist
import reportlab
from advisor.charting.Font import Font
from reportlab.lib.colors import Color, HexColor
from reportlab.lib.pagesizes import cm, inch
from reportlab.graphics.charts.legends import Legend
from reportlab.graphics.charts.textlabels import Label
from reportlab.graphics.charts.linecharts import HorizontalLineChart
from reportlab.graphics.charts.lineplots import LinePlot
from reportlab.graphics.shapes import Drawing, String, _DrawingEditorMixin
from reportlab.graphics.widgets.markers import makeMarker
class TacticalAugLineGraph(_DrawingEditorMixin, Drawing):
def __init__(self, width=100, height=110, legend=False, *args, **kw):
apply(Drawing.__init__, (self, width, height) + args, kw)
chartFont = Font('Gotham-Bold')
self._add(self, LinePlot(), name='chart', validate=None, desc=None)
self.chart._inFill = 1
self.chart.x = 20
self.chart.y = 15
self.chart.width = 85
self.chart.height = 95
#self.chart.lineLabelFormat = '%d%%'
self.chart.yValueAxis.valueMin = 0
self.chart.yValueAxis.valueMax = 100
self.chart.yValueAxis.valueStep = 10
def apply_colors(self, colors):
self.chart.lines[0].strokeColor = colors[0]
self.chart.lines[1].strokeColor = colors[1]
self.chart.lines[2].strokeColor = colors[2]
self.chart.lines[3].strokeColor = colors[3]

I produced a simple example that you could test. The result could look like this:
#!/usr/bin/python
from reportlab.graphics.charts.lineplots import LinePlot
from reportlab.graphics.shapes import Drawing
from reportlab.lib import colors
from random import randint
from datetime import date, timedelta
# Generate some testdata
data = [
[(x,randint(90,100)) for x in range(0,2001,100)],
[(x,randint(30,80)) for x in range(0,2001,100)],
[(x,randint(5,20)) for x in range(0,2001,100)],
]
# Create the drawing and the lineplot
drawing = Drawing(400, 200)
lp = LinePlot()
lp.x = 50
lp.y = 50
lp.height = 125
lp.width = 300
lp._inFill = 1
lp.data = data
for i in range(len(data)):
lp.lines[i].strokeColor = colors.toColor('hsl(%s,80%%,40%%)'%(i*60))
# Specify where the labels should be
lp.xValueAxis.valueSteps = [5, 500, 1402, 1988]
# Create a formatter that takes the value and format it however you like.
def formatter(val):
#return val
#return 'x=%s'%val
return (date(2010,1,1) + timedelta(val)).strftime('%Y-%m-%d')
# Use the formatter
lp.xValueAxis.labelTextFormat = formatter
drawing.add(lp)
from reportlab.graphics import renderPDF
renderPDF.drawToFile(drawing, 'example.pdf', 'lineplot with dates')
In the formatter there is 2 alternative return statements that you could play with to get a better grip on it.
Of course if the data on the x-axis is dates to begin with there need not be any formatter, in that case you could just specify where the labels should be (if you are not satisfied with the default positioning).
The example above borrows the ideas from page 105 in the reportlab documentation.
Good luck :)

Related

How to update my Bokeh Legend to reflect Categorical Variable in Pandas Dataframe

I'm trying to make a dropdown menu with Bokeh that highlights the points in clusters I found. I have the dropdown menu working, but now I want to be able to visualize another categorical variable by color: Noun Class with levels of Masc, Fem, and Neuter. The problem is that the legend won't update when I switch which cluster I'm visualizing. Furthermore, if the first cluster I visualize doesn't have all 3 noun classes in it, the code starts treating all the other clusters I try to look at as (incorrectly) having that first cluster's noun class. For example, if Cluster 0 is the default and only has Masc points, all other clusters I look at using the dropdown menu are treated as only having Masc points even if they have Fem or Neuter in the actual DF.
My main question is this: how can I update the legend such that it's only attending to the respective noun classes of 'Curr'
Here's some reproducible code:
import pandas as pd
from bokeh.io import output_file, show, output_notebook, save, push_notebook
from bokeh.models import ColumnDataSource, Select, DateRangeSlider, CustomJS
from bokeh.plotting import figure, Figure, show
from bokeh.models import CustomJS
from bokeh.layouts import row,column,layout
import random
import numpy as np
from bokeh.transform import factor_cmap
from bokeh.palettes import Colorblind
import bokeh.io
from bokeh.resources import INLINE
#Generate reproducible DF
noun_class_names = ["Masc","Fem","Neuter"]
x = [random.randint(0,50) for i in range(100)]
y = [random.randint(0,50) for i in range(100)]
rand_clusters = [str(random.randint(0,10)) for i in range(100)]
noun_classes = [random.choice(noun_class_names) for i in range(100)]
df = pd.DataFrame({'x_coord':x, 'y_coord':y,'noun class':noun_classes,'cluster labels':rand_clusters})
df.loc[df['cluster labels'] == '0', 'noun class'] = 'Masc' #ensure that cluster 0 has all same noun class to illustrate error
clusters = [str(i) for i in range(len(df['cluster labels'].unique()))]
cols1 = df#[['cluster labels','x_coord', 'y_coord']]
cols2 = cols1[cols1['cluster labels'] == '0']
Overall = ColumnDataSource(data=cols1)
Curr = ColumnDataSource(data=cols2)
#plot and the menu is linked with each other by this callback function
callback = CustomJS(args=dict(source=Overall, sc=Curr), code="""
var f = cb_obj.value
sc.data['x_coord']=[]
sc.data['y_coord']=[]
for(var i = 0; i <= source.get_length(); i++){
if (source.data['cluster labels'][i] == f){
sc.data['x_coord'].push(source.data['x_coord'][i])
sc.data['y_coord'].push(source.data['y_coord'][i])
sc.data['noun class'].push(source.data['noun class'][i])
sc.data['cluster labels'].push(source.data['cluster labels'][i])
}
}
sc.change.emit();
""")
menu = Select(options=clusters, value='0', title = 'Cluster #') # create drop down menu
bokeh_p=figure(x_axis_label ='X Coord', y_axis_label = 'Y Coord', y_axis_type="linear",x_axis_type="linear") #creating figure object
mapper = factor_cmap(field_name = "noun class", palette = Colorblind[6], factors = df['noun class'].unique()) #color mapper for noun classes
bokeh_p.circle(x='x_coord', y='y_coord', color='gray', alpha = .5, source=Overall) #plot all other points in gray
bokeh_p.circle(x='x_coord', y='y_coord', color=mapper, line_width = 1, source=Curr, legend_group = 'noun class') # plotting the desired cluster using glyph circle and colormapper
bokeh_p.legend.title = "Noun Classes"
menu.js_on_change('value', callback) # calling the function on change of selection
bokeh.io.output_notebook(INLINE)
show(layout(menu,bokeh_p), notebook_handle=True)
Thanks in advance and I hope you have a nice day :)
Imma keep it real with y'all... The code works how I want now and I'm not entirely sure what I did. What I think I did was reset the noun classes in the Curr data source and then update the legend label field after selecting a new cluster to visualize and updating the xy coords. If anyone can confirm or correct me for posterity's sake I would appreciate it :)
Best!
import pandas as pd
import random
import numpy as np
from bokeh.plotting import figure, Figure, show
from bokeh.io import output_notebook, push_notebook, show, output_file, save
from bokeh.transform import factor_cmap
from bokeh.palettes import Colorblind
from bokeh.layouts import layout, gridplot, column, row
from bokeh.models import ColumnDataSource, Slider, CustomJS, Select, DateRangeSlider, Legend, LegendItem
import bokeh.io
from bokeh.resources import INLINE
#Generate reproducible DF
noun_class_names = ["Masc","Fem","Neuter"]
x = [random.randint(0,50) for i in range(100)]
y = [random.randint(0,50) for i in range(100)]
rand_clusters = [str(random.randint(0,10)) for i in range(100)]
noun_classes = [random.choice(noun_class_names) for i in range(100)]
df = pd.DataFrame({'x_coord':x, 'y_coord':y,'noun class':noun_classes,'cluster labels':rand_clusters})
df.loc[df['cluster labels'] == '0', 'noun class'] = 'Masc' #ensure that cluster 0 has all same noun class to illustrate error
clusters = [str(i) for i in range(len(df['cluster labels'].unique()))]
cols1 = df#[['cluster labels','x_coord', 'y_coord']]
cols2 = cols1[cols1['cluster labels'] == '0']
Overall = ColumnDataSource(data=cols1)
Curr = ColumnDataSource(data=cols2)
#plot and the menu is linked with each other by this callback function
callback = CustomJS(args=dict(source=Overall, sc=Curr), code="""
var f = cb_obj.value
sc.data['x_coord']=[]
sc.data['y_coord']=[]
sc.data['noun class'] =[]
for(var i = 0; i <= source.get_length(); i++){
if (source.data['cluster labels'][i] == f){
sc.data['x_coord'].push(source.data['x_coord'][i])
sc.data['y_coord'].push(source.data['y_coord'][i])
sc.data['noun class'].push(source.data['noun class'][i])
sc.data['cluster labels'].push(source.data['cluster labels'][i])
}
}
sc.change.emit();
bokeh_p.legend.label.field = sc.data['noun class'];
""")
menu = Select(options=clusters, value='0', title = 'Cluster #') # create drop down menu
bokeh_p=figure(x_axis_label ='X Coord', y_axis_label = 'Y Coord', y_axis_type="linear",x_axis_type="linear") #creating figure object
mapper = factor_cmap(field_name = "noun class", palette = Colorblind[6], factors = df['noun class'].unique()) #color mapper- sorry this was a thing that carried over from og code (fixed now)
bokeh_p.circle(x='x_coord', y='y_coord', color='gray', alpha = .05, source=Overall)
bokeh_p.circle(x = 'x_coord', y = 'y_coord', fill_color = mapper, line_color = mapper, source = Curr, legend_field = 'noun class')
bokeh_p.legend.title = "Noun Classes"
menu.js_on_change('value', callback) # calling the function on change of selection
bokeh.io.output_notebook(INLINE)
show(layout(menu,bokeh_p), notebook_handle=True)

Updating plot using Select Widget issue

I am currently trying to write a program that will switch between two sets of data when different options are chosen from the select widget. I am trying to make this program as autonomous as possible so in the future when people update the data they don't have to modify the code at all and the updates will happen automatically.
Currently, my issue is that when I select 'White' I want the plot to update but nothing is happening.
The two data sets are currently a dict of lists, one labeled 'White_dict' and the other labeled 'black_dict' solely to represent the color of the material for the data (I know its kinda ironic).
from bokeh.plotting import figure, curdoc
from bokeh.models import ColumnDataSource, Legend
from bokeh.models import Select
from bokeh.layouts import column
import pandas as pd
from plot_tools import add_hover
import itertools
from collections import defaultdict
bokeh_doc = curdoc()
material_types = pd.read_csv('data/material-information.csv')
df = pd.read_csv('data/Black_Materials_total_reflecatance.csv')
black_df = pd.read_csv('data/Black_Materials_total_reflecatance.csv')
white_df = pd.read_csv('data/SPIE18_white_all.csv')
names = []
w_names = []
black_dict = defaultdict(list)
white_dict = defaultdict(list)
for name, w_name in zip(df, white_df):
names.append(name)
w_names.append(w_name)
data = pd.read_csv('data/Black_Materials_total_reflecatance.csv', usecols = names)
w_data = pd.read_csv('data/SPIE18_white_all.csv', usecols = w_names)
for name, w_name in zip(names, w_names):
for i in range(0, 2250):
black_dict[name].append(data[name][i])
white_dict[w_name].append(w_data[w_name][i])
mySource = ColumnDataSource(data = black_dict)
#create total reflectance figure
total_fig = figure(plot_width = 650, plot_height = 350,
title = 'Total Reflectance',
x_axis_label = 'Wavelength(nm)', y_axis_label = 'Total Reflectance',
x_range = (250, 2500), y_range = (0,10),
title_location = 'above', sizing_mode = "scale_both",
toolbar_location = "below",
tools = "box_zoom, pan, wheel_zoom, save")
select = Select(title="Material Type", options=['Black', 'White'])
def update_plot(attr, old, new):
if new == 'White':
mySource.data = white_dict
else:
mySource.data = black_dict
for name, color in zip(mySource.data, Turbo256):
if name != 'nm':
total_fig.line('nm', name, line_width = .7, source = mySource, color = color)
select.on_change('value', update_plot)
bokeh_doc.add_root(total_fig)
bokeh_doc.add_root(select)
I'm currently using bokeh serve bokehWork.py to launch the server. If anyone has any idea on what I should fix it would be much appreciated! Thanks!
EDIT:
Adding data for Black_materials_total_reflectance.csv
Black Reflectance Data sample
Adding data for White_all.csv
White Reflectance Data sample
There are two main issues with your code:
You read the same files multiple times and you do a lot of work that Pandas and Bokeh can already do for you
(the main one) You do not take into account the fact that different CSV files have different column names
Here's a fixed version. Notice also the usage of the palette. With just Turbo256 you were getting almost the same color for all lines.
import pandas as pd
from bokeh.models import ColumnDataSource, Select
from bokeh.palettes import turbo
from bokeh.plotting import figure, curdoc
black_ds = ColumnDataSource(pd.read_csv('/home/p-himik/Downloads/Black_material_data - Sheet1.csv').set_index('nm'))
white_ds = ColumnDataSource(pd.read_csv('/home/p-himik/Downloads/White Materials Sample - Sheet1.csv').set_index('nm'))
total_fig = figure(plot_width=650, plot_height=350,
title='Total Reflectance',
x_axis_label='Wavelength(nm)', y_axis_label='Total Reflectance',
title_location='above', sizing_mode="scale_both",
toolbar_location="below",
tools="box_zoom, pan, wheel_zoom, save")
total_fig.x_range.range_padding = 0
total_fig.x_range.only_visible = True
total_fig.y_range.only_visible = True
palette = turbo(len(black_ds.data) + len(white_ds.data))
def plot_lines(ds, color_offset, visible):
renderers = []
for name, color in zip(ds.data, palette[color_offset:]):
if name != 'nm':
r = total_fig.line('nm', name, line_width=.7, color=color,
source=ds, visible=visible)
renderers.append(r)
return renderers
black_renderers = plot_lines(black_ds, 0, True)
white_renderers = plot_lines(white_ds, len(black_ds.data), False)
select = Select(title="Material Type", options=['Black', 'White'], value='Black')
def update_plot(attr, old, new):
wv = new == 'White'
for r in white_renderers:
r.visible = wv
for r in black_renderers:
r.visible = not wv
select.on_change('value', update_plot)
bokeh_doc = curdoc()
bokeh_doc.add_root(total_fig)
bokeh_doc.add_root(select)

Show/Hide Series in Bokeh Chart Based on Widget Selection

Given the following bokeh chart (this code must be run in a jupyter notebook):
from bokeh.io import output_notebook, show
from bokeh.plotting import figure
from bokeh.palettes import Dark2_5 as palette
from bokeh.layouts import widgetbox, row, column
from bokeh.models.widgets import CheckboxButtonGroup
import itertools
import numpy as np
output_notebook()
# create a new plot (with a title) using figure
p = figure(plot_width=800, plot_height=400, title="My Line Plot")
start = 10.0
x = range(20)
colors = itertools.cycle(palette)
nseries = 50
# add a line renderer
for n in range(nseries):
y = np.cumsum(np.random.randn(1,20))
p.line(x, y, line_width=1, legend=str(n), color=next(colors))
p.legend.location = "top_left"
p.legend.click_policy="hide"
checkbox_button_group = CheckboxButtonGroup(
labels=[str(n) for n in range(nseries)], active=[0, 1])
show(column([p, checkbox_button_group])) # show the results
Which produces a chart like this:
How can I connect up the checkbox buttons so that they show/hide the relevant series on the plot?
Note:
I know that I can click the legend to achieve this effect. However, I want to plot more series than the legend can show (e.g. it only shows 13 series in the screenshot). Obviously, people will only have maybe 10 series shown at any one time otherwise it becomes hard to see information.
Here is my attempt. It feels clunky though, is there a better solution? Also, how can I call my callback automatically when the plot has loaded, so that series [0,1,2,3] only are made active?
from bokeh.io import output_notebook, show
from bokeh.plotting import figure
from bokeh.palettes import Dark2_5 as palette
from bokeh.layouts import widgetbox, row, column
from bokeh.models.widgets import CheckboxButtonGroup
import itertools
import numpy as np
output_notebook()
# create a new plot (with a title) using figure
p = figure(plot_width=800, plot_height=400, title="My Line Plot")
start = 10.0
x = range(20)
colors = itertools.cycle(palette)
nseries = 50
series = []
# add a line renderer
for n in range(nseries):
y = np.cumsum(np.random.randn(1,20))
series.append(p.line(x, y, line_width=1, legend=str(n), color=next(colors)))
p.legend.location = "top_left"
p.legend.click_policy="hide"
js = ""
for n in range(nseries):
js_ = """
if (checkbox.active.indexOf({n}) >-1) {{
l{n}.visible = true
}} else {{
l{n}.visible = false
}} """
js += js_.format(n=n)
callback = CustomJS(code=js, args={})
checkbox_button_group = CheckboxButtonGroup(labels=[str(n) for n in range(nseries)], active=[0,1,2,3], callback=callback)
callback.args = dict([('l{}'.format(n), series[n]) for n in range(nseries)])
callback.args['checkbox'] = checkbox_button_group
show(column([p, checkbox_button_group])) # show the results
Your solution is fine.
Here is a more compact js callback that relies on the line being numbered with their "name" attribute
from bokeh.io import output_notebook, show
from bokeh.plotting import figure
from bokeh.palettes import Dark2_5 as palette
from bokeh.layouts import widgetbox, row, column
from bokeh.models import CheckboxButtonGroup, CustomJS
import itertools
import numpy as np
# create a new plot (with a title) using figure
p = figure(plot_width=800, plot_height=400, title="My Line Plot")
start = 10.0
x = range(20)
colors = itertools.cycle(palette)
nseries = 50
# add a line renderer
for n in range(nseries):
y = np.cumsum(np.random.randn(1,20))
p.line(x, y, line_width=1, legend=str(n), color=next(colors), name=str(n))
p.legend.location = "top_left"
p.legend.click_policy="hide"
checkbox_button_group = CheckboxButtonGroup(
labels=[str(n) for n in range(nseries)], active=[])
code = """
active = cb_obj.active;
rend_list = fig.renderers;
for (rend of rend_list) {
if (rend.name!==null) {
rend.visible = !active.includes(Number(rend.name));
}
}
"""
checkbox_button_group.callback = CustomJS(args={'fig':p},code=code)
show(column([p, checkbox_button_group])) # show the results
It's also useful if you want to hide groups of lines via keywords by having them share those in their "name" attribute
And here is how you can do it with the bokeh server:
from bokeh.io import curdoc
from bokeh.plotting import figure
from bokeh.palettes import Dark2_5 as palette
from bokeh.layouts import column
from bokeh.models import CheckboxButtonGroup, CustomJS
import itertools
import numpy as np
# create a new plot (with a title) using figure
p = figure(plot_width=800, plot_height=400, title="My Line Plot")
start = 10.0
x = range(20)
colors = itertools.cycle(palette)
nseries = 50
# add a line renderer
line_list = []
for n in range(nseries):
y = np.cumsum(np.random.randn(1,20))
line_list += [p.line(x, y, line_width=1, legend=str(n), color=next(colors))]
p.legend.location = "top_left"
p.legend.click_policy="hide"
checkbox_button_group = CheckboxButtonGroup(labels=[str(n) for n in range(nseries)], active=[])
def update(attr,old,new):
for lineID,line in enumerate(line_list):
line.visible = lineID in new
checkbox_button_group.on_change('active',update)
curdoc().add_root(column([p, checkbox_button_group]))
def init_active():
checkbox_button_group.active = range(3)
curdoc().add_timeout_callback(init_active,1000)

How to add an image to PDF using Python and create a report from esri geodatabase tables

I have an old script that uses SSReport to create and generate a PDF adding some basic data. I want to update it to include an image file and loop through some esri geodatabase tables. I don't see much documentation on the SSReport library, using 2.7. Is there a newer/better way?
I don't want to have to install 3rd party package, if possible.
import SSReport as REPORT
pdfOutput = REPORT.openPDF(fileName)
#Set up Table
NumColumns = 4
report = REPORT.startNewReport(NumColumns,
title = 'Report ',
landscape = False,
numRows = "", # probably empty
titleFont = REPORT.ssTitleFont)
grid = report.grid
grid.writeCell((grid.rowCount, 1),"hello world",justify = "left",)
grid.finalizeTable() # Will fill empty rows with spaces.
report.write(pdfOutput) # write to PDF
Overall I wanted to create a PDF report that included a map, and image and some tabular data stored in an Esri geodatabase. Due to lack of info on the above methods and time crunch, I ended up relying on arcpy.Mapping to update map layers elements including an image and export the layout.
I used MatplotLib to generate additional charts and tables since there are many examples and threads to assist. As well comes installed with ArcGIS Desktop and Server to avoid having a prerequisite to install 3rd party libs for a client.
Here is the sample:
import matplotlib.pyplot as plt
import xlrd
import xlwt
import datetime
import os
import glob
import arcpy
import SSReport as REPORT
import numpy as NUM
from matplotlib.backends.backend_pdf import PdfPages as PDF
import matplotlib.pyplot as PLT
# get path for mxd template from location of script
filePath = os.path.dirname(os.path.abspath(__file__))
templatePath = os.path.join(filePath,"templates")
def TabletoPDF2(outdir, outputPDF):
tables = [os.path.join(outdir,'scratch.gdb', 'table1'),
os.path.join(outdir, 'scratch.gdb', 'table2')]
pp = PDF(outputPDF)
for table in tables:
# convert to format for creating charts.
ntab = arcpy.da.TableToNumPyArray(table, "*")
columns = [fld for fld in ntab.dtype.names if fld not in ["OBJECTID"]]
cell_text = []
for row in ntab.tolist():
cell_text.append(row[1:])
fig = PLT.figure(figsize=(8.2, 10.6))
# Create PIECHART (percent lengeth)
ax1 = PLT.subplot(211)
data = ntab["percent_len"].tolist()
vals = [x for x in data if x > 0.]
labels = ntab[columns[0]].tolist()
labels = [n for n, v in zip(labels, data) if v > 0]
ax1.pie(vals, autopct='%1.1f%%', shadow=True, startangle=90)
ax1.axis('equal')
ax1.legend(labels, loc='center right', fontsize=8, bbox_to_anchor=(1, 1))
# Create TABLE of values
ax2 = PLT.subplot(212)
the_table = PLT.table(cellText=cell_text, colLabels=columns, loc='center')
the_table.auto_set_font_size(True)
PLT.axis('off')
PLT.tight_layout()
#### Set Title ####
title = os.path.basename(table)
PLT.suptitle(title, fontsize=11, fontweight='bold')
pp.savefig(fig)
pp.close()
return outputPDF
def CreatePDFReport(sPath, outprofilegraph, sfroute_feat):
template_mxd_filename = os.path.join(templatePath,'Reportemplate.mxd')
layoutpdf_filename = os.path.join(sPath,'Layout.pdf')
tablepdf_filename = os.path.join(sPath,'TableReport.pdf')
finalpdf_filename = os.path.join(sPath,'Final Report.pdf')
#Create layout report (page1)
arcpy.AddMessage("Creating Layout...")
arcpy.AddMessage(template_mxd_filename)
mxd = arcpy.mapping.MapDocument(template_mxd_filename)
change_picture_element(mxd, outprofilegraph)
change_layer_source(mxd, sfroute_feat)
arcpy.mapping.ExportToPDF(mxd,layoutpdf_filename)
# Create Table Report(page2)
TablePDF = CreateTableReport(tablepdf_filename)
TablePDF = TabletoPDF2(sPath, tablepdf_filename)
#Create Final report (merge)
msg = ("Creating Output Report {}".format(finalpdf_filename))
arcpy.AddMessage(msg)
if os.path.exists(finalpdf_filename):
os.remove(finalpdf_filename)
pdfFinal = arcpy.mapping.PDFDocumentCreate(finalpdf_filename)
pdfFinal.appendPages(layoutpdf_filename)
pdfFinal.appendPages(TablePDF)
pdfFinal.saveAndClose()
os.remove(tablepdf_filename)
os.remove(layoutpdf_filename)
return finalpdf_filename
def change_layer_source(mxd, route_feat):
""" Update layer datasource and zoom to extent """
arcpy.AddMessage(route_feat)
replace_workspace_path, datasetname = os.path.split(route_feat)
df = arcpy.mapping.ListDataFrames(mxd)[0]
for lyr in arcpy.mapping.ListLayers(mxd, data_frame=df):
if lyr.name == 'RouteLayer':
lyr.replaceDataSource (replace_workspace_path, "None")
ext = lyr.getExtent()
df.extent = ext
# mxd.save()
def change_picture_element(mxd, outprofilegraph):
for elm in arcpy.mapping.ListLayoutElements(mxd, "PICTURE_ELEMENT"):
if elm.name == "elevprofile":
elm.sourceImage = outprofilegraph
# mxd.save()
if __name__ == '__main__':
folderPath = arcpy.GetParameterAsText(0)
inTable = arcpy.GetParameterAsText(1)
sfroute_feat = arcpy.GetParameterAsText(2)
finalReport = CreatePDFReport(folderPath, outprofilegraph, sfroute_feat)
# Set outputs
arcpy.SetParameterAsText(4, finalReport)

How do I animate a scatterplot over a basemap in matplotlib?

The code below generates a animated basemap, but not exactly the one I want: I want the scatterplot from the previous frame to disappear, but it persists through the remainder of the animation.
I suspect it has something to do with my not understanding what the basemap really is. I understand calling it on lat/lons to project them to x/y, but I don't entirely get what's going on when I call event_map.scatter().
import random
import os
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.basemap import Basemap
from matplotlib import animation
import pandas as pd
from IPython.display import HTML
# Enables animation display directly in IPython
#(http://jakevdp.github.io/blog/2013/05/12/embedding-matplotlib-animations/)
from tempfile import NamedTemporaryFile
VIDEO_TAG = """<video controls>
<source src="data:video/x-m4v;base64,{0}" type="video/mp4">
Your browser does not support the video tag.
</video>"""
def anim_to_html(anim):
if not hasattr(anim, '_encoded_video'):
with NamedTemporaryFile(suffix='.mp4') as f:
anim.save(f.name, fps=20, extra_args=['-vcodec', 'libx264'])
video = open(f.name, "rb").read()
anim._encoded_video = video.encode("base64")
return VIDEO_TAG.format(anim._encoded_video)
def display_animation(anim):
plt.close(anim._fig)
return HTML(anim_to_html(anim))
animation.Animation._repr_html_ = anim_to_html
FRAMES = 20
POINTS_PER_FRAME = 30
LAT_MIN = 40.5
LAT_MAX = 40.95
LON_MIN = -74.15
LON_MAX = -73.85
FIGSIZE = (10,10)
MAP_BACKGROUND = '.95'
MARKERSIZE = 20
#Make Sample Data
data_frames = {}
for i in range(FRAMES):
lats = [random.uniform(LAT_MIN, LAT_MAX) for x in range(POINTS_PER_FRAME)]
lons = [random.uniform(LON_MIN, LON_MAX) for x in range(POINTS_PER_FRAME)]
data_frames[i] = pd.DataFrame({'lat':lats, 'lon':lons})
class AnimatedMap(object):
""" An animated scatter plot over a basemap"""
def __init__(self, data_frames):
self.dfs = data_frames
self.fig = plt.figure(figsize=FIGSIZE)
self.event_map = Basemap(projection='merc',
resolution='i', area_thresh=1.0, # Medium resolution
lat_0 = (LAT_MIN + LAT_MAX)/2, lon_0=(LON_MIN + LON_MAX)/2, # Map center
llcrnrlon=LON_MIN, llcrnrlat=LAT_MIN, # Lower left corner
urcrnrlon=LON_MAX, urcrnrlat=LAT_MAX) # Upper right corner
self.ani = animation.FuncAnimation(self.fig, self.update, frames=FRAMES, interval=1000,
init_func=self.setup_plot, blit=True,
repeat=False)
def setup_plot(self):
self.event_map.drawcoastlines()
self.event_map.drawcounties()
self.event_map.fillcontinents(color=MAP_BACKGROUND) # Light gray
self.event_map.drawmapboundary()
self.scat = self.event_map.scatter(x = [], y=[], s=MARKERSIZE,marker='o', zorder=10)
return self.scat
def project_lat_lons(self, i):
df = data_frames[i]
x, y = self.event_map(df.lon.values, df.lat.values)
x_y = pd.DataFrame({'x': x, 'y': y}, index=df.index)
df = df.join(x_y)
return df
def update(self, i):
"""Update the scatter plot."""
df = self.project_lat_lons(i)
self.scat = self.event_map.scatter(x = df.x.values, y=df.y.values, marker='o', zorder=10)
return self.scat,
s = AnimatedMap(data_frames)
s.ani
It looks like you're simply adding a new scatter plot at each update. What you should do instead is change the data in the existing path collection at each update. Try something along the lines of
def update(self, i):
"""Update the scatter plot."""
df = self.project_lat_lons(i)
new_offsets = np.vstack([df.x.values, df.y.values]).T
self.scat.set_offsets(new_offsets)
return self.scat,
Note that I haven't tested this.

Categories

Resources