Bug in a custom encryption/decryption program - encryption

I'm creating an encryption software in python 3.5. It should go along a key, using key[0] to shift raw[0], then key[1] to shift raw[1] etc, going back to key[0] when raw[i] is greater then key[i%len(key)].
# Converts the key into a numerical list.
def convert(alph, key):
for i in range(0, len(key)):
rem = alph.index(key[i])
numkey.append(rem)
print(numkey)
return numkey
#shifts the text dependant on the key
def encrypt (numkey, raw, alph):
encr = ""
emi = ()
emi = list(emi)
for i in range (0, len(raw)):
rem = raw[i]
rem = alph.index(rem)
suba = i%len(numkey)
ram = numkey[suba]
shift = (rem + ram) % 28 #ensures that shift is an index of alph
shift = alph[shift]
emi.append(shift)
for i in range(0, len(emi)):
encr = encr + str(emi[i])
print (encr)
letters = [
' ', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k',
'l', 'm', 'n', 'o', 'p', 'q', 'r', 't', 's', 'u', 'v', 'w',
'x', 'y', 'z', '.', ',', '!', '?']
raw_key = input("Please enter the key:\n")
raw_text = input("Please enter the text you would like to encrypt (no numbers or capitals):")
numkey = convert(letters, raw_key)
encrypt(numkey, raw_text, letters)
My problem is with the decryption program(below).
# Converts the key into a numerical list.
def convert(alph, key):
numkey = ()
numkey = list(numkey) # parse numkey as list
for i in range(0, len(key)):
rem = alph.index(key[i])
numkey.append(rem)
return numkey
# shifts the text dependant on the key
def encrypt (numkey,raw,alph):
encr = ""
emi = ()
emi = list(emi)
for i in range (0, len(raw)):
rem = raw[i]
rem = alph.index(rem)
suba = i%len(numkey)
ram = numkey[suba]
shift = (rem - ram)
if shift < 0:
shift = shift + 28
else:
pass
shift = alph[shift]
emi.append(shift)
for i in range(0, len(emi)):
encr = encr + str(emi[i])
print (encr)
letters = [
' ', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k',
'l', 'm', 'n', 'o', 'p', 'q', 'r', 't', 's', 'u', 'v', 'w',
'x', 'y', 'z', '.', ',' ,'!' ,'?']
raw_key = input("Please enter the key:\n")
raw_text = input("Please enter the text you would like to decrypt:\n")
numkey = convert(letters, raw_key)
encrypt(numkey, raw_text, letters)
For some reason, after encrypting the characters ",", "?" & "!", if I pass them beck through the decryption they always returns as " ", "a" and "b" respectively. This isn't a problem with any other element in the characters list.
If anyone can spot the problem I would be extremely grateful.

