pub-analyzer 0.5.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. pub_analyzer/__init__.py +1 -0
  2. pub_analyzer/__main__.py +7 -0
  3. pub_analyzer/css/body.tcss +87 -0
  4. pub_analyzer/css/buttons.tcss +24 -0
  5. pub_analyzer/css/checkbox.tcss +29 -0
  6. pub_analyzer/css/collapsible.tcss +31 -0
  7. pub_analyzer/css/datatable.tcss +50 -0
  8. pub_analyzer/css/editor.tcss +60 -0
  9. pub_analyzer/css/main.tcss +50 -0
  10. pub_analyzer/css/report.tcss +131 -0
  11. pub_analyzer/css/search.tcss +81 -0
  12. pub_analyzer/css/summary.tcss +75 -0
  13. pub_analyzer/css/tabs.tcss +18 -0
  14. pub_analyzer/css/tree.tcss +44 -0
  15. pub_analyzer/internal/__init__.py +1 -0
  16. pub_analyzer/internal/identifier.py +106 -0
  17. pub_analyzer/internal/limiter.py +34 -0
  18. pub_analyzer/internal/render.py +41 -0
  19. pub_analyzer/internal/report.py +497 -0
  20. pub_analyzer/internal/templates/author_report.typ +591 -0
  21. pub_analyzer/main.py +81 -0
  22. pub_analyzer/models/__init__.py +1 -0
  23. pub_analyzer/models/author.py +87 -0
  24. pub_analyzer/models/concept.py +19 -0
  25. pub_analyzer/models/institution.py +138 -0
  26. pub_analyzer/models/report.py +111 -0
  27. pub_analyzer/models/source.py +77 -0
  28. pub_analyzer/models/topic.py +59 -0
  29. pub_analyzer/models/work.py +158 -0
  30. pub_analyzer/widgets/__init__.py +1 -0
  31. pub_analyzer/widgets/author/__init__.py +1 -0
  32. pub_analyzer/widgets/author/cards.py +65 -0
  33. pub_analyzer/widgets/author/core.py +122 -0
  34. pub_analyzer/widgets/author/tables.py +50 -0
  35. pub_analyzer/widgets/body.py +55 -0
  36. pub_analyzer/widgets/common/__init__.py +18 -0
  37. pub_analyzer/widgets/common/card.py +29 -0
  38. pub_analyzer/widgets/common/filesystem.py +203 -0
  39. pub_analyzer/widgets/common/filters.py +111 -0
  40. pub_analyzer/widgets/common/input.py +97 -0
  41. pub_analyzer/widgets/common/label.py +36 -0
  42. pub_analyzer/widgets/common/modal.py +43 -0
  43. pub_analyzer/widgets/common/selector.py +66 -0
  44. pub_analyzer/widgets/common/summary.py +7 -0
  45. pub_analyzer/widgets/institution/__init__.py +1 -0
  46. pub_analyzer/widgets/institution/cards.py +78 -0
  47. pub_analyzer/widgets/institution/core.py +122 -0
  48. pub_analyzer/widgets/institution/tables.py +24 -0
  49. pub_analyzer/widgets/report/__init__.py +1 -0
  50. pub_analyzer/widgets/report/author.py +43 -0
  51. pub_analyzer/widgets/report/cards.py +130 -0
  52. pub_analyzer/widgets/report/concept.py +47 -0
  53. pub_analyzer/widgets/report/core.py +308 -0
  54. pub_analyzer/widgets/report/editor.py +80 -0
  55. pub_analyzer/widgets/report/export.py +112 -0
  56. pub_analyzer/widgets/report/grants.py +85 -0
  57. pub_analyzer/widgets/report/institution.py +39 -0
  58. pub_analyzer/widgets/report/locations.py +75 -0
  59. pub_analyzer/widgets/report/source.py +90 -0
  60. pub_analyzer/widgets/report/topic.py +55 -0
  61. pub_analyzer/widgets/report/work.py +391 -0
  62. pub_analyzer/widgets/search/__init__.py +11 -0
  63. pub_analyzer/widgets/search/core.py +96 -0
  64. pub_analyzer/widgets/search/results.py +82 -0
  65. pub_analyzer/widgets/sidebar.py +70 -0
  66. pub_analyzer-0.5.6.dist-info/METADATA +102 -0
  67. pub_analyzer-0.5.6.dist-info/RECORD +70 -0
  68. pub_analyzer-0.5.6.dist-info/WHEEL +4 -0
  69. pub_analyzer-0.5.6.dist-info/entry_points.txt +3 -0
  70. pub_analyzer-0.5.6.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,591 @@
