charting.scm 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990
  1. ;; guile-charting
  2. ;; Copyright (C) 2007, 2012, 2014, 2019 Andy Wingo <wingo at pobox dot com>
  3. ;; This library is free software; you can redistribute it and/or
  4. ;; modify it under the terms of the GNU Lesser General Public
  5. ;; License as published by the Free Software Foundation; either
  6. ;; version 3 of the License, or (at your option) any later version.
  7. ;;
  8. ;; This library is distributed in the hope that it will be useful,
  9. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  11. ;; Lesser General Public License for more details.
  12. ;;
  13. ;; You should have received a copy of the GNU Lesser General Public
  14. ;; License along with this library; if not, see
  15. ;; <http://www.gnu.org/licenses/>.
  16. ;;; Commentary:
  17. ;;
  18. ;;
  19. ;;; Code:
  20. (define-module (charting)
  21. #:use-module (cairo)
  22. #:use-module (charting util)
  23. #:use-module (charting draw)
  24. #:use-module (ice-9 format)
  25. #:use-module (ice-9 match)
  26. #:use-module ((srfi srfi-1) #:select (lset-adjoin))
  27. #:export (make-bar-chart
  28. make-chart
  29. make-performance-chart
  30. make-bar-chart/histograms
  31. make-performance-series
  32. make-scatter-plot
  33. make-page-map))
  34. (define-syntax-rule (with-move-to cr x y body ...)
  35. (begin
  36. (cairo-move-to cr x y)
  37. body ...))
  38. (define (fold proc seed list)
  39. (if (null? list)
  40. seed
  41. (fold proc (proc (car list) seed) (cdr list))))
  42. (define-with-kwargs (text-measurer (family "Bitstream Vera Sans")
  43. (size 10))
  44. (lambda (text)
  45. (if text
  46. (let ((cr (cairo-create (cairo-image-surface-create 'argb32 100 100))))
  47. (cairo-select-font-face cr family 'normal 'normal)
  48. (cairo-set-font-size cr size)
  49. (inexact->exact
  50. (round (cairo-text-extents:width (cairo-text-extents cr text)))))
  51. 0)))
  52. (define-with-kwargs (make-chart title
  53. (chart-height 360)
  54. (chart-width 360)
  55. (font-family "Bitstream Vera Sans")
  56. (line-width 1)
  57. (title-text-height 16)
  58. (axis-text-height 12)
  59. x-axis-label
  60. y-axis-label
  61. (tick-size 5)
  62. y-axis-ticks
  63. x-axis-ticks
  64. y-axis-tick-labels
  65. x-axis-tick-labels
  66. (vertical-x-axis-tick-labels? #f)
  67. (x-axis-tick-mode 'none)
  68. (y-axis-tick-mode 'grid)
  69. (chart-margin 5)
  70. (margin 5)
  71. (padding-left 0)
  72. (padding-right 0)
  73. (padding-top 0)
  74. (padding-bottom 0)
  75. (make-surface
  76. (lambda (x y)
  77. (cairo-image-surface-create
  78. 'argb32
  79. (inexact->exact (ceiling x))
  80. (inexact->exact (ceiling y))))))
  81. "Make a chart.
  82. @var{tick-lables} is an alist of label-value pairs, where the
  83. value is given in chart height coordinates. The label can be #f.
  84. This function makes the basic chart, setting up the basics like
  85. the title, axes, etc. You probably don't want to call this unless
  86. you are making a custom chart type.
  87. This function returns a cairo context whose coordinate system has
  88. been flipped so that the origin of the chart is (0, 0), with
  89. positive in the northeast quadrant.
  90. "
  91. (define measure-text
  92. (text-measurer font-family axis-text-height))
  93. (let* ((x-axis-text-length (if x-axis-tick-labels
  94. (apply max (map measure-text
  95. (map car x-axis-tick-labels)))
  96. 0))
  97. (y-axis-text-length (if y-axis-tick-labels
  98. (apply max (map measure-text
  99. (map car y-axis-tick-labels)))
  100. 0))
  101. (total-width (+ chart-width (if y-axis-ticks tick-size 0)
  102. (if y-axis-label axis-text-height 0)
  103. (if (zero? y-axis-text-length) 0
  104. (+ y-axis-text-length chart-margin))
  105. chart-margin chart-margin margin margin
  106. padding-left padding-right))
  107. (total-height (+ chart-height (if title title-text-height 0)
  108. (if x-axis-ticks tick-size 0)
  109. (if x-axis-tick-labels
  110. (if vertical-x-axis-tick-labels?
  111. x-axis-text-length
  112. axis-text-height)
  113. 0)
  114. (if x-axis-label axis-text-height 0)
  115. chart-margin chart-margin margin margin
  116. padding-top padding-bottom))
  117. (surface (make-surface total-width total-height))
  118. (cr (cairo-create surface)))
  119. ;; Move to cartesian coordinates centered at graph origin.
  120. (cairo-translate cr (+ margin chart-margin padding-left
  121. (if y-axis-ticks tick-size 0)
  122. y-axis-text-length
  123. (if y-axis-label axis-text-height 0))
  124. (+ margin chart-margin padding-top
  125. (if title title-text-height 0) chart-height))
  126. (cairo-scale cr 1.0 -1.0)
  127. (cairo-set-line-width cr line-width)
  128. (cairo-select-font-face cr font-family 'normal 'normal)
  129. (draw-background cr)
  130. (with-move-to
  131. cr 0 0
  132. (draw-chart-area cr chart-width chart-height))
  133. (with-move-to
  134. cr (/ chart-width 2) (+ chart-height chart-margin)
  135. (draw-title cr title title-text-height))
  136. (when x-axis-ticks
  137. (case x-axis-tick-mode
  138. ((grid)
  139. (with-move-to
  140. cr 0 0
  141. (draw-grid cr x-axis-ticks chart-height #f)))
  142. ((none) #t)
  143. ((ticks)
  144. (with-move-to
  145. cr 0 0
  146. (draw-ticks cr x-axis-ticks tick-size #f)))
  147. (else (error "unknown tick mode" x-axis-tick-mode))))
  148. (when y-axis-ticks
  149. (case y-axis-tick-mode
  150. ((grid)
  151. (with-move-to
  152. cr 0 0
  153. (draw-grid cr y-axis-ticks chart-width #t)))
  154. ((none) #t)
  155. ((ticks)
  156. (with-move-to
  157. cr 0 0
  158. (draw-ticks cr y-axis-ticks tick-size #t)))
  159. (else (error "unknown tick mode" y-axis-tick-mode))))
  160. (when x-axis-tick-labels
  161. (with-move-to
  162. cr 0 0
  163. (draw-tick-labels cr x-axis-tick-labels tick-size #f
  164. vertical-x-axis-tick-labels? axis-text-height)))
  165. (when y-axis-tick-labels
  166. (with-move-to
  167. cr 0 0
  168. (draw-tick-labels cr y-axis-tick-labels tick-size #t #f
  169. axis-text-height)))
  170. (when x-axis-label
  171. (with-move-to
  172. cr 0 (- (+ axis-text-height
  173. (if vertical-x-axis-tick-labels?
  174. x-axis-text-length
  175. tick-size)))
  176. (draw-axis-label cr x-axis-label axis-text-height chart-width #f)))
  177. (when y-axis-label
  178. (with-move-to
  179. cr
  180. (- 0 (if (zero? y-axis-text-length) 0
  181. (+ y-axis-text-length chart-margin))
  182. tick-size)
  183. 0
  184. (draw-axis-label cr y-axis-label axis-text-height chart-height #t)))
  185. cr))
  186. (define (make-uniform-ticks min max step)
  187. (let lp ((pos min) (out '()))
  188. (if (> pos max)
  189. (reverse! out)
  190. (lp (+ pos step)
  191. (cons pos out)))))
  192. (define* (make-sensible-ticks min max #:optional log-base (max-count 20))
  193. (cond
  194. (log-base
  195. (let ((logn (lambda (n) (/ (log n) (log log-base))))
  196. (exptn (lambda (n) (expt log-base n))))
  197. (let lp ((x (ceiling (logn min))))
  198. (if (< x (logn max))
  199. (cons (exptn x) (lp (1+ x)))
  200. '()))))
  201. (else
  202. (let ((range (- max min)))
  203. (let lp ((step (/ (expt 10 (ceiling (log10 range))) 100)))
  204. (if (> (/ range step) max-count)
  205. (lp (* step 2))
  206. (make-uniform-ticks (* step (ceiling/ min step)) max step)))))))
  207. (define* (default-formatter value #:optional (precision 3))
  208. (cond
  209. ((and (exact? value) (integer? value))
  210. (number->string value))
  211. ((zero? value)
  212. "0")
  213. (else
  214. (let ((order (inexact->exact (floor (log10 (abs value))))))
  215. (cond
  216. ((>= order precision)
  217. ;; integer
  218. (number->string (inexact->exact (round value))))
  219. ((>= order -1)
  220. ;; decimal
  221. (format #f "~,vf" (- precision order) value))
  222. (else
  223. ;; scientific
  224. (format #f "~,v,,,,,'ee" (1- precision) value)))))))
  225. (define-with-kwargs (make-bar-chart title
  226. data
  227. write-to-png
  228. (bar-width 40)
  229. (group-spacing 40)
  230. (chart-height 360)
  231. (max-y #f)
  232. (chart-params '())
  233. (legend-params '())
  234. (ytick-label-formatter default-formatter)
  235. (bar-value-formatter #f))
  236. "Make a bar chart.
  237. The format of @var{data} is defined as follows:
  238. @table @var
  239. @item data
  240. (@var{group}+)
  241. @item group
  242. (@var{group-label} @var{bar}+)
  243. @item group-label
  244. A string, to be written to the X axis.
  245. @item bar
  246. (@var{height} @var{bar-params}?)
  247. @item height
  248. The bar height, as a number.
  249. @item bar-params
  250. A property list suitable to passing to
  251. @ref{charting draw draw-bar}.
  252. @end table
  253. This function returns the cairo surface. By default, make-chart
  254. will create an image surface, but you may override this by
  255. passing a @code{#:make-surface} function in the
  256. @var{chart-params}. In this way you can render charts to any
  257. surface supported by Cairo, e.g. PS, PDF, SVG, GDK, etc.
  258. The #:write-to-png option will write the chart out to the PNG file
  259. that you name.
  260. An example invocation might look like:
  261. @example
  262. (make-bar-chart \"Average Height at Iihenda JSS\"
  263. '((\"Grade 9\" (150 \"Boys\") (140 \"Girls\"))
  264. (\"Grade 10\" (150 \"Boys\")
  265. (140 \"Girls\" (#:y+-bracket 5 #:y--bracket 4.5))))
  266. #:write-to-png \"/tmp/graph.png\")
  267. @end example"
  268. (let* ((num-groups (length data))
  269. (max-group-bars (1- (apply max (map length data))))
  270. (chart-width (* num-groups (+ (* max-group-bars bar-width)
  271. group-spacing)))
  272. (max-height (or max-y
  273. (* (apply max (map (lambda (x)
  274. (apply max (map car (cdr x))))
  275. data))
  276. 5/4)))
  277. (height-scale (/ chart-height max-height))
  278. (yticks-unscaled (make-uniform-ticks
  279. 0 max-height
  280. ((lambda (x) (if (<= (/ max-height x) 5)
  281. (/ x 2)
  282. x))
  283. (expt 10 (1- (round
  284. (log10 max-height)))))))
  285. (yticks (map (lambda (x) (* x height-scale)) yticks-unscaled))
  286. (ytick-labels (map (lambda (pos) (cons (ytick-label-formatter pos)
  287. (* pos height-scale)))
  288. yticks-unscaled))
  289. (xticks (make-uniform-ticks
  290. 0 chart-width
  291. (+ (* max-group-bars bar-width) group-spacing)))
  292. (xtick-labels (let ((step (+ (* max-group-bars bar-width)
  293. group-spacing)))
  294. (map cons
  295. (map car data)
  296. (make-uniform-ticks
  297. (/ step 2) chart-width step))))
  298. (series-list (reverse!
  299. (fold (lambda (group series-list)
  300. (fold
  301. (lambda (bar series-list)
  302. (if (not (member (cadr bar) series-list))
  303. (cons (cadr bar) series-list)
  304. series-list))
  305. series-list
  306. (cdr group)))
  307. '()
  308. data)))
  309. (cr (apply make-chart title chart-height chart-width
  310. #:y-axis-ticks yticks #:y-axis-tick-labels ytick-labels
  311. #:x-axis-ticks xticks #:x-axis-tick-labels xtick-labels
  312. chart-params)))
  313. (for-each
  314. (lambda (group pos)
  315. (with-move-to
  316. cr pos 0
  317. (draw-bar-group cr (cdr group) bar-width height-scale
  318. bar-value-formatter)))
  319. data
  320. (map cdr xtick-labels))
  321. (with-move-to
  322. cr 5 (- chart-height 5)
  323. (apply draw-legend cr #t #t #:series-list series-list legend-params))
  324. (if write-to-png
  325. (cairo-surface-write-to-png (cairo-get-target cr) write-to-png))
  326. (cairo-get-target cr)))
  327. (define (tests-minmax f tests)
  328. (apply f (map (lambda (points) (apply f points))
  329. (map cdr tests))))
  330. (define (scenarios-minmax f scenarios)
  331. (apply f (map (lambda (tests) (tests-minmax f tests))
  332. (map cdr scenarios))))
  333. (define* (make-performance-chart title
  334. data
  335. #:key
  336. write-to-png
  337. (box-width 20)
  338. (box-spacing 8)
  339. (test-spacing 24)
  340. chart-height
  341. (baseline #f)
  342. (log-y-base #f)
  343. (min-y
  344. (if log-y-base
  345. (/ (scenarios-minmax min data)
  346. log-y-base)
  347. 0))
  348. (max-y
  349. (if log-y-base
  350. (* (scenarios-minmax max data)
  351. log-y-base)
  352. (+ min-y
  353. (* 7/6 (- (scenarios-minmax max data)
  354. min-y)))))
  355. (axis-text-height 12)
  356. (chart-params '())
  357. (legend-params '())
  358. (y-axis-label "Benchmark score")
  359. (vertical-xtick-labels? #f)
  360. (vertical-box-labels? #f)
  361. (ytick-label-formatter default-formatter)
  362. (box-value-formatter default-formatter)
  363. (box-label-height 10))
  364. "Make a performance chart.
  365. A performance chart compares runtimes for some set of tests across some
  366. set of scenarios.
  367. The format of @var{data} is defined as follows:
  368. @example
  369. ((@var{scenario} (@var{test} @var{data-point} ...) ...) ...)
  370. @end example
  371. @var{scenario} and @var{test} should be strings.
  372. @var{data-point} should be numbers.
  373. The resulting plot will have time on the Y axis, and one X axis entry
  374. for each test. Each test/scenario data set will be represented as a box
  375. plot. In the future we should add more options (for example, a small
  376. vertical histogram on the plot).
  377. This function returns the cairo surface. By default, make-chart
  378. will create an image surface, but you may override this by
  379. passing a @code{#:make-surface} function in the
  380. @var{chart-params}. In this way you can render charts to any
  381. surface supported by Cairo, e.g. PS, PDF, SVG, GDK, etc.
  382. The #:write-to-png option will write the chart out to the PNG file
  383. that you name.
  384. An example invocation might look like:
  385. @example
  386. (make-performance-chart
  387. \"Gabriel Benchmarks\"
  388. '((\"guile-1.8\"
  389. (\"tak\" 0.12 0.13 0.17)
  390. (\"fib\" 1.13 1.24 1.05))
  391. (\"guile-2.0\"
  392. (\"tak\" 0.05 0.051 0.047)
  393. (\"fib\" 0.64 0.59 0.71)))
  394. #:write-to-png \"/tmp/graph.png\")
  395. @end example"
  396. (let* ((test-folder (lambda (f)
  397. (lambda (scenario seed)
  398. (fold f seed (cdr scenario)))))
  399. (datum-folder (lambda (f)
  400. (lambda (test seed)
  401. (fold f seed (cdr test)))))
  402. (num-scenarios (length data))
  403. (tests (reverse
  404. (fold (test-folder
  405. (lambda (test tests)
  406. (lset-adjoin equal? tests (car test))))
  407. '() data)))
  408. (num-tests (length tests))
  409. (max-x-label-length
  410. (let ((measurer (text-measurer #:size axis-text-height)))
  411. (fold (lambda (scenario seed)
  412. (fold (lambda (series seed)
  413. (max (measurer (car series)) seed))
  414. seed
  415. (cdr scenario)))
  416. 0
  417. data)))
  418. (test-width (max (if vertical-xtick-labels?
  419. axis-text-height
  420. max-x-label-length)
  421. (+ (* num-scenarios box-width)
  422. (* (1- num-scenarios) box-spacing))))
  423. (test-step (+ test-width test-spacing))
  424. (chart-width (max (* num-tests test-step) 400))
  425. (x-scale (/ (/ chart-width num-tests) test-step))
  426. (test-step (* test-step x-scale))
  427. (test-width (* test-width x-scale))
  428. (test-spacing (* test-spacing x-scale))
  429. (box-width (* box-width x-scale))
  430. (box-spacing (* box-spacing x-scale))
  431. (chart-height (or chart-height
  432. (round/ (* chart-width 3) 4)))
  433. (height (- max-y min-y))
  434. (height-scale (/ chart-height height))
  435. (translate-y (if log-y-base
  436. (lambda (y)
  437. (* (/ (log (/ y min-y)) (log (/ max-y min-y)))
  438. chart-height))
  439. (lambda (y)
  440. (* (/ (- y min-y) height) chart-height))))
  441. (yticks-unscaled (make-sensible-ticks min-y max-y log-y-base))
  442. (yticks (map translate-y yticks-unscaled))
  443. (ytick-labels (map (lambda (y pos)
  444. (cons (ytick-label-formatter y) pos))
  445. yticks-unscaled yticks))
  446. (xticks (make-uniform-ticks 0 chart-width test-step))
  447. (xtick-labels (map cons
  448. tests
  449. (make-uniform-ticks
  450. (/ test-step 2) chart-width test-step)))
  451. (legend-dimensions
  452. (call-with-values (lambda ()
  453. (apply draw-legend #f #:measure-only? #t
  454. #:series-list (map car data)
  455. #:text-measurer
  456. (lambda (size)
  457. (text-measurer #:size size))
  458. legend-params))
  459. cons))
  460. (cr (apply make-chart title chart-height chart-width
  461. #:axis-text-height axis-text-height
  462. #:y-axis-ticks yticks #:y-axis-tick-labels ytick-labels
  463. #:x-axis-ticks xticks #:x-axis-tick-labels xtick-labels
  464. #:y-axis-label y-axis-label
  465. #:x-axis-label ""
  466. #:padding-right (car legend-dimensions)
  467. #:vertical-x-axis-tick-labels? vertical-xtick-labels?
  468. chart-params)))
  469. (for-each
  470. (lambda (test pos)
  471. (with-move-to
  472. cr pos 0
  473. (draw-perf-test cr
  474. (map (lambda (scenario)
  475. (cons (car scenario)
  476. (or (assoc-ref (cdr scenario) test)
  477. '())))
  478. data)
  479. box-width box-spacing
  480. translate-y box-value-formatter
  481. box-label-height vertical-box-labels?
  482. baseline)))
  483. tests
  484. (map cdr xtick-labels))
  485. (with-move-to
  486. cr (+ chart-width 5) (- chart-height 5)
  487. (apply draw-legend cr #t #t #:series-list (map car data)
  488. #:draw-outlines? #f #:draw-background? #f
  489. legend-params))
  490. (if write-to-png
  491. (cairo-surface-write-to-png (cairo-get-target cr) write-to-png))
  492. (cairo-get-target cr)))
  493. (define* (make-bar-chart/histograms title
  494. data
  495. #:key
  496. write-to-png
  497. (box-width 20)
  498. (box-spacing 12)
  499. (scenario-spacing 24)
  500. chart-height
  501. (baseline #f)
  502. (log-y-base #f)
  503. (min-y
  504. (if log-y-base
  505. (/ (tests-minmax min data)
  506. log-y-base)
  507. 0))
  508. (max-y
  509. (if log-y-base
  510. (* (tests-minmax max data)
  511. log-y-base)
  512. (+ min-y
  513. (* 7/6 (- (tests-minmax max data)
  514. min-y)))))
  515. (axis-text-height 12)
  516. (y-axis-label "Benchmark score")
  517. (chart-params '())
  518. (legend-params '())
  519. (ytick-label-formatter default-formatter)
  520. (box-value-formatter default-formatter)
  521. (box-label-height 10)
  522. (vertical-box-labels? #f))
  523. "Make a bar chart, with overlaid histograms on the bars.
  524. A performance chart compares runtimes for a test across some set of
  525. scenarios.
  526. The format of @var{data} is defined as follows:
  527. @example
  528. ((@var{scenario} @var{data-point} ...) ...)
  529. @end example
  530. @var{scenario} should be strings. @var{data-point} should be numbers.
  531. The resulting plot will have the data points on the Y axis, and one
  532. bar+histogram for each scenario.
  533. This function returns the cairo surface. By default, make-chart will
  534. create an image surface, but you may override this by passing a
  535. @code{#:make-surface} function in the @var{chart-params}. In this way
  536. you can render charts to any surface supported by Cairo, e.g. PS, PDF,
  537. SVG, GDK, etc.
  538. The #:write-to-png option will write the chart out to the PNG file
  539. that you name.
  540. An example invocation might look like:
  541. @example
  542. (make-bar-chart/histograms
  543. \"Fibonacci benchmark\"
  544. '((\"guile-1.8\" 1.13 1.24 1.05)
  545. (\"guile-2.0\" 0.64 0.59 0.71))
  546. #:write-to-png \"/tmp/graph.png\")
  547. @end example"
  548. (let* ((datum-folder (lambda (f)
  549. (lambda (scenario seed)
  550. (match scenario
  551. ((name . datums)
  552. (fold f seed datums))))))
  553. (num-scenarios (length data))
  554. (scenario-names (match data (((name . _) ...) name)))
  555. (scenario-width (let ((measurer (text-measurer #:size axis-text-height)))
  556. (+ box-spacing
  557. (fold (lambda (name width)
  558. (max (measurer name) width))
  559. box-width
  560. scenario-names))))
  561. (natural-width (+ (* num-scenarios scenario-width) scenario-spacing))
  562. (chart-width (max natural-width 400))
  563. (x-scale (/ chart-width natural-width 1.0))
  564. (scenario-width (* scenario-width x-scale))
  565. (scenario-spacing (* scenario-spacing x-scale))
  566. (box-width (* (/ box-width (+ box-width box-spacing))
  567. scenario-width))
  568. (box-spacing (* (/ box-spacing (+ box-width box-spacing))
  569. scenario-spacing))
  570. (box-spacing (* box-spacing x-scale))
  571. (chart-height (or chart-height
  572. (round/ (* chart-width 3) 4)))
  573. (height (- max-y min-y))
  574. (height-scale (/ chart-height height))
  575. (translate-y (if log-y-base
  576. (lambda (y)
  577. (* (/ (log (/ y min-y)) (log (/ max-y min-y)))
  578. chart-height))
  579. (lambda (y)
  580. (* (/ (- y min-y) height) chart-height))))
  581. (yticks-unscaled (make-sensible-ticks min-y max-y log-y-base))
  582. (yticks (map translate-y yticks-unscaled))
  583. (ytick-labels (map (lambda (y pos)
  584. (cons (ytick-label-formatter y) pos))
  585. yticks-unscaled yticks))
  586. (xticks (make-uniform-ticks (/ scenario-spacing 2.)
  587. chart-width scenario-width))
  588. (xtick-labels (map cons
  589. scenario-names
  590. (make-uniform-ticks
  591. (/ (+ scenario-spacing scenario-width) 2)
  592. chart-width scenario-width)))
  593. (cr (apply make-chart title chart-height chart-width
  594. #:axis-text-height axis-text-height
  595. #:y-axis-ticks yticks #:y-axis-tick-labels ytick-labels
  596. #:x-axis-ticks xticks #:x-axis-tick-labels xtick-labels
  597. #:y-axis-label y-axis-label
  598. chart-params)))
  599. (for-each
  600. (lambda (scenario pos)
  601. (with-move-to
  602. cr pos 0
  603. (draw-perf-test cr
  604. (list scenario)
  605. box-width box-spacing
  606. translate-y
  607. box-value-formatter
  608. box-label-height
  609. vertical-box-labels?
  610. baseline)))
  611. data
  612. (map cdr xtick-labels))
  613. (if write-to-png
  614. (cairo-surface-write-to-png (cairo-get-target cr) write-to-png))
  615. (cairo-get-target cr)))
  616. (define-with-kwargs (make-performance-series title
  617. data
  618. write-to-png
  619. (box-width 20)
  620. (box-spacing 4)
  621. (test-spacing 12)
  622. chart-height
  623. (max-y #f)
  624. (min-y 0)
  625. (chart-params '())
  626. (annotations '())
  627. (ytick-label-formatter default-formatter)
  628. (box-value-formatter default-formatter))
  629. "Make a performance chart.
  630. A performance chart compares runtimes for some set of tests across some
  631. set of scenarios.
  632. The format of @var{data} is defined as follows:
  633. @example
  634. ((@var{x} @var{data-point} ...) ...)
  635. @end example
  636. @var{x} and @var{data-point} should be numbers.
  637. The resulting plot will have time on the Y axis, and one X axis entry
  638. for each test. Each data set will be represented as a box
  639. plot. In the future we should add more options (for example, a small
  640. vertical histogram on the plot).
  641. This function returns the cairo surface. By default, make-chart
  642. will create an image surface, but you may override this by
  643. passing a @code{#:make-surface} function in the
  644. @var{chart-params}. In this way you can render charts to any
  645. surface supported by Cairo, e.g. PS, PDF, SVG, GDK, etc.
  646. The #:write-to-png option will write the chart out to the PNG file
  647. that you name.
  648. An example invocation might look like:
  649. @example
  650. (make-performance-chart
  651. \"Gabriel Benchmarks\"
  652. '((\"guile-1.8\"
  653. (\"tak\" 0.12 0.13 0.17)
  654. (\"fib\" 1.13 1.24 1.05))
  655. (\"guile-2.0\"
  656. (\"tak\" 0.05 0.051 0.047)
  657. (\"fib\" 0.64 0.59 0.71)))
  658. #:write-to-png \"/tmp/graph.png\")
  659. @end example"
  660. (let* ((datum-folder (lambda (f)
  661. (lambda (test seed)
  662. (fold f seed (cdr test)))))
  663. (num-tests (length data))
  664. (test-width (+ (* num-tests box-width)
  665. (* (1- num-tests) box-spacing)))
  666. (test-step (+ test-width test-spacing))
  667. (chart-width (min (* num-tests test-step) 800))
  668. (x-scale (/ (/ chart-width num-tests) test-step))
  669. (test-step (* test-step x-scale))
  670. (test-width (* test-width x-scale))
  671. (test-spacing (* test-spacing x-scale))
  672. (box-width (* box-width x-scale))
  673. (box-spacing (* box-spacing x-scale))
  674. (chart-height (or chart-height
  675. (round/ (* chart-width 3) 7)))
  676. (max-y* (fold (datum-folder max) 0 data))
  677. (min-y* (fold (datum-folder min) 0 data))
  678. (max-y (or max-y
  679. (+ max-y* (* 1/6 (- max-y* min-y*)))))
  680. (min-y (or min-y
  681. (max (- min-y* (* 1/6 (- max-y* min-y*))) 0)))
  682. (height (- max-y min-y))
  683. (height-scale (/ chart-height height))
  684. (yticks-unscaled (make-sensible-ticks min-y max-y))
  685. (yticks (map (lambda (y) (* (- y min-y) height-scale))
  686. yticks-unscaled))
  687. (ytick-labels (map (lambda (y) (cons (ytick-label-formatter y)
  688. (* (- y min-y) height-scale)))
  689. yticks-unscaled))
  690. (xticks (make-uniform-ticks 0 chart-width test-step))
  691. (xtick-labels (map cons
  692. (map car data)
  693. (make-uniform-ticks
  694. (/ test-step 2) chart-width test-step)))
  695. (cr (apply make-chart title chart-height chart-width
  696. #:y-axis-ticks yticks #:y-axis-tick-labels ytick-labels
  697. #:x-axis-ticks xticks #:x-axis-tick-labels xtick-labels
  698. #:y-axis-label "Benchmark score"
  699. #:x-axis-label "Warmup time (ms)"
  700. chart-params)))
  701. (draw-annotations cr annotations xtick-labels chart-width chart-height)
  702. (draw-perf-series cr data (map cdr xtick-labels)
  703. box-width box-spacing
  704. min-y height-scale
  705. box-value-formatter)
  706. (if write-to-png
  707. (cairo-surface-write-to-png (cairo-get-target cr) write-to-png))
  708. (cairo-get-target cr)))
  709. (define-with-kwargs (make-scatter-plot title
  710. data
  711. write-to-png
  712. (test-spacing 24)
  713. (chart-height 300)
  714. (chart-width 400)
  715. (min-x 0)
  716. (max-x #f)
  717. (min-y 0)
  718. (max-y #f)
  719. (log-x-base #f)
  720. (log-y-base #f)
  721. (chart-params '())
  722. (legend-params '())
  723. (x-axis-label "")
  724. (y-axis-label "")
  725. (x-ticks #f)
  726. (y-ticks #f)
  727. (tick-label-formatter default-formatter))
  728. "Make a scatter plot.
  729. A scatter plot shows a number of series as individual points.
  730. The format of @var{data} is defined as follows:
  731. @example
  732. ((@var{series} (@var{x} . @var{y}) ...) ...)
  733. @end example
  734. @var{series} should be a string. @var{x} and @var{y} should be numbers.
  735. This function returns the cairo surface. By default, make-chart
  736. will create an image surface, but you may override this by
  737. passing a @code{#:make-surface} function in the
  738. @var{chart-params}. In this way you can render charts to any
  739. surface supported by Cairo, e.g. PS, PDF, SVG, GDK, etc.
  740. The #:write-to-png option will write the chart out to the PNG file
  741. that you name.
  742. An example invocation might look like:
  743. @example
  744. (make-scatter-plot
  745. \"MPG for cars\"
  746. '((\"ford\" (1 . 2) (2 . 3))
  747. (\"opel\" (1.2 . 3.5) (4.5 . 1)))
  748. #:write-to-png \"/tmp/graph.png\")
  749. @end example"
  750. (let* ((datum-folder (lambda (f cxr)
  751. (lambda (series seed)
  752. (fold (lambda (pair seed)
  753. (f (cxr pair) seed))
  754. seed (cdr series)))))
  755. (num-series (length data))
  756. (series-names (map car data))
  757. (min-x* (fold (datum-folder min car) 0 data))
  758. (max-x* (fold (datum-folder max car) 0 data))
  759. (min-y* (fold (datum-folder min cdr) 0 data))
  760. (max-y* (fold (datum-folder max cdr) 0 data))
  761. (min-y (or min-y (max (- min-y* (* 1/6 (- max-y* min-y*))) 0)))
  762. (max-y (or max-y (+ max-y* (* 1/6 (- max-y* min-y*)))))
  763. (min-x (or min-x (max (- min-x* (* 1/6 (- max-x* min-x*))) 0)))
  764. (max-x (or max-x (+ max-x* (* 1/6 (- max-x* min-x*)))))
  765. (width (- max-x min-x))
  766. (height (- max-y min-y))
  767. (translate-x (if log-x-base
  768. (lambda (x)
  769. (* (/ (log (- x min-x)) (log width)) chart-width))
  770. (lambda (x)
  771. (* (/ (- x min-x) width) chart-width))))
  772. (translate-y (if log-y-base
  773. (lambda (y)
  774. (* (/ (log (- y min-y)) (log height)) chart-height))
  775. (lambda (y)
  776. (* (/ (- y min-y) height) chart-height))))
  777. (width-scale (/ chart-width width))
  778. (height-scale (/ chart-height height))
  779. (xticks-unscaled (or x-ticks
  780. (make-sensible-ticks min-x max-x log-x-base)))
  781. (xticks (map translate-x xticks-unscaled))
  782. (xtick-labels (map (lambda (x pos) (cons (tick-label-formatter x) pos))
  783. xticks-unscaled xticks))
  784. (yticks-unscaled (or y-ticks
  785. (make-sensible-ticks min-y max-y log-y-base)))
  786. (yticks (map translate-y yticks-unscaled))
  787. (ytick-labels (map (lambda (y pos) (cons (tick-label-formatter y) pos))
  788. yticks-unscaled yticks))
  789. (legend-dimensions
  790. (call-with-values (lambda ()
  791. (apply draw-legend #f #:measure-only? #t
  792. #:series-list (map car data)
  793. #:text-measurer
  794. (lambda (size)
  795. (text-measurer #:size size))
  796. legend-params))
  797. cons))
  798. (cr (apply make-chart title chart-height chart-width
  799. #:y-axis-ticks yticks #:y-axis-tick-labels ytick-labels
  800. #:x-axis-ticks xticks #:x-axis-tick-labels xtick-labels
  801. #:x-axis-label x-axis-label
  802. #:y-axis-label y-axis-label
  803. #:padding-right (car legend-dimensions)
  804. #:x-axis-tick-mode 'grid
  805. chart-params)))
  806. (for-each
  807. (match-lambda
  808. ((series (x . y) ...)
  809. (for-each (lambda (x y)
  810. (draw-point cr (translate-x x) (translate-y y) series))
  811. x y)))
  812. data)
  813. (with-move-to
  814. cr (+ chart-width 5) (- chart-height 5)
  815. (apply draw-legend cr #t #t #:series-list (map car data)
  816. #:draw-outlines? #f #:draw-background? #f
  817. legend-params))
  818. (if write-to-png
  819. (cairo-surface-write-to-png (cairo-get-target cr) write-to-png))
  820. (cairo-get-target cr)))
  821. (define-with-kwargs (make-page-map title data write-to-png
  822. (margin 10)
  823. (page-size 4096)
  824. (page-width 512)
  825. (page-height 2)
  826. (page-spacing 1)
  827. (title-text-height 10)
  828. (text-height 10)
  829. (label-bar-spacing 2)
  830. (font-family "Bitstream Vera Sans"))
  831. "Make a page map.
  832. A page map shows the components of a one-dimensional space. Each
  833. component has a label, a start, and a size. The result is a graphical
  834. representation of the space, divided in @var{page-size} strips, along
  835. with a summary list of the different components.
  836. The format of @var{data} is as follows:
  837. @example
  838. ((@var{label} . (@var{start} . @var{size})) ...)}
  839. @end example
  840. @var{label} should be a string. @var{start} and @var{size} should be
  841. numbers.
  842. The #:write-to-png option will write the chart out to the PNG file
  843. that you name.
  844. An example invocation might look like:
  845. @example
  846. (make-page-map
  847. \"foo.so\"
  848. '((\".text\" 1024 65535)
  849. (\".data\" 65536 20)
  850. (\".rodata\" 65556 200))
  851. #:write-to-png \"foo.png\")
  852. @end example"
  853. (call-with-values (lambda ()
  854. (ceiling/ (match data
  855. (((labels . (starts . sizes)) ...)
  856. (apply max (map + starts sizes))))
  857. page-size))
  858. (lambda (pages last-page-empty)
  859. (let* ((chart-width (+ margin page-width margin))
  860. (chart-height (+ (* pages page-height)
  861. (* (- pages 1) page-spacing)))
  862. (total-height (+ margin
  863. (if title (+ title-text-height margin) 0)
  864. chart-height
  865. margin
  866. (* (length data)
  867. (+ text-height label-bar-spacing))
  868. margin))
  869. (total-width (+ margin chart-width margin))
  870. (surface (cairo-image-surface-create 'argb32
  871. total-width total-height))
  872. (cr (cairo-create surface)))
  873. ;; Move to cartesian coordinates centered at graph origin.
  874. (cairo-translate cr margin
  875. (+ margin
  876. (if title (+ title-text-height margin) 0)
  877. chart-height))
  878. (cairo-scale cr 1.0 -1.0)
  879. (cairo-set-line-width cr 1)
  880. (cairo-select-font-face cr font-family 'normal 'normal)
  881. (draw-background cr)
  882. (with-move-to
  883. cr (/ chart-width 2) (+ chart-height margin)
  884. (draw-title cr title title-text-height))
  885. (draw-page-map cr data chart-width chart-height
  886. #:page-size page-size
  887. #:page-height page-height
  888. #:page-spacing page-spacing)
  889. (with-move-to
  890. cr 0 (- margin)
  891. (draw-bar-legend cr data chart-width
  892. #:text-height text-height
  893. #:font-family font-family
  894. #:horizontal-spacing margin
  895. #:vertical-spacing label-bar-spacing))
  896. (when write-to-png
  897. (cairo-surface-write-to-png (cairo-get-target cr) write-to-png))
  898. (cairo-get-target cr)))))