The problem is here in the encryption program:
shift = (rem + ram) % 28
The length of letters is 31 not 28. This is where you're looping back to the beginning of the array prematurely.
The problem is mirrored here in the decryption program:
shift = shift + 28
There are other problems as well. Just a few examples:
In the encryption program numkey is not initialized in convert()
no need to use range(), just use for char in key:
no need for the lst = () followed by lst = list(lst) pattern, just use a list in the first place, lst = []
no checking for invalid characters
function is still named encrypt() in the decryption program
Here's a quick first pass at cleaning both up.
Encryption:
import sys
LETTERS = (
' ', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k',
'l', 'm', 'n', 'o', 'p', 'q', 'r', 't', 's', 'u', 'v', 'w',
'x', 'y', 'z', '.', ',', '!', '?')
# Converts the key into a numerical list.
def convert(alph, key):
numkey = []
for char in key:
if char not in alph:
sys.exit("Invalid character")
numkey.append(alph.index(char))
print(numkey)
return numkey
# Shifts the text dependant on the key.
def encrypt (numkey, raw, alph):
encr = ""
for i, char in enumerate(raw):
if char not in alph:
sys.exit("Invalid character")
rem = alph.index(char)
ram = numkey[i % len(numkey)]
# Ensure that shift is an index of alph
shift = (rem + ram) % len(alph)
encr = encr + alph[shift]
print(encr)
raw_key = input("Please enter the key: ")
raw_text = input("Please enter the text you would like to encrypt (no numbers or capitals):\n")
numkey = convert(LETTERS, raw_key)
encrypt(numkey, raw_text, LETTERS)
Decryption:
import sys
LETTERS = (
' ', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k',
'l', 'm', 'n', 'o', 'p', 'q', 'r', 't', 's', 'u', 'v', 'w',
'x', 'y', 'z', '.', ',' ,'!' ,'?')
# Converts the key into a numerical list.
def convert(alph, key):
numkey = []
for char in key:
if char not in alph:
sys.exit("Invalid character")
numkey.append(alph.index(char))
return numkey
# Shifts the text dependant on the key.
def decrypt(numkey, raw, alph):
decr = ""
for i, char in enumerate(raw):
if char not in alph:
sys.exit("Invalid character")
rem = alph.index(char)
ram = numkey[i % len(numkey)]
shift = rem - ram
if shift < 0:
shift = shift + len(alph)
decr = decr + alph[shift]
print(decr)
raw_key = input("Please enter the key: ")
raw_text = input("Please enter the text you would like to decrypt:\n")
numkey = convert(LETTERS, raw_key)
decrypt(numkey, raw_text, LETTERS)

Related

Vertical Line ggplot for x categorical variable (not date)

I have this dataframe that I'm trying to make a vertical line on an x-axis that is categorical.
data <- data.frame(
condition = c('1', '1', '1', '1', '1', '2', '2', '2', '2', '2', '3', '3', '3', '3', '3'),
AssessmentGrade = c('400', '410', '420', '430', '440', '500', '510', '520', '530', '540',
'300', '310', '320', '330', '340'),
Freq = c('1', '2', '1', '5', '7', '9', '1', '5', '3', '4', '5', '8', '1', '3', '5'),
MathGrade = c('A+', 'B-', 'C-', 'D', 'F', 'A-', 'B', 'C+', 'D-', 'F', 'A+', 'D', 'D', 'F', 'C'),
Condition = c('Condition 1', 'Condition 1', 'Condition 1', 'Condition 1', 'Condition 1',
'Condition 2', 'Condition 2', 'Condition 2', 'Condition 2', 'Condition 2',
'Condition 3', 'Condition 3', 'Condition 3', 'Condition 3', 'Condition 3'))
I tried adding a field to make grade numeric and that helped
data$Gradenum <- as.numeric(data$MathGrade)
I used ggplot to get abubble graph but I was wondering how I would edit it to use my company's standard colors
p <- ggplot(data, aes(x = MathGrade, y = AssessmentGrade, size = Freq, fill = Condition)) +
geom_point(aes(colour = Condition)) +
ggtitle("Main Title") +
labs(x = "First Math Grade", y = "Math Assessment Score")
How can I get a vertical line between C+ and D? I see a lot of information out there if your x axis is a date but not for other categorical values
Hardcoded solutions are error-prone
MrSnake's solution works - but only for the given data set because the value of 7.5 is hardcoded.
It will fail with just a minor change to the data, e.g., by replacing grade "A+" in row 1 of data by an "A".
Using the hardcoded xintercept of 7.5
p + geom_vline(xintercept = 7.5)
draws the line between grades C- and C+ instead of C+ and D:
This can be solved using ordered factors. But first note that the chart contains another flaw: The grades on the x-axis are ordered alphabetically
A, A-, A+, B, B-, C, C-, C+, D, D-, F
where I would have expected
A+, A, A-, B, B-, C+, C, C-, D, D-, F
Fixing the x-axis
This can be fixed by turning MathGrade into an ordered factor with levels in a given order:
grades <- c(as.vector(t(outer(LETTERS[1:4], c("+", "", "-"), paste0))), "F")
grades
[1] "A+" "A" "A-" "B+" "B" "B-" "C+" "C" "C-" "D+" "D" "D-" "F"
data$MathGrade <- ordered(data$MathGrade, levels = grades)
factor()would be sufficient to plot a properly ordered x-axis but we need an ordered factor for the next step, the correct placement of the vertical line.
Programmatically placing the vertical line
Let's suppose that the vertical line should be drawn between grades C- and D+. However, it may happen that either or both grades are missing from the data. Missing factors won't be plotted. In the sample data set, there are no data with grade D+, so the vertical line should be plotted between grades C- and D.
So, we need to look for the lowest grade equal or greater D+ and the highest grade equal or less than C- in the data set:
upper <- as.character(min(data$MathGrade[data$MathGrade >= "D+"]))
lower <- as.character(max(data$MathGrade[data$MathGrade <= "C-"]))
These are the grades in the actual data set where the vertical line is to be plotted between:
xintercpt <- mean(which(levels(droplevels(data$MathGrade)) %in% c(lower, upper)))
p + geom_vline(xintercept = xintercpt)
Just add geom_vline ;)
p + geom_vline(xintercept = 7.5)
For changing the colors as to fit your company scheme, you can add something like:
+ scale_color_manual(values = c('Condition 1' = 'grey20',
'Condition 2' = 'darkred',
'Condition 3' = 'blue'))

