analytics.py 20 KB

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