graphs.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624
  1. #GPL 3 or later
  2. import os
  3. import math
  4. import json
  5. import time
  6. import random
  7. import colorsys
  8. import threading
  9. from gi.repository import Gtk
  10. from gi.repository import Gdk
  11. from gi.repository import GLib
  12. from gi.repository import cairo
  13. def box_of_color(color):
  14. def on_draw(widget, cr, data):
  15. width = widget.get_allocated_width()
  16. height = widget.get_allocated_height()
  17. cr.set_source_rgb(*color)
  18. cr.rectangle(0, 0, width, height)
  19. cr.fill()
  20. area = Gtk.DrawingArea()
  21. area.set_size_request(40, 40)
  22. area.connect("draw", on_draw, color)
  23. return area
  24. def pie_chart_draw(d, main_layer, win, data, colors):
  25. # Need to know how big is our chart
  26. w = d.get_allocated_width()
  27. h = d.get_allocated_height()
  28. # We want our circle to fit, so we find which one is
  29. # smaller.
  30. smaller = min(w, h)
  31. last = 0
  32. whole = float(math.pi*2)
  33. sum_of_data = 0
  34. for i in data:
  35. sum_of_data += float(data[i])
  36. for n, i in enumerate(data):
  37. this = whole* ( float(data[i]) / sum_of_data )
  38. main_layer.move_to(w/2, h/2)
  39. main_layer.arc(w/2, h/2, smaller/2, last, last+this)
  40. main_layer.close_path()
  41. main_layer.set_source_rgb(*colors[n%len(colors)])
  42. main_layer.fill()
  43. last = last+this
  44. def pie_chart(win, data, title="", converter=False):
  45. ret = Gtk.HBox(True)
  46. colors = make_colors(len(data))
  47. da = Gtk.DrawingArea()
  48. da.connect("draw", pie_chart_draw, win, data, colors)
  49. ret.pack_start(da, 1,1,5)
  50. lbox = Gtk.VBox()
  51. ret.pack_start(lbox, 1,1,5)
  52. sum_of_data = 0
  53. for i in data:
  54. sum_of_data += float(data[i])
  55. if converter:
  56. sum_of_data = converter(sum_of_data)
  57. lbox.pack_start(Gtk.Label(" Total : "+str(sum_of_data)+" "), 0,0,1)
  58. for n, i in enumerate(data):
  59. ibox = Gtk.HBox()
  60. lbox.pack_start(ibox, 0,0,3)
  61. ibox.pack_start(box_of_color(colors[n%len(colors)]), 0,0,0)
  62. show_size = data[i]
  63. if converter:
  64. show_size = converter(show_size)
  65. ibox.pack_start(Gtk.Label(" "+i+": "+str(show_size)+" "), 0,0,1)
  66. return ret
  67. def graph_draw(d, main_layer, win, data, currancy, graph_addition):
  68. data["items"] = sorted(data["items"], key=lambda k: k["timestamp"])
  69. # Need to know how big is our graph
  70. w = d.get_allocated_width()
  71. h = d.get_allocated_height()
  72. if data.get("allow_negative", False):
  73. zero_at = h / 2
  74. else:
  75. zero_at = h
  76. # The mouse position of a given frame
  77. mx = d.get_pointer()[0]
  78. my = d.get_pointer()[1]
  79. # Test of the mouse position
  80. # main_layer.move_to(0,0)
  81. # main_layer.line_to(mx, my)
  82. # main_layer.stroke()
  83. len_day = 60*60*24 # Seconds in a day
  84. len_hour = 60*60 # Seconds in an hour
  85. len_minute = 60 # Seconds in a minute
  86. # Here we are getting the latest and the earliest
  87. # timestamp, so we could calculate the step of the
  88. # graph. ( So the data will be readable )
  89. latest = 0
  90. try:
  91. earliest = data["items"][0]["timestamp"]
  92. except:
  93. earliest = 0
  94. for i in data["items"]:
  95. if i.get("timestamp", 0) > latest:
  96. latest = i.get("timestamp", 0)
  97. if i.get("timestamp", 0) < earliest:
  98. earliest = i.get("timestamp", 0)
  99. # Now let's look at our zoom value
  100. to_zoom = data["zoom"][0] != earliest or data["zoom"][1] != latest
  101. if data["zoom"][0] < earliest or data["zoom"][0] == 0:
  102. data["zoom"][0] = earliest
  103. if data["zoom"][1] > latest or data["zoom"][1] == 0:
  104. data["zoom"][1] = latest
  105. earliest, latest = data["zoom"]
  106. # Now I want to make a scale of dates from left
  107. # to right.
  108. main_layer.select_font_face("Monospace")
  109. main_layer.set_font_size(10)
  110. full_date = "%Y-%m-%d %H:%M:%S"
  111. only_date = "%Y-%m-%d"
  112. if latest - earliest > 10 * len_day:
  113. show_format = only_date
  114. count = int( w / (len("xxxx-xx-xx")*6+12) )
  115. else:
  116. show_format = full_date
  117. count = int( w / (len("xxxx-xx-xx xx:xx:xx")*6+12) )
  118. # Now I want to show the current date / time for
  119. # the area where the user is hovering.
  120. suglen = len("xxxx-xx-xx xx:xx:xx")*6+12
  121. thexm = mx-suglen/2
  122. if thexm < 2:
  123. thexm = 2
  124. elif thexm > w - suglen - 2:
  125. thexm = w - suglen - 2
  126. try:
  127. res_date = int( ( latest - earliest ) / w * mx + earliest )
  128. show_date = time.strftime(full_date, time.gmtime(res_date))
  129. except:
  130. show_date = "0000-00-00"
  131. main_layer.set_source_rgba(0.1,0.1,0.1,0.5)
  132. main_layer.rectangle(2+thexm,2,len(show_date)*6+2, 14)
  133. main_layer.fill()
  134. main_layer.move_to(3+thexm,12)
  135. main_layer.set_source_rgba(1,1,1,1)
  136. main_layer.show_text(str(show_date))
  137. # main_layer.set_source_rgba(0.7,0.7,0.7,1)
  138. # main_layer.move_to( mx, 20 )
  139. # main_layer.line_to( mx, h )
  140. # main_layer.stroke()
  141. # main_layer.set_dash([10,10])
  142. # main_layer.set_source_rgba(0.2,0.2,0.2,1)
  143. # main_layer.move_to( mx, 20 )
  144. # main_layer.line_to( mx, h )
  145. # main_layer.stroke()
  146. # main_layer.set_dash([1])
  147. # And the rest of the dates
  148. for date in range(count):
  149. try:
  150. res_date = int( ( latest - earliest ) / count * date + earliest )
  151. show_date = time.strftime(show_format, time.gmtime(res_date))
  152. except:
  153. show_date = "0000-00-00"
  154. thex = w / count * date
  155. # If not in range of the mouse ( so I could show the current day
  156. # for that specific area ).
  157. if int(thex) not in range(int(thexm-suglen/2), int(thexm+suglen)):
  158. main_layer.set_source_rgba(0.1,0.1,0.1,0.5)
  159. main_layer.rectangle(2+thex,2,len(show_date)*6+2, 14)
  160. main_layer.fill()
  161. main_layer.move_to(3+thex,12)
  162. main_layer.set_source_rgba(1,1,1,1)
  163. main_layer.show_text(str(show_date))
  164. # A step is how often will there be a data point
  165. # of the graph. Step of one minute, means every
  166. # point on the graph will consist all the data
  167. # happened in this minute.
  168. step = (latest - earliest) / (w / 2) # A second
  169. # Now we need the smallest and biggest value in a
  170. # given step
  171. values = []
  172. times = []
  173. pstep = earliest
  174. s = 0
  175. av = []
  176. for n, i in enumerate(data["items"]):
  177. if i.get("timestamp", 0) < earliest:
  178. continue
  179. if graph_addition == "add":
  180. s += float(i.get("amount", i.get("value", 0)))
  181. elif graph_addition == "average":
  182. av.append( float(i.get("amount", i.get("value", 0))) )
  183. elif graph_addition == "last":
  184. s = float(i.get("amount", i.get("value", 0)))
  185. if i.get("timestamp", 0) > pstep + step-1:
  186. pstep = i.get("timestamp", n)
  187. if graph_addition == "average":
  188. try:
  189. values.append(sum(av)/len(av))
  190. except:
  191. values.append(0)
  192. else:
  193. values.append(s)
  194. times.append(pstep)
  195. s = 0
  196. av = []
  197. if i.get("timestamp", 0) > latest:
  198. break
  199. # Finding the farthest point from the center
  200. # center being the 0 (zero)
  201. try:
  202. biggest = max(values)
  203. if min(values) * -1 > biggest:
  204. biggest = min(values) * -1 # Multuply by -1 reverses the - to a +
  205. except Exception as e:
  206. biggest = 1
  207. # Now let's draw it
  208. main_layer.set_line_cap(cairo.LineCap.ROUND)
  209. # POSITIVE VALUE
  210. try:
  211. toy = ( zero_at ) - ( ( zero_at ) / biggest * values[0] ) *0.9
  212. except:
  213. toy = zero_at
  214. #toy = min(toy, zero_at)
  215. main_layer.rectangle(0,0,w,zero_at)
  216. main_layer.clip()
  217. main_layer.move_to(0, toy)
  218. prex = 0
  219. prey = toy
  220. toxes = []
  221. toyes = []
  222. for n, i in enumerate(values):
  223. tox = w / (latest - earliest) * (times[n]-earliest)
  224. try:
  225. toy = ( zero_at ) - ( ( zero_at ) / biggest * i ) *0.9
  226. except:
  227. toy = zero_at
  228. toxes.append(tox)
  229. toyes.append(toy)
  230. #toy = min(toy, zero_at)
  231. main_layer.curve_to(
  232. tox - (tox - prex)/2,
  233. prey,
  234. prex + (tox - prex)/2,
  235. toy,
  236. tox,
  237. toy)
  238. prex = tox
  239. prey = toy
  240. main_layer.line_to( w, zero_at)
  241. main_layer.line_to( 0, zero_at)
  242. main_layer.set_source_rgba(0.2,0.8,0.2,0.5)
  243. main_layer.fill_preserve()
  244. main_layer.set_source_rgba(0.2,0.8,0.2,1)
  245. main_layer.stroke()
  246. # NEGATIVE VALUE
  247. try:
  248. toy = ( zero_at ) - ( ( zero_at ) / biggest * values[0] ) *0.9
  249. except:
  250. toy = zero_at
  251. #toy = max(toy, zero_at)
  252. main_layer.reset_clip()
  253. main_layer.rectangle(0,zero_at,w,h)
  254. main_layer.clip()
  255. main_layer.move_to(0, toy)
  256. prex = 0
  257. prey = toy
  258. for n, i in enumerate(values):
  259. tox = w / (latest - earliest) * (times[n]-earliest)
  260. try:
  261. toy = ( zero_at ) - ( ( zero_at ) / biggest * i ) *0.9
  262. except:
  263. toy = zero_at
  264. #toy = max(toy, zero_at)
  265. main_layer.curve_to(
  266. tox - (tox - prex)/2,
  267. prey,
  268. prex + (tox - prex)/2,
  269. toy,
  270. tox,
  271. toy)
  272. prex = tox
  273. prey = toy
  274. main_layer.line_to( w, zero_at)
  275. main_layer.line_to( 0, zero_at)
  276. main_layer.set_source_rgba(0.8,0.2,0.2,0.5)
  277. main_layer.fill_preserve()
  278. main_layer.set_source_rgba(0.8,0.2,0.2,1)
  279. main_layer.stroke()
  280. main_layer.reset_clip()
  281. # Reference line
  282. main_layer.set_source_rgba(0.7,0.7,0.7,1)
  283. main_layer.move_to( 0, zero_at )
  284. main_layer.line_to( w, zero_at )
  285. main_layer.stroke()
  286. main_layer.set_dash([10,10])
  287. main_layer.set_source_rgba(0.2,0.2,0.2,1)
  288. main_layer.move_to( 0, zero_at )
  289. main_layer.line_to( w, zero_at )
  290. main_layer.stroke()
  291. main_layer.set_dash([1])
  292. # MOUSE OVER SELECTOR
  293. def closest(l, v):
  294. distances = []
  295. for i in l:
  296. distances.append(max(i-v, v-i))
  297. try:
  298. return l[distances.index(min(distances))]
  299. except:
  300. return 0
  301. selectx = closest(toxes, mx)
  302. if selectx:
  303. selecty = toyes[toxes.index(selectx)]
  304. # Litte circle
  305. main_layer.arc(selectx, selecty, 8, 0, math.pi*2)
  306. main_layer.set_source_rgba(0.2,0.8,0.2,1)
  307. if selecty > zero_at:
  308. main_layer.set_source_rgba(0.8,0.2,0.2,1)
  309. main_layer.fill()
  310. # Line from that circle downwards
  311. main_layer.move_to(selectx, selecty)
  312. main_layer.line_to(selectx, zero_at)
  313. main_layer.stroke()
  314. # Data about this time frame
  315. to_data = times[toxes.index(selectx)]
  316. from_data = to_data - step
  317. try:
  318. from_data = time.strftime(show_format, time.gmtime(from_data))
  319. except:
  320. from_data = "0000-00-00"
  321. try:
  322. to_data = time.strftime(show_format, time.gmtime(to_data))
  323. except:
  324. to_data = "0000-00-00"
  325. # Counting the largest thing
  326. plist = ["From: "+from_data,
  327. "To: "+to_data,
  328. "Total: "+currancy+" "+str(round(values[toxes.index(selectx)], 2)) ]
  329. leng = 0
  330. for thing in plist:
  331. if len(str(thing))*6+2 > leng:
  332. leng = len(str(thing))*6+2
  333. if selectx > w/2:
  334. recx = selectx - leng - 10
  335. else:
  336. recx = selectx + 10
  337. if selecty + len(plist)*15 > h:
  338. recy = selecty - len(plist)*15
  339. else:
  340. recy = selecty
  341. main_layer.set_source_rgba(0.1,0.1,0.1,0.7)
  342. main_layer.rectangle(recx, recy, leng, len(plist)*15)
  343. main_layer.fill()
  344. for n, thing in enumerate(plist):
  345. main_layer.move_to(recx+2, recy+12+(15*n))
  346. main_layer.set_source_rgba(1,1,1,1)
  347. main_layer.show_text(thing)
  348. # Now let's get the values ( to the side of the graph )
  349. for i in range(int(h/20)):
  350. # TODO: This has to be tuned a bit. It's not perfect. But it's
  351. # very close.
  352. they = i*20+20
  353. try:
  354. value_is = round( biggest / zero_at * (zero_at - they), 2)
  355. except Exception as e:
  356. print("what", e)
  357. value_is = 0
  358. show_value = currancy + " " + str(value_is)
  359. if mx > w / 2:
  360. main_layer.set_source_rgba(0.1,0.1,0.1,0.5)
  361. main_layer.rectangle(2, 2+they,len(show_value)*6+4, 14)
  362. main_layer.fill()
  363. main_layer.move_to(3,12+they)
  364. main_layer.set_source_rgba(1,1,1,1)
  365. main_layer.show_text(show_value)
  366. else:
  367. main_layer.set_source_rgba(0.1,0.1,0.1,0.5)
  368. main_layer.rectangle(w-len(show_value)*6-4, 2+they,len(show_value)*6+4, 14)
  369. main_layer.fill()
  370. main_layer.move_to(w-len(show_value)*6-3,12+they)
  371. main_layer.set_source_rgba(1,1,1,1)
  372. main_layer.show_text(show_value)
  373. # Render a little pressed selector
  374. if "pressed" in data:
  375. for i in [data["pressed"], mx]:
  376. main_layer.set_source_rgba(0.7,0.7,0.7,1)
  377. main_layer.move_to( i, 0 )
  378. main_layer.line_to( i, h )
  379. main_layer.stroke()
  380. main_layer.set_dash([10,10])
  381. main_layer.set_source_rgba(0.2,0.2,0.2,1)
  382. main_layer.move_to( i, 0 )
  383. main_layer.line_to( i, h )
  384. main_layer.stroke()
  385. main_layer.set_dash([1])
  386. # Keep redrawing the graph
  387. d.queue_draw()
  388. def graph_button_press(w, e, data, da):
  389. data["pressed"] = e.x
  390. print(data["zoom"])
  391. print(e.x, e.y)
  392. def graph_button_release(w, e, data, da):
  393. if "pressed" in data:
  394. x = data["pressed"]
  395. # If there was no motion
  396. if x-2 < e.x < x+2:
  397. data["zoom"] = [0,0]
  398. else:
  399. w = da.get_allocated_width()
  400. zoom0 = data["zoom"][0] + ((data["zoom"][1] - data["zoom"][0]) / w * min(x, e.x))
  401. zoom1 = data["zoom"][0] + ((data["zoom"][1] - data["zoom"][0]) / w * max(x, e.x))
  402. data["zoom"] = [zoom0, zoom1]
  403. print(data["zoom"])
  404. del data["pressed"]
  405. def graph(win, data, title="", currancy="$", add_now=True, add_value="Same", graph_addition="add"):
  406. # adding one more data point for "now"
  407. if add_now:
  408. try:
  409. data["items"] = sorted(data["items"], key=lambda k: k["timestamp"])
  410. last = data["items"][-1].copy()
  411. last["timestamp"] = int(time.time())
  412. if not add_value == "Same":
  413. last["amount"] = add_value
  414. data["items"].append(last)
  415. except:
  416. pass
  417. event_box = Gtk.EventBox()
  418. da = Gtk.DrawingArea()
  419. da.set_size_request(200,200)
  420. da.connect("draw", graph_draw, win, data, currancy, graph_addition)
  421. event_box.connect("button-press-event", graph_button_press, data, da)
  422. event_box.connect("button-release-event", graph_button_release, data, da)
  423. event_box.add(da)
  424. return event_box