Curious how one might create a plot with only text information. This will essentially be a "print" for the plot window.
The best option I've found so far is the following:
library(RGraphics)
library(gridExtra)
text = paste("\n The following is text that'll appear in a plot window.\n",
" As you can see, it's in the plot window",
" One might imagine useful informaiton here")
grid.arrange(splitTextGrob(text))
However, one doesn't have control (as far as I can tell) over font type, size, justification and so on.
You can do this using base graphics. First you'll want to take away all of the margins from the plot window:
par(mar = c(0,0,0,0))
And then you'll plot an empty plot:
plot(c(0, 1), c(0, 1), ann = F, bty = 'n', type = 'n', xaxt = 'n', yaxt = 'n')
Here's a guide to what's going on here (use ?plot.default and ?par for more details):
ann - Display Annotoations (set to FALSE)
bty - Border Type (none)
type - Plot Type (one that produces no points or lines)
xaxt - x axis type (none)
yaxt - y axis type (none)
Now to plot the text. I took out the extra spaces because they didn't seem to be necessary.
text(x = 0.5, y = 0.5, paste("The following is text that'll appear in a plot window.\n",
"As you can see, it's in the plot window\n",
"One might imagine useful informaiton here"),
cex = 1.6, col = "black")
Now to restore the default settings
par(mar = c(5, 4, 4, 2) + 0.1)
I hope that helps!
You could use annotate in ggplot2 like
library(ggplot2)
text = paste("\n The following is text that'll appear in a plot window.\n",
" As you can see, it's in the plot window\n",
" One might imagine useful information here")
ggplot() +
annotate("text", x = 4, y = 25, size=8, label = text) +
theme_void()
And you can of course remove the plot margins, axes, etc. to have just the text
Here's a handy example to play with too:
par(mar = c(0,0,0,0))
plot(c(0, 1), c(0, 1), ann = F, bty = 'n', type = 'n', xaxt = 'n', yaxt = 'n')
text(x = 0.34, y = 0.9, paste("This is a plot without a plot."),
cex = 1.5, col = "black", family="serif", font=2, adj=0.5)
text(x = 0.34, y = 0.6, paste(" Perhpas you'll:"),
cex = 1.2, col = "gray30", family="sans", font=1, adj=1)
text(x = 0.35, y = 0.6, paste("Find it helpful"),
cex = 1.2, col = "black", family="mono", font=3, adj=0)
Read up on ?par . There is limited capability to select the font type via the family and font arguments.
Related
Goodmorning,
Question, I've got this script that creates a horizontal bar chart (see image)
I would like to have one label in the y-axis bold "Nederland".
I've searched an tried a lot, but I really have no idea how I can do this.
I found this solution:
Matplotlib - Changing the color of a single x-axis tick label
But I could not get it to work.
Any hint to a solution would be great.
def AVG_BarChart(self, data:dict=None, graph_file:str = None, datum:str=None, countries:dict=None, watermarktext:str="energieprijzenbot.nl", prijsper:str="kWh")->bool:
plt.figure(figsize=(9, 6))
plt.xlabel(f"Prijs per {prijsper}")
plt.title(f"Gemiddelde {prijsper} inkoopprijs per land {datum}")
colors = ["#FE8000", "#EFBD76", "#FFA52B", "#FF9D3C", "#FFF858", "#FCFFCB", "#07EEB2", "#FF4179","#E05B4B", "#E09336", "#DAB552", "#DBD9A6", "#87B49C", "#4B8A7E", "#A5DD96", "#E1F3C9", "#0095AD", "#00D5E5", "#82E9F0", "#C0ED42", "#FFE301", "#FFF352", "#FF85DA", "#FF69B3","#A15AC4", "#3F7539", "#B8CBAD", "#E1E2C2", "#F84040", "#9D1E29"]
random.shuffle(colors)
values = 2 ** np.random.randint(2, 10, len(data))
max_value = values.max()
labels = list(data.keys())
values = list(data.values())
height = 0.9
plt.barh(y=labels, width=values, height=height, color=colors, align='center', alpha=0.8)
ax = plt.gca()
ax.xaxis.set_major_formatter('€ {x:n}')
plt.bar_label(ax.containers[0], labels=[f'€ {x:n}' for x in ax.containers[0].datavalues], label_type="edge", padding=-50)
ax.text(0.5, 0.5, watermarktext, transform=ax.transAxes,
fontsize=40, color='gray', alpha=0.3,
ha='center', va='center', rotation='30')
for i, (label, value) in enumerate(zip(labels, values)):
country_iso = self.get_key(val=label, my_dict=countries).lower()
self.offset_image(x=value, y=i, flag=country_iso, bar_is_too_short=value < max_value / 10, ax=ax)
plt.subplots_adjust(left=0.15)
plt.savefig(graph_file, bbox_inches='tight', width = 0.4)
return True
I tried looping thru the labels like this
i = 0
for w in ax.get_yticklabels():
country = ax.get_yticklabels()[i].get_text()
if country == "Nederland":
ax.get_yticklabels()[i].set_color('red')
ax.get_yticklabels()[i].set_fontweight('bold')
i += 1
When debugging I actually get a country name back, but when running the script normal, all country labels are empty...
So, I was close to the answer. But somehow I got back empty .get_text() string.
# ... some code
labels = list(data.keys())
# ... more code
ax.set_yticklabels(labels)
for lab in ax.get_yticklabels():
if lab.get_text() == "Nederland":
lab.set_fontweight('bold')
I just hope by setting the labels again, It does not mix up anything :-)
I would like to change the width of the heatmap cell so that its dimensions are square (even). Ideally, the cells are small and square so that I can fit several heatmaps with just one column of data. I want to reproduce something like this:
My current code makes one heatmap that is too wide and there is lots of white space, as well as a strange y position that cuts off the bottom cell. Not sure what's going on. Thanks.
def genHeatMap():
colours = ['#67d33d',
'#76d74f',
'#84da5f',
'#91de6e',
'#9ce17b',
'#a6e488',
'#b1e795',
'#bbeaa1',
'#91de6e',
'#9ce17b']
values = [1.0,
0.17647058823529413,
0.08021390374331551,
0.04054054054054054,
0.06,
0.07894736842105263,
0.07317073170731707,
0.05813953488372093,
0.1320754716981132,
0.0]
y_labels=['103', '134', '140', '185', '235', '292', '299', '431', '566', '659']
y = list(range(10))
x = ['a'] * 10
df = {'xs':x,'ys':y,'value':values,'colour':colours,'labels':y_labels}
p = figure(x_range='a',y_range=y_labels,plot_width=300,plot_height=300,
tooltips = [('CSID', f'#labels-103'), ('Tanimoto', '#value')])
p.rect('xs', 'ys', width=1, height=1, source=df,color='colour', line_color="black")
p.toolbar.logo = None
p.min_border_bottom = 20
p.min_border_left = 0
p.min_border_right = 0
p.min_border_top = 0
p.xaxis.major_tick_line_color = None
p.xaxis.minor_tick_line_color = None
p.xaxis.major_label_text_font_size = '0pt'
return p
Set p.x_range.range_padding = 0
https://docs.bokeh.org/en/latest/docs/reference/models/ranges.html#bokeh.models.ranges.FactorRange.range_padding
This question already has an answer here:
Matplotlib: create multiple subplot in one figure
(1 answer)
Closed 5 years ago.
I am learning how to play with matplotlib recently. However, some problems come up. I read in a non-standard data file named students.data with the following command.
student_dataset = pd.read_csv("students.data", index_col=0)
Here is how students.data looks like.
Then I plot a figure with four subplots of histograms in it with the following commands.
fig = plt.figure(0) #Use it to create subplots.
fig.subplots_adjust(hspace=0.5, wspace=0.5) #Adjust height-spacing to
#de-overlap titles and ticks
ax1 = fig.add_subplot(2, 2, 1)
my_series1 = student_dataset["G1"]
my_series1.plot.hist(alpha=0.5, color = "blue", histtype = "bar", bins = 30)
ax2 = fig.add_subplot(2, 2, 2)
my_series2 = student_dataset["G2"]
my_series2.plot.hist(alpha=1, color = "green", histtype = "step", bins = 20)
ax3 = fig.add_subplot(2, 2, 3)
my_series3 = student_dataset["G3"]
my_series3.plot.hist(alpha=0.5, color = "red", histtype = "stepfilled")
ax4 = fig.add_subplot(2, 2, 4)
my_series1.plot.hist(alpha=0.5, color = "blue")
my_series2.plot.hist(alpha=0.5, color = "green")
my_series3.plot.hist(alpha=0.5, color = "red")
And the result is exactly the stuff I want. However, as I try to do so for scatter subplots, they are separated in different figures. And I cannot figure out why. Here are the commands.
fig = plt.figure(2)
ax1 = fig.add_subplot(2, 2, 1)
student_dataset.plot.scatter(x = "freetime", y = "G1")
ax2 = fig.add_subplot(2, 2, 2)
student_dataset.plot.scatter(x = "freetime", y = "G2")
ax3 = fig.add_subplot(2, 2, 3)
student_dataset.plot.scatter(x = "freetime", y = "G3")
After searching for a day, I find the solution that almost fits my target. But, still, why? Why my original method is not working?
Here are the new commands and the result.
fig, axes = plt.subplots(2, 2, figsize=(6, 6), sharex=False, sharey=False)
x = student_dataset["freetime"].values
for i in range(3):
axes[i//2, i%2].scatter(x, student_dataset.iloc[:, i + 25].values)
fig.tight_layout()
Sorry that I cannot put more images in this post to describe my question. Hope you can understand my point.
Thanks in advance.
You may choose to use option 2 of the linked question,
fig = plt.figure(2)
ax1 = fig.add_subplot(2, 2, 1)
student_dataset.plot.scatter(x = "freetime", y = "G1", ax=ax1)
ax2 = fig.add_subplot(2, 2, 2)
student_dataset.plot.scatter(x = "freetime", y = "G2", ax=ax2)
ax3 = fig.add_subplot(2, 2, 3)
student_dataset.plot.scatter(x = "freetime", y = "G3", ax=ax3)
If you don't specify ax, pandas will produce a new figure.
At the moment I don't have any good explanation for why plot.hist does not require the ax keyword; it probably has to do with it directly calling the plt.hist function instead of preprocessing the data first.
The recent version of Bokeh allows the programmer to put the legend outside of the chart area. This can be accomplished like described here:
p = figure(toolbar_location="above")
r0 = p.circle(x, y)
legend = Legend(items=[
("sin(x)" , [r0]),),
], location=(0, -30))
p.add_layout(legend, 'right')
show(p)
Note: A legend object is attached to a plot via add_layout. The legend object itself consists of tuples and strings together with glyph lists.
The question is what to do when you are just drawing one "data" series as is the case with the code below, adapted from here:
from bokeh.io import show
from bokeh.models import ColumnDataSource, HoverTool, LinearColorMapper
from bokeh.plotting import figure
col = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]
row = ['A', 'B', 'C' , 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
'N', 'O', 'P']
# this is the colormap from the original NYTimes plot
colors = ["#75968f", "#a5bab7", "#c9d9d3", "#e2e2e2", "#dfccce",
"#ddb7b1", "#cc7878", "#933b41", "#550b1d"]
mapper = LinearColorMapper(palette=colors)
source = ColumnDataSource(data = dict (
row = test['plate_row'],
col = test['plate_col'],
values = test['Melt Temp']
))
TOOLS = "hover,save,pan,box_zoom,wheel_zoom"
p = figure(title="Plate Heatmap", x_range = (0.0,25.0), y_range =
list(reversed(row)), x_axis_location="above", tools=TOOLS)
r1 = p.rect(x="col", y="row", width=1, height=1,
source=source,
fill_color={'field': 'values', 'transform': mapper},
line_color=None)
legend = Legend(items=[
("test" , [r1]),
], location=(0, -30))
p.add_layout(legend, 'left')
show(p) # show the plot
The issue here is that there is only one glyph. What I actually need is an explanation of what value range is included for different colors. Clearly, this is possible, because the plots defined here show that it's possible.
Update:
Now that I am writing about the problem, I am starting to think, that perhaps I can can just plot multiple series, one for each color...and only plot those coordinates that fall within a certain range...that seems rather clunky though. So any ideas are appreciated!
I figured out a way through using CategoricalColorMapper and then not creating an explicit legend object.
There may be a way to create the legend object explicitly with the same layout, I will have a look later.
import numpy as np
from bokeh.io import show
from bokeh.models import Legend
from bokeh.models import ColumnDataSource, HoverTool,CategoricalColorMapper
from bokeh.plotting import figure
from bokeh.palettes import Blues8
# values to assign colours on
values = np.arange(100,107)
# values that will appear in the legend!!!
legend_values = ['100-101','101-102','102-103','103-04','104-05','105-06',
'106-07']
source = ColumnDataSource(data = dict (
row = np.arange(100,107),
col = np.arange(100,107),
values = np.arange(100,107),
legend_values = legend_values
))
mapper = CategoricalColorMapper(factors=list(values),palette=Blues8)
TOOLS = "hover,save,pan,box_zoom,wheel_zoom"
p = figure(title="Plate Heatmap", x_range = (100,107), y_range =
[90,107], x_axis_location="above", tools=TOOLS)
r1 = p.rect(x="col", y="row", width=1, height=1,
source=source,
fill_color={'field': 'values', 'transform': mapper},
line_color=None,legend='legend_values')
p.legend.location = "bottom_right"
show(p) # show the plot
See the image here 1
After researching this a bit more, I found 2 ways of creating a legends that show what each color means on the heatmap:
1.) Painting several glyph series:
First, I divide the number range into bins like so:
min_value = test['Melt Temp'].min()
max_value = test['Melt Temp'].max()
increment = round((max_value - min_value)/9)
num_bins = [(lower, lower+increment) for lower in
range(int(floor(min_value)), int(round(max_value)),
int(round(increment)))]
Then, I create sub tables from the main tables like so:
source_dict = {}
for range_tuple in num_bins:
range_data = test[(test['Melt Temp'] > int(range_tuple[0])) &
(test['Melt Temp'] <= int(range_tuple[1]))]
source = ColumnDataSource(data = dict (
row = range_data['x'],
col = range_data['y'],
values = range_data['Value']))
source_dict[range_tuple] = source
Then I zip up the colors with a column data source sub-table:
colors = RdYlBu9
glyph_list = []
for color, range_tuple in zip(colors, num_bins):
r1 = p.rect(x="col", y="row", width=1, height=1,
source=source_dict[range_tuple],
fill_color=color,
line_color=None)
glyph_list.append(r1)
Lastly, I create an explicit legend object which requires string-glyph-tuples. The legend object then gets attached to the plot:
legend_list = [("{0}<={1}".format(bin[0], bin[1]), [glyph]) for bin,
glyph in zip(num_bins, glyph_list)]
legend = Legend(items=legend_list, location=(0, -50))
p.add_layout(legend, 'left')
show(p)
Downsides to this approach:
It somehow seems a bit clunky.
Another potential downside I discovered while trying to select objects: If you click on one datapoint of a certain color, all datapoints of that color get selected. Depending on what you want to do this may be a plus or a minus.
2.) Colorbar:
Second approach makes use of #Okonomiyaki's comment above, and is a lot simpler. The basic gist is that you use a color mapper for determining colors of your glyphs. You also create a ColorBar as Okonomiyaki pointed out:
mapper = LogColorMapper(palette="Viridis256", low=min_value,
high=max_value)
source = ColumnDataSource(data = dict (
row = test['x'], col = test['y'], values = test['value']))
p = figure(title="Plate Heatmap", x_range = (0.0,25.0), y_range =
list(reversed(row)),
x_axis_location="above", plot_width=650, plot_height=400)
r1 = p.rect(x="col", y="row", width=1, height=1,
source=source,
fill_color={'field': 'values', 'transform': mapper},
line_color=None)
color_bar = ColorBar(color_mapper=mapper, ticker=LogTicker(),
label_standoff=12, border_line_color=None,
location(0,0))
p.add_layout(color_bar, 'left')
layout = p
show(layout)
I like the elegance of this approach. The only downside to this approach is that you don't get a clean range of numbers that define a given color.
If other people come up with even more elegant approaches, please
share!
There is an example here for how to create a multi-colored text title.
However, I want to apply this to a plot that already has a figure in it.
For example, if I apply it to this (same code as with the example minus a few extras and with another figure)...:
plt.rcdefaults()
import matplotlib.pyplot as plt
%matplotlib inline
from matplotlib import transforms
fig = plt.figure(figsize=(4,3), dpi=300)
def rainbow_text(x,y,ls,lc,**kw):
t = plt.gca().transData
fig = plt.gcf()
plt.show()
#horizontal version
for s,c in zip(ls,lc):
text = plt.text(x,y," "+s+" ",color=c, transform=t, **kw)
text.draw(fig.canvas.get_renderer())
ex = text.get_window_extent()
t = transforms.offset_copy(text._transform, x=ex.width, units='dots')
plt.figure()
rainbow_text(0.5,0.5,"all unicorns poop rainbows ! ! !".split(),
['red', 'orange', 'brown', 'green', 'blue', 'purple', 'black'],
size=40)
...the result is 2 plots with the title enlarged.
This sort of makes sense to me because I'm using plt. two times.
But how do I integrate it so that it only refers to the first instance of plt. in creating the title?
Also, about this line:
t = transforms.offset_copy(text._transform, x=ex.width, units='dots')
I notice it can alter the spacing between words, but when I play with the values of x, results are not predictable (spacing is inconsistent between words).
How can I meaningfully adjust that value?
And finally, where it says "units='dots'", what are the other options? Are 'dots' 1/72nd of an inch (and is that the default for Matplotlib?)?
How can I convert units from dots to inches?
Thanks in advance!
In fact the bounding box of the text comes in units unlike the ones used, for example, in scatterplot. Text is a different kind of object that gets somehow redraw if you resize the window or change the ratio. By having a stabilized window you can ask the coordinates of the bounding box in plot units and build your colored text that way:
a = "all unicorns poop rainbows ! ! !".split()
c = ['red', 'orange', 'brown', 'green', 'blue', 'purple', 'black']
f = plt.figure(figsize=(4,3), dpi=120)
ax = f.add_subplot(111)
r = f.canvas.get_renderer()
space = 0.1
w = 0.5
counter = 0
for i in a:
t = ax.text(w, 1.2, a[counter],color=c[counter],fontsize=12,ha='left')
transf = ax.transData.inverted()
bb = t.get_window_extent(renderer=f.canvas.renderer)
bb = bb.transformed(transf)
w = w + bb.xmax-bb.xmin + space
counter = counter + 1
plt.ylim(0.5,2.5)
plt.xlim(0.6,1.6)
plt.show()
, which results in:
This, however, is still not ideal since you need to keep controlling the size of your plot axis to obtain the correct spaces between words. This is somewhat arbitrary but if you manage to do your program with such a control it's feasible to use plot units to achieve your intended purpose.
ORIGINAL POST:
plt. is just the call to the library. In truth you are creating an instance of plt.figure in the global scope (so it can be seen in locally in the function). Due to this you are overwriting the figure because you use the same name for the variable (so it's just one single instance in the end). To solve this try controlling the names of your figure instances. For example:
import matplotlib.pyplot as plt
#%matplotlib inline
from matplotlib import transforms
fig = plt.figure(figsize=(4,3), dpi=300)
#plt.show(fig)
def rainbow_text(x,y,ls,lc,**kw):
t = plt.gca().transData
figlocal = plt.gcf()
#horizontal version
for s,c in zip(ls,lc):
text = plt.text(x,y," "+s+" ",color=c, transform=t, **kw)
text.draw(figlocal.canvas.get_renderer())
ex = text.get_window_extent()
t = transforms.offset_copy(text._transform, x=ex.width, units='dots')
plt.show(figlocal) #plt.show((figlocal,fig))
#plt.figure()
rainbow_text(0.5,0.5,"all unicorns poop rainbows ! ! !".split(),
['red', 'orange', 'brown', 'green', 'blue', 'purple', 'black'],
size=40,)
I've commented several instructions but notice I give a different name for the figure local to the function (figlocal). Also notice that in my examples of show I control directly which figure should be shown.
As for your other questions notice you can use other units as can be seen in the function documentation:
Return a new transform with an added offset.
args:
trans is any transform
kwargs:
fig is the current figure; it can be None if units are 'dots'
x, y give the offset
units is 'inches', 'points' or 'dots'
EDIT: Apparently there's some kind of problem with the extents of the bounding box for text that does not give the correct width of the word and thus the space between words is not stable. My advise is to use the latex functionality of Matplotlib to write the colors in the same string (so only one call of plt.text). You can do it like this:
import matplotlib
import matplotlib.pyplot as plt
matplotlib.use('pgf')
from matplotlib import rc
rc('text',usetex=True)
rc('text.latex', preamble=r'\usepackage{color}')
a = "all unicorns poop rainbows ! ! !".split()
c = ['red', 'orange', 'brown', 'green', 'blue', 'purple', 'black']
st = ''
for i in range(len(a)):
st = st + r'\textcolor{'+c[i]+'}{'+a[i]+'}'
plt.text(0.5,0.5,st)
plt.show()
This however is not an ideal solution. The reason is that you need to have Latex installed, including the necessary packages (notice I'm using the color package). Take a look at Yann answer in this question: Partial coloring of text in matplotlib
#armatita: I think your answer actually does what I need. I thought I needed display coordinates instead, but it looks like I can just use axis 1 coordinates, if that's what this is (I'm planning on using multiple axes via subplot2grid). Here's an example:
import matplotlib.pyplot as plt
%matplotlib inline
dpi=300
f_width=4
f_height=3
f = plt.figure(figsize=(f_width,f_height), dpi=dpi)
ax1 = plt.subplot2grid((100,115), (0,0), rowspan=95, colspan=25)
ax2 = plt.subplot2grid((100,115), (0,30), rowspan=95, colspan=20)
ax3 = plt.subplot2grid((100,115), (0,55), rowspan=95, colspan=35)
ax4 = plt.subplot2grid((100,115), (0,95), rowspan=95, colspan=20)
r = f.canvas.get_renderer()
t = ax1.text(.5, 1.1, 'a lot of text here',fontsize=12,ha='left')
space=0.1
w=.5
transf = ax1.transData.inverted()
bb = t.get_window_extent(renderer=f.canvas.renderer)
bb = bb.transformed(transf)
e = ax1.text(.5+bb.width+space, 1.1, 'text',fontsize=12,ha='left')
print(bb)
plt.show()
I'm not sure what you mean about controlling the axis size, though. Are you referring to using the code in different environments or exporting the image in different sizes? I plan on having the image used in the same environment and in the same size (per instance of using this approach), so I think it will be okay. Does my logic make sense? I have a weak grasp on what's really going on, so I hope so. I would use it with a function (via splitting the text) like you did, but there are cases where I need to split on other characters (i.e. when a word in parentheses should be colored, but not the parentheses). Maybe I can just put a delimiter in there like ','? I think I need a different form of .split() because it didn't work when I tried it.
At any rate, if I can implement this across all of my charts, it will save me countless hours. Thank you so much!
Here is an example where there are 2 plots and 2 instances of using the function for posterity:
import matplotlib.pyplot as plt
%matplotlib inline
dpi=300
f_width=4
f_height=3
f = plt.figure(figsize=(f_width,f_height), dpi=dpi)
ax1 = plt.subplot2grid((100,60), (0,0), rowspan=95, colspan=30)
ax2 = plt.subplot2grid((100,60), (0,30), rowspan=95, colspan=30)
f=f #Name for figure
string = str("Group 1 ,vs. ,Group 2 (,sub1,) and (,sub2,)").split(',')
color = ['black','red','black','green','black','blue','black']
xpos = .5
ypos = 1.2
axis=ax1
#No need to include space if incuded between delimiters above
#space = 0.1
def colortext(f,string,color,xpos,ypos,axis):
#f=figure object name (i.e. fig, f, figure)
r = f.canvas.get_renderer()
counter = 0
for i in string:
t = axis.text(xpos, ypos, string[counter],color=color[counter],fontsize=12,ha='left')
transf = axis.transData.inverted()
bb = t.get_window_extent(renderer=f.canvas.renderer)
bb = bb.transformed(transf)
xpos = xpos + bb.xmax-bb.xmin
counter = counter + 1
colortext(f,string,color,xpos,ypos,axis)
string2 = str("Group 1 part 2 ,vs. ,Group 2 (,sub1,) and (,sub2,)").split(',')
ypos2=1.1
colortext(f,string2,color,xpos,ypos2,axis)
plt.show()