"ValueError: year is out of range" when attempting to use matplotlib pyplot

I am attempting to get a matplotlib plotting function to be able to produce a graph with the x-axis set as a time axis. However, when I attempt to plot some values against UNIX times, I encounter the error ValueError: year is out of range. What is going wrong and how can it be addressed?
import os
import time
import matplotlib.dates
import matplotlib.pyplot
import shijian
def main():
data = [
[1484611200.0, 844.4333],
[1484524800.0, 783.3373],
[1484438400.0, 774.194 ],
[1484352000.0, 769.2299]
]
save_graph_matplotlib(
values = data,
line = True,
line_width = 0.5,
title_axis_x = "time",
title_axis_y = "value",
#time_axis_x = True
)
def save_graph_matplotlib(
values = None,
title = None,
title_axis_x = None,
title_axis_y = None,
filename = None,
directory = ".",
overwrite = True,
color = "black",
LaTeX = False,
markers = True,
marker_size = 1,
aspect = None,
line = False,
line_style = "-",
line_width = 0.2,
font_size = 20,
scientific_notation = False,
time_axis_x = False
):
# 1D or 2D data
if isinstance(values[0], list):
x = [element[0] for element in values]
y = [element[1] for element in values]
else:
x = range(0, len(values))
y = values
matplotlib.pyplot.ioff()
if LaTeX is True:
matplotlib.pyplot.rc("text", usetex = True)
matplotlib.pyplot.rc("font", family = "serif")
if filename is None:
if title is None:
filename = "graph.png"
else:
filename = shijian.propose_filename(
filename = title + ".png",
overwrite = overwrite
)
else:
filename = shijian.propose_filename(
filename = filename,
overwrite = overwrite
)
figure = matplotlib.pyplot.figure()
if title is not None:
figure.suptitle(
title,
fontsize = font_size
)
if markers is True:
matplotlib.pyplot.scatter(
x,
y,
s = marker_size,
c = color,
edgecolors = "none",
)
if line is True:
matplotlib.pyplot.plot(
x,
y,
line_style,
c = color,
linewidth = line_width
)
# Turn on or off axes scientific notation.
if scientific_notation is False:
matplotlib.pyplot.gca().get_xaxis().\
get_major_formatter().set_scientific(False)
matplotlib.pyplot.gca().get_yaxis().\
get_major_formatter().set_scientific(False)
# Set axes titles.
if title_axis_x is not None:
matplotlib.pyplot.xlabel(title_axis_x, fontsize = font_size)
if title_axis_y is not None:
matplotlib.pyplot.ylabel(title_axis_y, fontsize = font_size)
# Set axes font size.
matplotlib.pyplot.xticks(fontsize = font_size)
matplotlib.pyplot.yticks(fontsize = font_size)
# Set or do not set axis x as time.
if time_axis_x:
time_formatter = matplotlib.dates.DateFormatter("%Y-%m-%d")
matplotlib.pyplot.axes().xaxis_date()
matplotlib.pyplot.axes().xaxis.set_major_formatter(time_formatter)
matplotlib.pyplot.xticks(rotation = -90)
# Set the aspect ratio.
if aspect is None:
matplotlib.pyplot.axes().set_aspect(
1 / matplotlib.pyplot.axes().get_data_ratio()
)
else:
matplotlib.pyplot.axes().set_aspect(aspect)
if not os.path.exists(directory):
os.makedirs(directory)
matplotlib.pyplot.savefig(
directory + "/" + filename,
dpi = 700
)
matplotlib.pyplot.close()
if __name__ == "__main__":
main()
You need to convert your timestamp-like x data to a python datetime object, which can then be used in matplotlib and be understood by the matplotlib.dates.DateFormatter.
This can be done using the datetime.datetime.fromtimestamp() method.
import datetime
import matplotlib.dates
import matplotlib.pyplot as plt
data = [
[1484611200.0, 844.4333],
[1484524800.0, 783.3373],
[1484438400.0, 774.194 ],
[1484352000.0, 769.2299]
]
x = [datetime.datetime.fromtimestamp(element[0]) for element in data]
y = [element[1] for element in data]
plt.plot( x, y, ls="-", c= "b", linewidth = 2 )
plt.xlabel("Dates")
time_formatter = matplotlib.dates.DateFormatter("%Y-%m-%d")
plt.axes().xaxis.set_major_formatter(time_formatter)
plt.axes().xaxis_date() # this is not actually necessary
plt.show()
Whilst not directly addressing the text of the question, the error mentioned in the title can also occur when one attempts to plot data on an existing axis whose timeline units don't match those of the plot data (e.g. seconds vs datetime).

