analytics.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752
  1. #####################################################################
  2. # #
  3. # THIS IS A SOURCE CODE FILE FROM A PROGRAM TO INTERACT WITH THE #
  4. # LBRY PROTOCOL ( lbry.com ). IT WILL USE THE LBRY SDK ( lbrynet ) #
  5. # FROM THEIR REPOSITORY ( https://github.com/lbryio/lbry-sdk ) #
  6. # WHICH I GONNA PRESENT TO YOU AS A BINARY. SINCE I DID NOT DEVELOP #
  7. # IT AND I'M LAZY TO INTEGRATE IN A MORE SMART WAY. THE SOURCE CODE #
  8. # OF THE SDK IS AVAILABLE IN THE REPOSITORY MENTIONED ABOVE. #
  9. # #
  10. # ALL THE CODE IN THIS REPOSITORY INCLUDING THIS FILE IS #
  11. # (C) J.Y.Amihud and Other Contributors 2021. EXCEPT THE LBRY SDK. #
  12. # YOU CAN USE THIS FILE AND ANY OTHER FILE IN THIS REPOSITORY UNDER #
  13. # THE TERMS OF GNU GENERAL PUBLIC LICENSE VERSION 3 OR ANY LATER #
  14. # VERSION. TO FIND THE FULL TEXT OF THE LICENSE GO TO THE GNU.ORG #
  15. # WEBSITE AT ( https://www.gnu.org/licenses/gpl-3.0.html ). #
  16. # #
  17. # THE LBRY SDK IS UNFORTUNATELY UNDER THE MIT LICENSE. IF YOU ARE #
  18. # NOT INTENDING TO USE MY CODE AND JUST THE SDK. YOU CAN FIND IT ON #
  19. # THEIR OFFICIAL REPOSITORY ABOVE. THEIR LICENSE CHOICE DOES NOT #
  20. # SPREAD ONTO THIS PROJECT. DON'T GET A FALSE ASSUMPTION THAT SINCE #
  21. # THEY USE A PUSH-OVER LICENSE, I GONNA DO THE SAME. I'M NOT. #
  22. # #
  23. # THE LICENSE CHOSEN FOR THIS PROJECT WILL PROTECT THE 4 ESSENTIAL #
  24. # FREEDOMS OF THE USER FURTHER, BY NOT ALLOWING ANY WHO TO CHANGE #
  25. # THE LICENSE AT WILL. SO NO PROPRIETARY SOFTWARE DEVELOPER COULD #
  26. # TAKE THIS CODE AND MAKE THEIR USER-SUBJUGATING SOFTWARE FROM IT. #
  27. # #
  28. #####################################################################
  29. # Since LBRY wallet is just a long stream of increasing and decreasing
  30. # numbers. I call this the analytics.
  31. import os
  32. import math
  33. import json
  34. import time
  35. import random
  36. import colorsys
  37. import threading
  38. from gi.repository import Gtk
  39. from gi.repository import Gdk
  40. from gi.repository import GLib
  41. from gi.repository import cairo
  42. from flbry import fetch
  43. from flbry import settings
  44. def make_colors(amount=4, color=(0.1,0.5,0.8)):
  45. # TODO: Figure out how to got the themes prefered active color
  46. # and use it here.
  47. colors = []
  48. for i in range(amount):
  49. nc = colorsys.rgb_to_hsv(*color)
  50. nc = list(nc)
  51. if i != 0:
  52. nc[1] = 0
  53. nc[2] = (i)*(1/amount)
  54. colors.append(colorsys.hsv_to_rgb(*nc))
  55. return colors
  56. def window(win):
  57. awin = Gtk.Window()
  58. awin.set_title("FastLBRY GTK: Wallet / Analytics")
  59. awin.set_size_request(500, 500)
  60. notebook = Gtk.Notebook()
  61. notebook.set_scrollable(True)
  62. awin.add(notebook)
  63. ############################ ANALYTICS SECTION #############################
  64. box = Gtk.VBox()
  65. notebook.append_page(box, Gtk.Label("Totals / Analytics"))
  66. # Let's get...
  67. raw_balance = fetch.lbrynet("wallet_balance")
  68. # ... and format the balance information.
  69. balance = {
  70. "Spendable":raw_balance["available"],
  71. "Claim Bids":raw_balance["reserved_subtotals"]["claims"],
  72. "Supports":raw_balance["reserved_subtotals"]["supports"],
  73. "Tips":raw_balance["reserved_subtotals"]["tips"],
  74. }
  75. # Pie chart for balance
  76. def conv(t):
  77. return str(t) + " LBC"
  78. box.pack_start(pie_chart(win, balance, "Totals", converter=conv), 0, 0 , 0)
  79. minus_30_days = 60*60*24*30*-1
  80. transactions = {"items":[], # The list of items
  81. "zoom":[0,0], #minus_30_days,0], # The range of the selection ( by timestamp )
  82. "allow_negative":True
  83. }
  84. def getting_all_transactions(data, win):
  85. # This is a small function funnin in a separate thread, loading
  86. # all transactions so the graph could draw them.
  87. # TODO: this commented code works perfectly fine to get the kind of
  88. # data that I want. But it's hell'a heavy on my computer. Last time
  89. # I tried, it froze. So I need to figure out something about this.
  90. # cash_file = settings.get_settings_folder()+"GTK-transactions-graph.json"
  91. # try:
  92. # with open(cash_file) as json_file:
  93. # data["items"] = json.load(json_file)
  94. # except Exception as e:
  95. # print(e)
  96. # def add(out, addpage):
  97. # ret = False
  98. # added = 0
  99. # count = 0
  100. # for i in out["items"]:
  101. # # Removing unnesesary data
  102. # a = i.copy()
  103. # # a["amount"] = i["amount"]
  104. # # a["timestamp"] = i["timestamp"]
  105. # if a["timestamp"] == None: # Too recent
  106. # a["timestamp"] = int(time.time())
  107. # # a["txid"] = i["txid"]
  108. # # a["name"] = i.get("name", "")
  109. # if a not in data["items"]:
  110. # added += 1
  111. # data["items"].append(a)
  112. # elif not addpage:
  113. # count += 1
  114. # ret = True
  115. # return ret
  116. # Fetching
  117. # out = fetch.lbrynet("transaction_list", { "page_size":50})
  118. # all_of_them = out["total_items"]
  119. # addpage = 0
  120. # add(out, 0)
  121. # for i in range(out["total_pages"]):
  122. # out = fetch.lbrynet("transaction_list", {"page":addpage+i+2,
  123. # #"exclude_internal_transfers": True,
  124. # #"no_totals":True,
  125. # "page_size":50})
  126. # if add(out, addpage):
  127. # addpage = int((len(data["items"]))/50) - 2
  128. # # Save to cash
  129. # with open(cash_file, 'w') as fp:
  130. # json.dump(data["items"], fp)
  131. # if not win.keep_loading_the_wallet_graph or all_of_them == len(data["items"]):
  132. # return
  133. # THE MEANWHILE SOLUTION
  134. out = fetch.lbrynet("txo_plot", { "days_back":1000, # Fetch 100 days of txo
  135. "exclude_internal_transfers":True, # Without crap
  136. "is_not_my_input":True, # Not from me ( as in support only )
  137. })
  138. for i in out:
  139. a = {}
  140. a["amount"] = i["total"]
  141. a["timestamp"] = int(time.mktime(time.strptime(i["day"],"%Y-%m-%d")))
  142. data["items"].append(a)
  143. win.keep_loading_the_wallet_graph = True
  144. t = threading.Thread(target=getting_all_transactions, args=(transactions, win))
  145. t.setDaemon(True)
  146. t.start()
  147. def kill_graph(w):
  148. win.keep_loading_the_wallet_graph = False
  149. awin.connect("destroy", kill_graph)
  150. # Graph with the history of all transactions
  151. the_graph = graph(win, transactions, "Totals")
  152. box.pack_start(the_graph, 1, 1 , 0)
  153. awin.show_all()
  154. def box_of_color(color):
  155. def on_draw(widget, cr, data):
  156. width = widget.get_allocated_width()
  157. height = widget.get_allocated_height()
  158. cr.set_source_rgb(*color)
  159. cr.rectangle(0, 0, width, height)
  160. cr.fill()
  161. area = Gtk.DrawingArea()
  162. area.set_size_request(40, 40)
  163. area.connect("draw", on_draw, color)
  164. return area
  165. def pie_chart_draw(d, main_layer, win, data, colors):
  166. # Need to know how big is our chart
  167. w = d.get_allocated_width()
  168. h = d.get_allocated_height()
  169. # We want our circle to fit, so we find which one is
  170. # smaller.
  171. smaller = min(w, h)
  172. last = 0
  173. whole = float(math.pi*2)
  174. sum_of_data = 0
  175. for i in data:
  176. sum_of_data += float(data[i])
  177. for n, i in enumerate(data):
  178. this = whole* ( float(data[i]) / sum_of_data )
  179. main_layer.move_to(w/2, h/2)
  180. main_layer.arc(w/2, h/2, smaller/2, last, last+this)
  181. main_layer.close_path()
  182. main_layer.set_source_rgb(*colors[n%len(colors)])
  183. main_layer.fill()
  184. last = last+this
  185. def pie_chart(win, data, title="", converter=False):
  186. ret = Gtk.HBox(True)
  187. colors = make_colors(len(data))
  188. da = Gtk.DrawingArea()
  189. da.connect("draw", pie_chart_draw, win, data, colors)
  190. ret.pack_start(da, 1,1,5)
  191. lbox = Gtk.VBox()
  192. ret.pack_start(lbox, 1,1,5)
  193. sum_of_data = 0
  194. for i in data:
  195. sum_of_data += float(data[i])
  196. if converter:
  197. sum_of_data = converter(sum_of_data)
  198. lbox.pack_start(Gtk.Label(" Total : "+str(sum_of_data)+" "), 0,0,1)
  199. for n, i in enumerate(data):
  200. ibox = Gtk.HBox()
  201. lbox.pack_start(ibox, 0,0,3)
  202. ibox.pack_start(box_of_color(colors[n%len(colors)]), 0,0,0)
  203. show_size = data[i]
  204. if converter:
  205. show_size = converter(show_size)
  206. ibox.pack_start(Gtk.Label(" "+i+": "+str(show_size)+" "), 0,0,1)
  207. return ret
  208. def graph_draw(d, main_layer, win, data):
  209. data["items"] = sorted(data["items"], key=lambda k: k["timestamp"])
  210. # Need to know how big is our graph
  211. w = d.get_allocated_width()
  212. h = d.get_allocated_height()
  213. if data.get("allow_negative", False):
  214. zero_at = h / 2
  215. else:
  216. zero_at = h
  217. # The mouse position of a given frame
  218. mx = d.get_pointer()[0]
  219. my = d.get_pointer()[1]
  220. # Test of the mouse position
  221. # main_layer.move_to(0,0)
  222. # main_layer.line_to(mx, my)
  223. # main_layer.stroke()
  224. len_day = 60*60*24 # Seconds in a day
  225. len_hour = 60*60 # Seconds in an hour
  226. len_minute = 60 # Seconds in a minute
  227. # Here we are getting the latest and the earliest
  228. # timestamp, so we could calculate the step of the
  229. # graph. ( So the data will be readable )
  230. latest = 0
  231. try:
  232. earliest = data["items"][0]["timestamp"]
  233. except:
  234. earliest = 0
  235. for i in data["items"]:
  236. if i.get("timestamp", 0) > latest:
  237. latest = i.get("timestamp", 0)
  238. if i.get("timestamp", 0) < earliest:
  239. earliest = i.get("timestamp", 0)
  240. # Now let's look at our zoom value
  241. for n, e in enumerate([earliest, latest]):
  242. if data["zoom"][n] == 0:
  243. data["zoom"][n] = e
  244. earliest, latest = data["zoom"]
  245. # Now I want to make a scale of dates from left
  246. # to right.
  247. main_layer.select_font_face("Monospace")
  248. main_layer.set_font_size(10)
  249. full_date = "%Y-%m-%d %H:%M:%S"
  250. only_date = "%Y-%m-%d"
  251. if latest - earliest > 10 * len_day:
  252. show_format = only_date
  253. count = int( w / (len("xxxx-xx-xx")*6+12) )
  254. else:
  255. show_format = full_date
  256. count = int( w / (len("xxxx-xx-xx xx:xx:xx")*6+12) )
  257. # Now I want to show the current date / time for
  258. # the area where the user is hovering.
  259. suglen = len("xxxx-xx-xx xx:xx:xx")*6+12
  260. thexm = mx-suglen/2
  261. if thexm < 2:
  262. thexm = 2
  263. elif thexm > w - suglen - 2:
  264. thexm = w - suglen - 2
  265. try:
  266. res_date = int( ( latest - earliest ) / w * mx + earliest )
  267. show_date = time.strftime(full_date, time.gmtime(res_date))
  268. except:
  269. show_date = "0000-00-00"
  270. main_layer.set_source_rgba(0.1,0.1,0.1,0.5)
  271. main_layer.rectangle(2+thexm,2,len(show_date)*6+2, 14)
  272. main_layer.fill()
  273. main_layer.move_to(3+thexm,12)
  274. main_layer.set_source_rgba(1,1,1,1)
  275. main_layer.show_text(str(show_date))
  276. # main_layer.set_source_rgba(0.7,0.7,0.7,1)
  277. # main_layer.move_to( mx, 20 )
  278. # main_layer.line_to( mx, h )
  279. # main_layer.stroke()
  280. # main_layer.set_dash([10,10])
  281. # main_layer.set_source_rgba(0.2,0.2,0.2,1)
  282. # main_layer.move_to( mx, 20 )
  283. # main_layer.line_to( mx, h )
  284. # main_layer.stroke()
  285. # main_layer.set_dash([1])
  286. # And the rest of the dates
  287. for date in range(count):
  288. try:
  289. res_date = int( ( latest - earliest ) / count * date + earliest )
  290. show_date = time.strftime(show_format, time.gmtime(res_date))
  291. except:
  292. show_date = "0000-00-00"
  293. thex = w / count * date
  294. # If not in range of the mouse ( so I could show the current day
  295. # for that specific area ).
  296. if int(thex) not in range(int(thexm-suglen/2), int(thexm+suglen)):
  297. main_layer.set_source_rgba(0.1,0.1,0.1,0.5)
  298. main_layer.rectangle(2+thex,2,len(show_date)*6+2, 14)
  299. main_layer.fill()
  300. main_layer.move_to(3+thex,12)
  301. main_layer.set_source_rgba(1,1,1,1)
  302. main_layer.show_text(str(show_date))
  303. # A step is how often will there be a data point
  304. # of the graph. Step of one minute, means every
  305. # point on the graph will consist all the data
  306. # happened in this minute.
  307. step = (latest - earliest) / (w / 20) # A second
  308. # Now we need the smallest and biggest value in a
  309. # given step
  310. values = []
  311. times = []
  312. pstep = earliest
  313. s = 0
  314. for n, i in enumerate(data["items"]):
  315. if i.get("timestamp", 0) < earliest:
  316. continue
  317. s += float(i.get("amount", i.get("value", 0)))
  318. if i.get("timestamp", 0) > pstep + step-1:
  319. pstep = i.get("timestamp", n)
  320. values.append(s)
  321. times.append(pstep)
  322. s = 0
  323. if i.get("timestamp", 0) > latest:
  324. break
  325. # If there is only on value
  326. if len(values) == 1:
  327. # We want to add a few move on both ends
  328. step = 10
  329. values = [0, values[0], 0]
  330. times = [times[0]-step, times[0], times[0]+step]
  331. latest = times[-1]
  332. earliest = times[0]
  333. # Finding the farthest point from the center
  334. # center being the 0 (zero)
  335. try:
  336. biggest = max(values)
  337. if min(values) * -1 > biggest:
  338. biggest = min(values) * -1 # Multuply by -1 reverses the - to a +
  339. except Exception as e:
  340. biggest = 0
  341. # Now let's draw it
  342. main_layer.set_line_cap(cairo.LineCap.ROUND)
  343. # POSITIVE VALUE
  344. main_layer.move_to(0, zero_at)
  345. prex = 0
  346. prey = zero_at
  347. toxes = []
  348. toyes = []
  349. for n, i in enumerate(values):
  350. tox = w / (latest - earliest) * (times[n]-earliest)
  351. toy = ( zero_at ) - ( ( zero_at ) / biggest * i ) *0.9
  352. toy = min(toy, zero_at)
  353. toxes.append(tox)
  354. toyes.append(toy)
  355. main_layer.curve_to(
  356. tox - (tox - prex)/2,
  357. prey,
  358. prex + (tox - prex)/2,
  359. toy,
  360. tox,
  361. toy)
  362. prex = tox
  363. prey = toy
  364. main_layer.line_to( w, zero_at)
  365. main_layer.set_source_rgba(0.2,0.8,0.2,0.5)
  366. main_layer.fill_preserve()
  367. main_layer.set_source_rgba(0.2,0.8,0.2,1)
  368. main_layer.stroke()
  369. # NEGATIVE VALUE
  370. # TODO: to make negative values appear in the graph, we have to have
  371. # negative values. Meanwhile I will comment it, to save time while rendering
  372. # the graph. NOTE: The code was originally a copy of the code above,
  373. # the positive values, but I improved the positive values since then.
  374. # main_layer.move_to(0, zero_at)
  375. # for n, i in enumerate(values):
  376. # tox = w / (latest - earliest) * (latest - times[n])
  377. # toy = ( zero_at ) - ( ( zero_at ) / biggest * i ) *0.9
  378. # toy = max(toy, zero_at)
  379. # main_layer.line_to(tox, toy)
  380. # main_layer.line_to( w, zero_at)
  381. # main_layer.set_source_rgba(0.8,0.2,0.2,0.5)
  382. # main_layer.fill_preserve()
  383. # main_layer.set_source_rgba(0.8,0.2,0.2,1)
  384. # main_layer.stroke()
  385. # Reference line
  386. main_layer.set_source_rgba(0.7,0.7,0.7,1)
  387. main_layer.move_to( 0, zero_at )
  388. main_layer.line_to( w, zero_at )
  389. main_layer.stroke()
  390. main_layer.set_dash([10,10])
  391. main_layer.set_source_rgba(0.2,0.2,0.2,1)
  392. main_layer.move_to( 0, zero_at )
  393. main_layer.line_to( w, zero_at )
  394. main_layer.stroke()
  395. main_layer.set_dash([1])
  396. # MOUSE OVER SELECTOR
  397. def closest(l, v):
  398. distances = []
  399. for i in l:
  400. distances.append(max(i-v, v-i))
  401. try:
  402. return l[distances.index(min(distances))]
  403. except:
  404. return 0
  405. selectx = closest(toxes, mx)
  406. if selectx:
  407. selecty = toyes[toxes.index(selectx)]
  408. # Litte circle
  409. main_layer.arc(selectx, selecty, 8, 0, math.pi*2)
  410. main_layer.set_source_rgba(0.2,0.8,0.2,1)
  411. main_layer.fill()
  412. # Line from that circle downwards
  413. main_layer.move_to(selectx, selecty)
  414. main_layer.line_to(selectx, zero_at)
  415. main_layer.stroke()
  416. # Data about this time frame
  417. to_data = times[toxes.index(selectx)]
  418. from_data = to_data - step
  419. try:
  420. from_data = time.strftime(show_format, time.gmtime(from_data))
  421. except:
  422. from_data = "0000-00-00"
  423. try:
  424. to_data = time.strftime(show_format, time.gmtime(to_data))
  425. except:
  426. to_data = "0000-00-00"
  427. # Counting the largest thing
  428. plist = ["From: "+from_data,
  429. "To: "+to_data,
  430. "Total: "+str(round(values[toxes.index(selectx)], 2))+" LBC" ]
  431. leng = 0
  432. for thing in plist:
  433. if len(str(thing))*6+2 > leng:
  434. leng = len(str(thing))*6+2
  435. if selectx > w/2:
  436. recx = selectx - leng - 10
  437. else:
  438. recx = selectx + 10
  439. if selecty + len(plist)*15 > h:
  440. recy = selecty - len(plist)*15
  441. else:
  442. recy = selecty
  443. main_layer.set_source_rgba(0.1,0.1,0.1,0.7)
  444. main_layer.rectangle(recx, recy, leng, len(plist)*15)
  445. main_layer.fill()
  446. for n, thing in enumerate(plist):
  447. main_layer.move_to(recx+2, recy+12+(15*n))
  448. main_layer.set_source_rgba(1,1,1,1)
  449. main_layer.show_text(thing)
  450. # Now let's get the values ( to the side of the graph )
  451. for i in range(int(h/20)):
  452. # TODO: This has to be tuned a bit. It's not perfect. But it's
  453. # very close.
  454. they = i*20+20
  455. try:
  456. value_is = round( biggest / zero_at * (zero_at - they), 2)
  457. except Exception as e:
  458. print("what", e)
  459. value_is = 0
  460. show_value = str(value_is) + " LBC"
  461. main_layer.set_source_rgba(0.1,0.1,0.1,0.5)
  462. main_layer.rectangle(2, 2+they,len(show_value)*6+4, 14)
  463. main_layer.fill()
  464. main_layer.move_to(3,12+they)
  465. main_layer.set_source_rgba(1,1,1,1)
  466. main_layer.show_text(show_value)
  467. # Render a little pressed selector
  468. if "pressed" in data:
  469. for i in [data["pressed"], mx]:
  470. main_layer.set_source_rgba(0.7,0.7,0.7,1)
  471. main_layer.move_to( i, 0 )
  472. main_layer.line_to( i, h )
  473. main_layer.stroke()
  474. main_layer.set_dash([10,10])
  475. main_layer.set_source_rgba(0.2,0.2,0.2,1)
  476. main_layer.move_to( i, 0 )
  477. main_layer.line_to( i, h )
  478. main_layer.stroke()
  479. main_layer.set_dash([1])
  480. # Keep redrawing the graph
  481. d.queue_draw()
  482. def graph_button_press(w, e, data, da):
  483. data["pressed"] = e.x
  484. print(data["zoom"])
  485. print(e.x, e.y)
  486. def graph_button_release(w, e, data, da):
  487. if "pressed" in data:
  488. x = data["pressed"]
  489. # If there was no motion
  490. if x-2 < e.x < x+2:
  491. data["zoom"] = [0,0]
  492. else:
  493. w = da.get_allocated_width()
  494. zoom0 = data["zoom"][0] + ((data["zoom"][1] - data["zoom"][0]) / w * min(x, e.x))
  495. zoom1 = data["zoom"][0] + ((data["zoom"][1] - data["zoom"][0]) / w * max(x, e.x))
  496. data["zoom"] = [zoom0, zoom1]
  497. print(data["zoom"])
  498. del data["pressed"]
  499. def graph(win, data, title=""):
  500. event_box = Gtk.EventBox()
  501. da = Gtk.DrawingArea()
  502. da.set_size_request(100,100)
  503. da.connect("draw", graph_draw, win, data)
  504. event_box.connect("button-press-event", graph_button_press, data, da)
  505. event_box.connect("button-release-event", graph_button_release, data, da)
  506. event_box.add(da)
  507. return event_box