1
+ // This document was generated using Pub Analyzer.
2
+ //
3
+ // Pub Analyzer is a tool designed to retrieve, process and present in a concise and understandable
4
+ // way the scientific production of a researcher, including detailed information about their articles,
5
+ // citations, collaborations and other relevant metrics.
6
+ //
7
+ // See more here: https://pub-analyzer.com
8
+
9
+ // Packages
10
+ //
11
+ // This document uses the Cetz package to render plots and graphs. For more information
12
+ // on how to edit the plots see: https://typst.app/universe/package/cetz/
13
+
14
+ #import "@preview/cetz:0.3.4"
15
+ #import "@preview/cetz-plot:0.1.1": plot, chart
16
+
17
+ // Colors
18
+ //
19
+ // The following variables control all colors used in the document.
20
+ // You can modify the color codes by specifying the four RGB(A) components or by
21
+ // using the hexadecimal code.
22
+ //
23
+ // See more here: https://typst.app/docs/reference/visualize/color/#definitions-rgb
24
+
25
+ #let SUCCESS = rgb("#909d63")
26
+ #let ERROR = rgb("#bc5653")
27
+
28
+ #let CATEGORY_1 = rgb("#42a2f8")
29
+ #let CATEGORY_2 = rgb("#82d452")
30
+ #let CATEGORY_3 = rgb("#929292")
31
+ #let CATEGORY_4 = rgb("#f0bb40")
32
+ #let CATEGORY_5 = rgb("#eb4025")
33
+ #let CATEGORY_6 = rgb("#c33375")
34
+
35
+ #let PALETTE = (CATEGORY_1, CATEGORY_2, CATEGORY_3, CATEGORY_4, CATEGORY_5, CATEGORY_6)
36
+
37
+ // Get data
38
+ #let report = json(bytes(sys.inputs.report))
39
+ #let version = str(bytes(sys.inputs.version))
40
+ #let author = report.at("author")
41
+ #let works = report.at("works")
42
+ #let citation_summary = report.at("citation_summary")
43
+ #let open_access_summary = report.at("open_access_summary")
44
+ #let works_type_summary = report.at("works_type_summary")
45
+ #let sources_summary = report.at("sources_summary")
46
+
47
+ // Set document metadata.
48
+ #let description = "This document was generated using Pub Analyzer version " + version + "."
49
+ #set document(
50
+ title: "Pub Analyzer",
51
+ description: description,
52
+ )
53
+
54
+ // Page Layout
55
+ #set page("us-letter")
56
+ #set page(flipped: true)
57
+
58
+ #set page(footer: grid(
59
+ columns: (1fr, 1fr),
60
+ align(left)[Made with #link("https://pub-analyzer.com")[_pub-analyzer_] version #version],
61
+ align(right)[#context counter(page).display("1")],
62
+ )
63
+ )
64
+
65
+ // Text config
66
+ #set heading(numbering: "1.")
67
+ #set text(size: 10.5pt)
68
+ #set par(linebreaks: "simple", justify: true)
69
+ #set text(lang: "en", overhang: true, font: "New Computer Modern")
70
+
71
+ // Override reference
72
+ #show ref: it => {
73
+ let el = it.element
74
+ if el != none and el.func() == heading {
75
+ // Override heading references.
76
+ numbering(
77
+ el.numbering,
78
+ ..counter(heading).at(el.location())
79
+ )
80
+ } else {
81
+ // Other references as usual.
82
+ it
83
+ }
84
+ }
85
+
86
+ // Shortcuts
87
+ #let capitalize(input) = {
88
+ return upper(input.first()) + input.slice(1)
89
+ }
90
+
91
+
92
+ // Header
93
+ #grid(
94
+ columns: (1fr),
95
+ row-gutter: 11pt,
96
+ align: center,
97
+
98
+ [
99
+ #text(size: 17pt, weight: "bold")[#author.at("display_name")]
100
+ ],
101
+ if author.at("last_known_institutions") != none and author.at("last_known_institutions").len() >= 1 [
102
+ #let last_known_institution = author.at("last_known_institutions").first()
103
+ #text(size: 15pt, weight: "thin")[#last_known_institution.at("display_name")]
104
+ ]
105
+ )
106
+
107
+ // Author Summary
108
+ = Author.
109
+
110
+ #let summary-card(title: "Title", body) = {
111
+ return block(
112
+ width: 100%, height: 150pt,
113
+ stroke: 1pt, radius: 2pt,
114
+ inset: (top: 20pt),
115
+ fill: rgb("e5e7eb"),
116
+ )[
117
+ #align(center)[#text(size: 12pt)[#title]]
118
+ #v(5pt)
119
+ #block(width: 100%, inset: (x: 20pt))[#body]
120
+ ]
121
+ }
122
+
123
+ // Cards
124
+ #grid(
125
+ columns: (1fr, 1fr, 1fr),
126
+ column-gutter: 15pt,
127
+
128
+ // Last institution.
129
+ [
130
+ #summary-card(title:"Last institution:")[
131
+ #if author.at("last_known_institutions") != none and author.at("last_known_institutions").len() >= 1 [
132
+ #let last_known_institution = author.at("last_known_institutions").first()
133
+ #let institution_type_name = capitalize(last_known_institution.at("type"))
134
+
135
+ #grid(
136
+ rows: auto, row-gutter: 10pt,
137
+
138
+ [*Name:* #last_known_institution.at("display_name")],
139
+ [*Country:* #last_known_institution.at("country_code")],
140
+ [*Type:* #institution_type_name],
141
+ )
142
+ ] else [
143
+ #text(size: 9pt, fill: luma(50%))[No associated institutions were found.]
144
+ ]
145
+ ]
146
+ ],
147
+
148
+ // Author identifiers.
149
+ [
150
+ #summary-card(title:"Identifiers:")[
151
+ #grid(
152
+ rows: auto, row-gutter: 10pt,
153
+ ..(
154
+ author.at("ids").pairs().filter(id => id.last() != none).map(
155
+ ((k, v)) => grid.cell[
156
+ - #underline( [#link(v)[#k]] )
157
+ ]
158
+ ).flatten()
159
+ )
160
+ )
161
+ ]
162
+ ],
163
+
164
+ // Citation metrics.
165
+ [
166
+ #summary-card(title: "Citation metrics:")[
167
+ #let summary_stats = author.at("summary_stats")
168
+
169
+ #grid(
170
+ rows: auto, row-gutter: 10pt,
171
+
172
+ [*2-year mean:* #calc.round(summary_stats.at("2yr_mean_citedness"), digits: 5)],
173
+ [*h-index:* #summary_stats.at("h_index")],
174
+ [*i10 index:* #summary_stats.at("i10_index")],
175
+ )
176
+ ]
177
+ ]
178
+ )
179
+
180
+ #align(center, text(11pt)[_Counts by year_])
181
+ #grid(
182
+ columns: (1fr, 1fr),
183
+ column-gutter: 15pt,
184
+ align: (auto, horizon),
185
+
186
+
187
+ [
188
+ #table(
189
+ columns: (1fr, 2fr, 2fr),
190
+ inset: 8pt,
191
+ align: horizon,
192
+ // Headers
193
+ [*Year*], [*Works count*], [*Cited by count*],
194
+
195
+ // Content
196
+ ..author.at("counts_by_year").rev().slice(0, calc.min(author.at("counts_by_year").len(), 8)).map(
197
+ ((year, works_count, cited_by_count)) => (
198
+ table.cell([#year]),
199
+ table.cell([#works_count]),
200
+ table.cell([#cited_by_count]),
201
+ )
202
+ ).flatten()
203
+ )
204
+ ],
205
+ grid.cell(
206
+ inset: (x: 10pt, bottom: 10pt, top: 2.5pt),
207
+ stroke: 1pt
208
+ )[
209
+ #align(center, text(10pt)[Cites by year])
210
+ #v(5pt)
211
+ #cetz.canvas(length: 100%, {
212
+ plot.plot(
213
+ size: (0.90, 0.48),
214
+ axis-style: "scientific-auto",
215
+ plot-style: (stroke: (1pt + PALETTE.at(0)),),
216
+ x-min: auto, x-max: auto,
217
+ x-tick-step: 1, y-tick-step: auto,
218
+ x-label: none, y-label: none,
219
+ {
220
+ plot.add((
221
+ ..author.at("counts_by_year").rev().slice(0, calc.min(author.at("counts_by_year").len(), 8)).map(
222
+ ((year, works_count, cited_by_count)) => (
223
+ (year, cited_by_count)
224
+ )
225
+ )
226
+ ))
227
+ })
228
+ })
229
+ ]
230
+ )
231
+
232
+ // Works
233
+ #pagebreak()
234
+ = Works.
235
+
236
+ #let works_metrics_card(title: "Title", graph, body) = {
237
+ grid(
238
+ rows: (18pt, 175pt, 60pt),
239
+ columns: (100%),
240
+
241
+ [
242
+ #block(width: 100%, height: 100%)[
243
+ #align(center + horizon)[#text(style: "italic")[#title]]
244
+ ]
245
+ ],
246
+ [
247
+ #block(width: 100%, height: 100%)[
248
+ #align(center + horizon)[#graph]
249
+ ]
250
+ ],
251
+ [
252
+ #set text(size: 9.5pt)
253
+ #block(width: 100%, height: 100%, inset: (x: 0pt, y: 10pt))[#body]
254
+ ],
255
+ )
256
+ }
257
+
258
+ #let leyend_box(color: rgb) = {
259
+ box(height: 7pt, width: 7pt, fill: color)
260
+ }
261
+
262
+ #grid(
263
+ columns: (1fr, 1fr, 1fr),
264
+ column-gutter: 15pt,
265
+
266
+ [
267
+ #let graph = {
268
+ let type_a = citation_summary.at("type_a_count")
269
+ let type_b = citation_summary.at("type_b_count")
270
+ let total = type_a + type_b
271
+
272
+ if total == 0 {
273
+ cetz.canvas(length: 35%, {
274
+ cetz.draw.circle(
275
+ (0,0),
276
+ radius: 1,
277
+ stroke: luma(90%),
278
+ fill: luma(98%),
279
+ )
280
+ cetz.draw.content(
281
+ (0, 0), text("No citations found", size: 9pt, fill: luma(50%))
282
+ )
283
+ })
284
+ } else {
285
+ cetz.canvas(length: 35%, {
286
+ chart.piechart(
287
+ (type_a, type_b),
288
+ radius: 1,
289
+ slice-style: (PALETTE.at(0), PALETTE.at(1)),
290
+ outer-label: (content: "%", radius: 115%),
291
+ )
292
+ })
293
+ }
294
+ }
295
+
296
+ #works_metrics_card(title: "Citation metrics", graph)[
297
+ #grid(
298
+ rows: auto, row-gutter: 10pt,
299
+ columns: (1fr, 1fr),
300
+
301
+ grid.cell(colspan: 2)[
302
+ *Count:* #citation_summary.values().sum()
303
+ ],
304
+ [
305
+ #leyend_box(color: PALETTE.at(0)) *Type A:* #citation_summary.at("type_a_count")
306
+ ],
307
+ [
308
+ #leyend_box(color: PALETTE.at(1)) *Type B:* #citation_summary.at("type_b_count")
309
+ ],
310
+ )
311
+ ]
312
+ ],
313
+ [
314
+ #let graph = {
315
+ cetz.canvas(length: 35%, {
316
+ chart.columnchart(
317
+ size: (2.45, 2.0),
318
+ y-grid: false,
319
+ bar-style: cetz.palette.new(
320
+ base: (stroke: none, fill: none),
321
+ colors: PALETTE
322
+ ),
323
+ (
324
+ works_type_summary.slice(0, calc.min(4, works_type_summary.len())).map(
325
+ ((type_name, count)) => (
326
+ (capitalize(type_name.slice(0,2)), count)
327
+ )
328
+ )
329
+ )
330
+ )
331
+ })
332
+ }
333
+ #works_metrics_card(title: "Work Type", graph)[
334
+ #grid(
335
+ rows: auto, row-gutter: 10pt,
336
+ columns: (1fr, 1fr),
337
+ column-gutter: 5pt,
338
+
339
+ grid.cell(colspan: 2)[
340
+ *Count:* #open_access_summary.values().sum()
341
+ ],
342
+ ..works_type_summary.slice(0, calc.min(4, works_type_summary.len())).enumerate().map(
343
+ ((idx, work_type)) => (
344
+ grid.cell([#leyend_box(color: PALETTE.at(idx)) *#capitalize(work_type.type_name):* #work_type.count])
345
+ )
346
+ )
347
+ )
348
+ ]
349
+ ],
350
+ [
351
+ #let graph = {
352
+ cetz.canvas(length: 35%, {
353
+ chart.piechart(
354
+ (
355
+ open_access_summary.diamond, // diamond
356
+ open_access_summary.gold, // Gold
357
+ open_access_summary.green, // Green
358
+ open_access_summary.hybrid, // Hybrid
359
+ open_access_summary.bronze, // Bronze
360
+ open_access_summary.closed, // Closed
361
+ ),
362
+ radius: 1,
363
+ inner-radius: .4,
364
+ slice-style: (PALETTE.at(0), PALETTE.at(3), PALETTE.at(1), PALETTE.at(4), PALETTE.at(5), PALETTE.at(2)),
365
+ outer-label: (content: "%", radius: 115%),
366
+ )
367
+ })
368
+ }
369
+
370
+ #works_metrics_card(title: "Open Access", graph)[
371
+ #grid(
372
+ rows: auto, row-gutter: 10pt,
373
+ columns: (1.17fr, 1fr, 1fr),
374
+ column-gutter: 5pt,
375
+
376
+ grid.cell(colspan: 3)[
377
+ *Count:* #open_access_summary.values().sum()
378
+ ],
379
+
380
+ [#leyend_box(color: PALETTE.at(0)) *Diamond:* #open_access_summary.diamond],
381
+ [#leyend_box(color: PALETTE.at(3)) *Gold:* #open_access_summary.gold],
382
+ [#leyend_box(color: PALETTE.at(1)) *Green:* #open_access_summary.green],
383
+ [#leyend_box(color: PALETTE.at(5)) *Bronze:* #open_access_summary.bronze],
384
+ [#leyend_box(color: PALETTE.at(2)) *Closed:* #open_access_summary.closed],
385
+ [#leyend_box(color: PALETTE.at(4)) *Hybrid:* #open_access_summary.hybrid],
386
+ )
387
+ ]
388
+ ]
389
+ )
390
+
391
+ #let first-pub-year(works, default: "-") = {
392
+ for w in works {
393
+ if w.work.publication_year != none {
394
+ return w.work.publication_year
395
+ }
396
+ }
397
+ default
398
+ }
399
+
400
+ #let last-pub-year(works, default: "-") = {
401
+ for w in works.rev() {
402
+ if w.work.publication_year != none {
403
+ return w.work.publication_year
404
+ }
405
+ }
406
+ default
407
+ }
408
+
409
+ #align(
410
+ center,
411
+ text(11pt)[
412
+ Works from #first-pub-year(works) to #last-pub-year(works)
413
+ ]
414
+ )
415
+
416
+ #{
417
+ set text(size: 10pt)
418
+ table(
419
+ columns: (auto, 2fr, auto, auto, auto, auto, auto, auto, auto),
420
+ inset: 8pt,
421
+ align: horizon,
422
+ // Headers
423
+ [], [*Title*], [*Type*], [*DOI*], [*Publication Date*], [*Cited by count*], [*Type A*], [*Type B*], [*OA*],
424
+
425
+ // Content
426
+ ..works.enumerate().map(
427
+ ((idx, work)) => (
428
+ table.cell([#underline[#link(label("work_" + str(idx)))[#ref(label("work_" + str(idx)))]]]),
429
+ table.cell([#work.work.title]),
430
+ table.cell([#work.work.type]),
431
+ table.cell([#if work.work.ids.doi != none [#underline[#link(work.work.ids.doi)[DOI]]] else [#align(center)[-]]]),
432
+ table.cell([#work.work.publication_date]),
433
+ table.cell([#work.citation_summary.values().sum()]),
434
+ table.cell([#work.citation_summary.type_a_count]),
435
+ table.cell([#work.citation_summary.type_b_count]),
436
+ table.cell([#work.work.open_access.oa_status]),
437
+ )
438
+ ).flatten()
439
+ )
440
+ }
441
+
442
+ // Works Extended
443
+ #let work_driven_version = (
444
+ submittedVersion: "submitted",
445
+ acceptedVersion: "accepted",
446
+ publishedVersion: "published"
447
+ )
448
+ #for (idx, work_report) in works.enumerate() [
449
+ #let work = work_report.work
450
+
451
+ #pagebreak()
452
+ #heading(level: 2)[#work.title] #label("work_" + str(idx))
453
+
454
+
455
+ #if work.abstract != none [
456
+ #v(5pt)
457
+ #work.abstract
458
+ ]
459
+
460
+ // Cards
461
+ #v(5pt)
462
+ #grid(
463
+ columns: (1fr, 1fr, 1fr),
464
+ column-gutter: 30pt,
465
+
466
+ [
467
+ #align(center)[_Authorships_]
468
+ #block()[
469
+ #for authorship in work.authorships.slice(0, calc.min(10, work.authorships.len())) [
470
+ #let author_link = if authorship.author.at("orcid") != none {
471
+ authorship.author.orcid
472
+ } else {
473
+ authorship.author.id
474
+ }
475
+ - *#authorship.author_position:* #underline[#link(author_link)[#if authorship.author.display_name == author.display_name [#text(rgb(SUCCESS))[#authorship.author.display_name]] else [#authorship.author.display_name]]]
476
+ ]
477
+ #if work.authorships.len() > 10 [- *...*]
478
+ ]
479
+ ],
480
+ [
481
+ #align(center)[_Open Access_]
482
+
483
+ - *Status:* #capitalize(work.open_access.oa_status)
484
+ #if work.open_access.oa_url != none [- *URL:* #underline[#link(work.open_access.oa_url)[#work.open_access.oa_url.find(regex("^(https?:\/\/[^\/]+\/)"))]]]
485
+ ],
486
+ [
487
+ #align(center)[_Citation_]
488
+
489
+ - *Count:* #work_report.citation_summary.values().sum()
490
+ - *Type A:* #work_report.citation_summary.type_a_count
491
+ - *Type B:* #work_report.citation_summary.type_b_count
492
+ ]
493
+ )
494
+
495
+ // Cited by Table
496
+ #if work_report.cited_by.len() >= 1 [
497
+ #align(center, text(11pt)[_Cited by_])
498
+
499
+ #table(
500
+ columns: (auto, 3fr, 0.8fr, auto, auto, auto, auto),
501
+ inset: 8pt,
502
+ align: horizon,
503
+ // Headers
504
+ [], [*Title*], [*Type*], [*DOI*], [*Cite Type*], [*Publication Date*], [*Cited by count*],
505
+
506
+ // Content
507
+ ..work_report.cited_by.enumerate(start: 1).map(
508
+ ((idx, cited_by)) => (
509
+ table.cell([#idx]),
510
+ table.cell([#cited_by.work.title]),
511
+ table.cell([#cited_by.work.type]),
512
+ table.cell([#if cited_by.work.ids.doi != none [#underline[#link(cited_by.work.ids.doi)[DOI]]] else [#align(center)[-]]]),
513
+ table.cell([#if cited_by.citation_type == 0 [#text(rgb(SUCCESS))[Type A]] else [#text(rgb(ERROR))[Type B]]]),
514
+ table.cell([#cited_by.work.publication_date]),
515
+ table.cell([#cited_by.work.cited_by_count]),
516
+ )
517
+ ).flatten()
518
+ )
519
+ ]
520
+
521
+ // Sources Table
522
+ #if work.locations.len() >= 1 [
523
+ #align(center, text(11pt)[_Sources_])
524
+ #table(
525
+ columns: (auto, 3fr, 2.5fr, 1fr, auto, auto, 1.2fr, auto),
526
+ inset: 8pt,
527
+ align: horizon,
528
+ // Headers
529
+ [], [*Name*], [*Publisher or institution*], [*Type*], [*ISSN-L*], [*Is OA*], [*License*], [*Version*],
530
+
531
+ // Content
532
+ ..work.locations.enumerate(start: 1).filter((location => location.last().source != none)).map(
533
+ ((idx, location)) => (
534
+ table.cell([#underline[#link(label("source_" + location.source.id.find(regex("S\d+$"))))[#idx]]]),
535
+ table.cell([#location.source.display_name]),
536
+ table.cell([#if location.source.host_organization_name != none [#location.source.host_organization_name] else [-]]),
537
+ table.cell([#location.source.type]),
538
+ table.cell([#if location.source.issn_l != none [#location.source.issn_l] else [-]]),
539
+ table.cell([#if location.is_oa [#text(rgb(SUCCESS))[True]] else [#text(rgb(ERROR))[False]]]),
540
+ table.cell([#if location.license != none [#location.license] else [-]]),
541
+ table.cell([#if location.version != none [#work_driven_version.at(location.version)]]),
542
+ )
543
+ ).flatten(),
544
+ ..work.locations.enumerate(start: 1).filter((location => location.last().source == none)).map(
545
+ ((idx, location)) => (
546
+ table.cell([#idx]),
547
+ table.cell([#underline([#link(location.landing_page_url)[#location.landing_page_url]])]),
548
+ table.cell([-]),
549
+ table.cell([-]),
550
+ table.cell([-]),
551
+ table.cell([#if location.is_oa [#text(rgb(SUCCESS))[True]] else [#text(rgb(ERROR))[False]]]),
552
+ table.cell([#if location.license != none [#location.license] else [-]]),
553
+ table.cell([#if location.version != none [#work_driven_version.at(location.version)]]),
554
+ )
555
+ ).flatten()
556
+ )
557
+ ]
558
+ ]
559
+
560
+
561
+ // Sources
562
+ #pagebreak()
563
+ = Sources.
564
+
565
+ #table(
566
+ columns: (auto, 2.7fr, 2.56fr, 1.2fr, auto, auto, auto, auto),
567
+ inset: 8pt,
568
+ align: horizon,
569
+ // Headers
570
+ [], [*Name*], [*Publisher or institution*], [*Type*], [*ISSN-L*], [*Impact factor*], [*h-index*], [*Is OA*],
571
+
572
+ // Content
573
+ ..sources_summary.sources.enumerate(start: 1).map(
574
+ ((idx, source)) => (
575
+ table.cell([3.#idx. #label("source_" + source.id.find(regex("S\d+$")))]),
576
+ table.cell([#if source.homepage_url != none [#underline[#link(source.homepage_url)[#source.display_name]]] else [#underline[#link(source.id)[#source.display_name]]]]),
577
+ table.cell([#if source.host_organization_name != none [#source.host_organization_name] else [-]]),
578
+ table.cell([#if source.type != none [#source.type] else [-]]),
579
+ table.cell([#if source.issn_l != none [#source.issn_l] else [-]]),
580
+ table.cell([#calc.round(source.summary_stats.at("2yr_mean_citedness"), digits: 3)]),
581
+ table.cell([#source.summary_stats.h_index]),
582
+ table.cell([#if source.is_oa [#text(rgb(SUCCESS))[True]] else [#text(rgb(ERROR))[False]]]),
583
+ )
584
+ ).flatten()
585
+ )
586
+
587
+
588
+ #pagebreak()
589
+ = Bibliography
590
+
591
+ Priem, J., Piwowar, H., & Orr, R. (2022). OpenAlex: A fully-open index of scholarly works, authors, venues, institutions, and concepts. ArXiv. https://arxiv.org/abs/2205.01833
pub_analyzer/main.py ADDED
@@ -0,0 +1,81 @@
1
+ """Entry Point."""
2
+
3
+ import urllib.parse
4
+ import webbrowser
5
+ from typing import ClassVar
6
+
7
+ from textual import log
8
+ from textual._path import CSSPathType
9
+ from textual.app import App, ComposeResult
10
+ from textual.binding import Binding, BindingType
11
+ from textual.dom import DOMNode
12
+ from textual.reactive import Reactive
13
+ from textual.widgets import Footer
14
+
15
+ from pub_analyzer.widgets.body import Body
16
+ from pub_analyzer.widgets.sidebar import SideBar
17
+
18
+
19
+ class PubAnalyzerApp(App[DOMNode]):
20
+ """Pub Analyzer App entrypoint."""
21
+
22
+ TITLE = "Pub Analyzer"
23
+ ENABLE_COMMAND_PALETTE = False
24
+
25
+ CSS_PATH: ClassVar[CSSPathType | None] = [
26
+ "css/body.tcss",
27
+ "css/buttons.tcss",
28
+ "css/checkbox.tcss",
29
+ "css/collapsible.tcss",
30
+ "css/datatable.tcss",
31
+ "css/editor.tcss",
32
+ "css/main.tcss",
33
+ "css/report.tcss",
34
+ "css/search.tcss",
35
+ "css/summary.tcss",
36
+ "css/tabs.tcss",
37
+ "css/tree.tcss",
38
+ ]
39
+ BINDINGS: ClassVar[list[BindingType]] = [
40
+ Binding(key="ctrl+d", action="toggle_dark", description="Dark mode"),
41
+ Binding(key="ctrl+s", action="toggle_sidebar", description="Sidebar"),
42
+ ]
43
+
44
+ dark: Reactive[bool] = Reactive(False)
45
+
46
+ def compose(self) -> ComposeResult:
47
+ """Create child widgets for the app."""
48
+ yield Body()
49
+ yield Footer(show_command_palette=False)
50
+
51
+ def action_toggle_dark(self) -> None:
52
+ """Toggle dark mode."""
53
+ self.dark = not self.dark
54
+
55
+ def action_toggle_sidebar(self) -> None:
56
+ """Toggle sidebar."""
57
+ self.set_focus(None)
58
+
59
+ sidebar = self.query_one(SideBar)
60
+ sidebar.toggle()
61
+
62
+ def action_save_screenshot(self) -> None:
63
+ """Take Screenshot."""
64
+ file_path = self.app.save_screenshot()
65
+ self.app.notify(
66
+ title="Screenshot saved!", message=f"You can see the screenshot at {file_path}", severity="information", timeout=10.0
67
+ )
68
+
69
+ def action_open_link(self, link: str) -> None:
70
+ """Open a link in the browser."""
71
+ log.info(f"Opening link: {link}")
72
+ if link and (link != "None"):
73
+ webbrowser.open(urllib.parse.unquote(link))
74
+ else:
75
+ log.warning("Link cannot be empty!")
76
+
77
+
78
+ def run() -> None:
79
+ """Run Pub Analyzer App."""
80
+ app = PubAnalyzerApp()
81
+ app.run()
@@ -0,0 +1 @@
1
+ """Scholarly entities models definitions from OpenAlex."""