How to change maker in scatter plot in Julia

I would like to change the marker style of a 2D scatter plot based on the value in a vector, ultimately displaying 3 dimensions on two axes. Below is what I'd like to do and the error I get when I try it.
x = rand(1:30,100)
y = rand(20:30,100)
MyMarker = [fill("o",50);fill("x",50)]
scatter(x,y,marker=MyMarker,alpha=0.5)
LoadError: PyError (:PyObject_Call) <type 'exceptions.ValueError'>
ValueError(u"Unrecognized marker style ['o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'o', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x']",)
File "/home/lara/.julia/v0.4/Conda/deps/usr/lib/python2.7/site-packages/matplotlib/pyplot.py", line 3251, in scatter
edgecolors=edgecolors, data=data, **kwargs)
File "/home/lara/.julia/v0.4/Conda/deps/usr/lib/python2.7/site-packages/matplotlib/__init__.py", line 1812, in inner
return func(ax, *args, **kwargs)
File "/home/lara/.julia/v0.4/Conda/deps/usr/lib/python2.7/site-packages/matplotlib/axes/_axes.py", line 3877, in scatter
marker_obj = mmarkers.MarkerStyle(marker)
File "/home/lara/.julia/v0.4/Conda/deps/usr/lib/python2.7/site-packages/matplotlib/markers.py", line 171, in __init__
self.set_marker(marker)
File "/home/lara/.julia/v0.4/Conda/deps/usr/lib/python2.7/site-packages/matplotlib/markers.py", line 252, in set_marker
' {0}'.format(marker))
while loading In[91], in expression starting on line 1
in getindex at /home/lara/.julia/v0.4/PyCall/src/PyCall.jl:228
in pysequence_query at /home/lara/.julia/v0.4/PyCall/src/conversions.jl:717
[inlined code] from /home/lara/.julia/v0.4/PyCall/src/conversions.jl:733
in pytype_query at /home/lara/.julia/v0.4/PyCall/src/conversions.jl:762
in convert at /home/lara/.julia/v0.4/PyCall/src/conversions.jl:782
in pycall at /home/lara/.julia/v0.4/PyCall/src/PyCall.jl:363
in call at /home/lara/.julia/v0.4/PyCall/src/PyCall.jl:372
in close_queued_figs at /home/lara/.julia/v0.4/PyPlot/src/PyPlot.jl:401
I have tried the following, neither of which is exactly what I want to do but they are close enough, and neither works.
using Plots
pyplot(size=(400,200), legend=false) # set backend and set some session defaults
scatter(rand(30),m = ColorGradient([:red,:green,:blue]),zcolor = repmat([0,0.5,1],10))
using Plots
pyplot(size=(400,200), legend=false) # set backend and set some session defaults
scatter(rand(30),
m = ColorGradient([:red,:green,:blue]), # colors are defined by a gradient
zcolor = repmat([0,0.5,1],10) # sample from the gradient, cycling through: 0, 0.5, 1
)
Can anyone tell me how to make marker take in a vector?
Update
I have a work around but I don't like having to do this: dividing the data into separate sets and then plotting them on the same axes:
PyPlot.plot(spacerdataActualSpacer[:Length],spacerdataActualSpacer[:Curvature],marker="x",".")
PyPlot.plot(spacerdataNoActualSpacer[:Length],spacerdataNoActualSpacer[:Curvature],marker="o",".")
There's a few different things you can try (I'm on Plots dev, so I can't be certain what will work for you out of the box):
scatter(rand(30), group = repmat(1:3,10), marker=:auto)
scatter(rand(30), group = repmat(1:3,10), shape=[:+ :o :utri])
scatter(rand(30), group = repmat(1:3,10), shape=[:+ :o :utri], layout=(3,1))
Let me know if any of that is what you want. To be honest I'm not totally sure I know what your ideal viz will look like.

Drawing manually on a figure

I have generated a graph:
library(DiagrammeR)
grViz("
digraph boxes_and_circles {
# a 'graph' statement
graph [layout = neato, overlap = true, fontsize = 10, outputorder = edgesfirst]
# several 'node' statements
node [shape = circle,
fontname = Helvetica]
A [pos = '1,1!'];
B [pos = '0,2!'];
C [pos = '1.5,3!'];
D [pos = '2.5,1!'];
E [pos = '4,1!'];
F [pos = '4,2!'];
G [pos = '5,1!'];
H [pos = '6,2!'];
I [pos = '1.5,-0.1!'];
# several 'edge' statements
A->B B->C
D->E D->F E->F E->G F->G G->H F->H
}
")
Which produces:
Now I would like to draw a box with dotted lines around the nodes A, B, and C.
How can I accomplish this in R? A key requirement of the solution is that it is reproducible, i.e. that I can run the script multiple times and get the same result.
Here's another approach based on igraph. It is inspired by this igraph code sample.
I'm assuming that using igraph instead of DiagrammeR is an option - maybe that is not the case...
We leave positioning of the vertices to a standard layout algorithm and query it for the resulting vertex positions. These positions are then used to draw a dotted rectangle around an arbitrary set of "selected" vertices. No user interaction is needed.
We start with the graph topology.
library(igraph)
set.seed(42)
df <- data.frame(from = c('A', 'B', 'I', 'D', 'D', 'E', 'E', 'F', 'F', 'G'),
to = c('B', 'C', 'I', 'E', 'F', 'G', 'F', 'H', 'G', 'H'))
g <- graph.data.frame(df, directed = TRUE)
The size of the vertices and arrows in the graph can be set freely, according to taste.
vertexsize <- 50
arrowsize <- 0.2
We ask the Fruchterman-Reingold layout engine to calculate the coordinates of the vertices.
coords <- layout_with_fr(g)
Then plot the graph.
plot(g,
layout = coords,
vertex.size = vertexsize,
edge.arrow.size = arrowsize,
rescale = FALSE,
xlim = range(coords[,1]),
ylim = range(coords[,2]))
If we like to see what's going on, we can add coordinate axes and print the vertex coordinates:
axis(1)
axis(2)
V(g) # ordered vertex list
coords # coordinates of the vertices (in the same coordinate system as our dotted rectangle)
We now figure out the bounding box of the vertices that we want a rectangle around.
selectedVertices = c("A", "B", "C")
vertexIndices <- sapply(selectedVertices, FUN = function(x) { return(as.numeric(V(g)[x])) } )
llx <- min(coords[vertexIndices, 1])
lly <- min(coords[vertexIndices, 2])
urx <- max(coords[vertexIndices, 1])
ury <- max(coords[vertexIndices, 2])
Almost there. We already have the coordinates of the vertex centers in coords[], but we also need the size of the vertices in the coordinate system of plot(). From the plot.igraph source code we can see that the vertex.size option for plot() gets divided by 200 and then used as radius for drawing the vertex. We use a 50% bigger value as the margin around the bounding box of the vertex coordinates when drawing the dotted rectangle.
margin <- (vertexsize / 200) * 1.5
rect(llx - margin, lly - margin, urx + margin, ury + margin, lty = 'dotted')
This is the result we get:
You could use #StevenBeaupre's solution for the widget, but there are a few packages for graphing networks using R's graphics. One is igraph if you are open to using other solutions.
This will make the graph
library('igraph')
set.seed(11)
g <- data.frame(from = c('A', 'B', 'I', 'D', 'D', 'E', 'E', 'F', 'F', 'G'),
to = c('B', 'C', 'I', 'E', 'F', 'G', 'F', 'H', 'G', 'H'))
(gg <- graph.data.frame(g, directed = TRUE))
plot(gg, vertex.color = 'white')
And there are many ways to add a box to r graphics; here is one where you can click the plot to add the box without having to calculate anything
rekt <- function(...) {
coords <- c(unlist(locator(1)), unlist(locator(1)))
rect(coords[1], coords[2], coords[3], coords[4], ..., xpd = NA)
}
rekt(border = 'red', lty = 'dotted', lwd = 2)
I get this
An easy solution with DiagrammR would be to use dot rather than neato. You mostly lose the ability to manually position the nodes (attribute pos doesn't work anymore), but you gain the ability to use cluster and subgraph to draw lines around sets of nodes.
library(DiagrammeR)
grViz("
digraph boxes_and_circles {
# a 'graph' statement
graph [ fontsize = 10,rankdir=LR]
# several 'node' statements
node [shape = circle,
fontname = Helvetica]
# several 'edge' statements
subgraph cluster_1 {
style=dotted
A->B->C
}
D->E D->F E->F E->G F->G G->H F->H
I
}
")

Matplotlib: Cursor snap to plotted data with datetime axis

I have a plot of 3 data sets that have datetime objetcs on the x axis.
I want to have a cursor that snaps to the data and shows the precise x and y value.
I already have a "snap to cursor", but that only works for scalar x axes.
Can anyone help me to modify the snap to cursor so that it works for datetime x axes as well?
Here are my data plots:
import numpy as np
import matplotlib.pyplot as plot
import matplotlib.ticker as mticker
import matplotlib.dates as dates
import datetime
import Helpers
fig = plot.figure(1)
DAU = ( 2, 20, 25, 60, 190, 210, 18, 196, 212)
WAU = ( 50, 160, 412, 403, 308, 379, 345, 299, 258)
MAU = (760, 620, 487, 751, 612, 601, 546, 409, 457)
firstDay = datetime.datetime(2012,1,15)
#create an array with len(DAU) entries from given starting day
dayArray = [firstDay + datetime.timedelta(days = i) for i in xrange(len(DAU))]
line1 = plot.plot(dayArray, DAU, 'o-', color = '#336699')
line2 = plot.plot(dayArray, WAU, 'o-', color = '#993333')
line3 = plot.plot(dayArray, MAU, 'o-', color = '#89a54e')
ax = plot.subplot(111)
dateLocator = mticker.MultipleLocator(2)
dateFormatter = dates.DateFormatter('%d.%m.%Y')
ax.xaxis.set_major_locator(dateLocator)
ax.xaxis.set_major_formatter(dateFormatter)
fig.autofmt_xdate(rotation = 90, ha = 'center')
yMax = max(np.max(DAU), np.max(WAU), np.max(MAU))
yLimit = 100 - (yMax % 100) + yMax
plot.yticks(np.arange(0, yLimit + 1, 100))
plot.title('Active users', weight = 'bold')
plot.grid(True, axis = 'both')
plot.subplots_adjust(bottom = 0.2)
plot.subplots_adjust(right = 0.82)
legend = plot.legend((line1[0], line2[0], line3[0]),
('DAU',
'WAU',
'MAU'),
'upper left',
bbox_to_anchor = [1, 1],
shadow = True)
frame = legend.get_frame()
frame.set_facecolor('0.80')
for t in legend.get_texts():
t.set_fontsize('small')
#THIS DOES NOT WORK
cursor = Helpers.SnaptoCursor(ax, dayArray, DAU, 'euro daily')
plot.connect('motion_notify_event', cursor.mouse_move)
plot.show()
And this is my module "Helper" that contains the "SnaptoCursor" class:
(I got the basic SnaptoCursor class from somewhere else and modified it a little bit)
from __future__ import print_function
import numpy as np
import matplotlib.pyplot as plot
def minsec(sec, unused):
"""
Returns a string of the input seconds formatted as mm'ss''.
"""
minutes = sec // 60
sec = sec - minutes * 60
return '{0:02d}\'{1:02d}\'\''.format(int(minutes), int(sec))
class SnaptoCursor():
"""
A cursor with crosshair snaps to the nearest x point.
For simplicity, I'm assuming x is sorted.
"""
def __init__(self, ax, x, y, formatting, z = None):
"""
ax: plot axis
x: plot spacing
y: plot data
formatting: string flag for desired formatting
z: optional second plot data
"""
self.ax = ax
self.lx = ax.axhline(color = 'k') #the horiz line
self.ly = ax.axvline(color = 'k') #the vert line
self.x = x
self.y = y
self.z = z
# text location in axes coords
self.txt = ax.text(0.6, 0.9, '', transform = ax.transAxes)
self.formatting = formatting
def format(self, x, y):
if self.formatting == 'minsec':
return 'x={0:d}, y='.format(x) + minsec(y, 0)
elif self.formatting == 'daily euro':
return u'day {0:d}: {1:.2f}€'.format(x, y)
def mouse_move(self, event):
if not event.inaxes: return
mouseX, mouseY = event.xdata, event.ydata
#searchsorted: returns an index or indices that suggest where x should be inserted
#so that the order of the list self.x would be preserved
indx = np.searchsorted(self.x, [mouseX])[0]
mouseX = self.x[indx]
#if z wasn't defined
if self.z == None:
mouseY = self.y[indx]
#if z was defined: compare the distance between mouse and the two plots y and z
#and use the nearest one
elif abs(mouseY - self.y[indx]) < abs(mouseY - self.z[indx]):
mouseY = self.y[indx]
else:
mouseY = self.z[indx]
#update the line positions
self.lx.set_ydata(mouseY)
self.ly.set_xdata(mouseX)
self.txt.set_text(self.format(mouseX, mouseY))
plot.draw()
Of course this does not work since I am calling the SnaptoCursor with the datetime array "dayArray", which is supposed to be compared to the mouse coordinates later on. And these data types are not comparable.
I got it!!!
The problems where these two lines in the init method of the SnaptoCursor class:
self.lx = ax.axhline(color = 'k') #the horiz line
self.ly = ax.axvline(color = 'k') #the vert line
They were somehow messing up the datetime x axis (that has ordinals up to 730,000 e.g.), so you just have to initialize the lines' coordinates:
self.lx = ax.axhline(y = min(y), color = 'k') #the horiz line
self.ly = ax.axvline(x = min(x), color = 'k') #the vert line
Then it works just fine!
I'll be posting my complete SnaptoCursor class now that I have modified so it accepts individual formatting strings, and it can take up to 3 input data plots - that get snapped to according to your mouse position.
def percent(x, unused):
"""
Returns a string of the float number x formatted as %.
"""
return '{0:1.2f}%'.format(x * 100)
def minsec(sec, unused):
"""
Returns a string of the input seconds formatted as mm'ss''.
"""
minutes = sec // 60
sec = sec - minutes * 60
return '{0:02d}\'{1:02d}\'\''.format(int(minutes), int(sec))
class SnaptoCursor():
"""
A cursor with crosshair snaps to the nearest x point.
For simplicity, I'm assuming x is sorted.
"""
def __init__(self, ax, x, y, formatting, y2 = None, y3 = None):
"""
ax: plot axis
x: plot spacing
y: plot data
formatting: string flag for desired formatting
y2: optional second plot data
y3: optional third plot data
"""
self.ax = ax
self.lx = ax.axhline(y = min(y), color = 'k') #the horiz line
self.ly = ax.axvline(x = min(x), color = 'k') #the vert line
self.x = x
self.y = y
self.y2 = y2
self.y3 = y3
# text location in axes coords
self.txt = ax.text(0.6, 0.9, '', transform = ax.transAxes)
self.formatting = formatting
def format(self, x, y):
if self.formatting == 'minsec':
return 'x={0:d}, y='.format(x) + minsec(y, 0)
if self.formatting == 'decimal':
return 'x={0:d}, y={1:d}'.format(x, int(y))
elif self.formatting == 'date decimal':
return 'x={0:%d.%m.%Y}, y={1:d}'.format(x, int(y))
elif self.formatting == 'decimal percent':
return 'x={0:d}, y={1:d}%'.format(x, int(y * 100))
elif self.formatting == 'float':
return 'x={0:d}, y={1:.2f}'.format(x, y)
elif self.formatting == 'float percent':
return 'x={0:d}, y='.format(x) + percent(y, 0)
elif self.formatting == 'daily euro':
return u'day {0:d}: {1:.2f}€'.format(x, y)
def mouse_move(self, event):
if not event.inaxes:
return
mouseX, mouseY = event.xdata, event.ydata
if type(self.x[0]) == datetime.datetime:
mouseX = dates.num2date(int(mouseX)).replace(tzinfo = None)
#searchsorted: returns an index or indices that suggest where mouseX should be inserted
#so that the order of the list self.x would be preserved
indx = np.searchsorted(self.x, [mouseX])[0]
#if indx is out of bounds
if indx >= len(self.x):
indx = len(self.x) - 1
#if y2 wasn't defined
if self.y2 == None:
mouseY = self.y[indx]
#if y2 was defined AND y3 wasn't defined
elif self.y3 == None:
if abs(mouseY - self.y[indx]) < abs(mouseY - self.y2[indx]):
mouseY = self.y[indx]
else:
mouseY = self.y2[indx]
#if y2 AND y3 were defined
elif abs(mouseY - self.y2[indx]) < abs(mouseY - self.y[indx]):
if abs(mouseY - self.y2[indx]) < abs(mouseY - self.y3[indx]):
mouseY = self.y2[indx]
else:
mouseY = self.y3[indx]
#lastly, compare y with y3
elif abs(mouseY - self.y[indx]) < abs(mouseY - self.y3[indx]):
mouseY = self.y[indx]
else:
mouseY = self.y3[indx]
#update the line positions
self.lx.set_ydata(mouseY)
self.ly.set_xdata(mouseX)
self.txt.set_text(self.format(mouseX, mouseY))
plot.draw()

Resources