|
9 | 9 | ### libraries like plPlot may be more suitable. However, out-of-the-box chart generation with minimal |
10 | 10 | ### dependencies is very useful to have. |
11 | 11 | ### |
12 | | -### Data is passed to most charts as a "data-frame", which is a table mapping string column names |
13 | | -### to arrays of data points. |
| 12 | +### Data is passed to most charts as a "data-frame", which is a table mapping keyword (or any Janet value) column names |
| 13 | +### to arrays of data points, usually numbers. |
14 | 14 | ### |
15 | 15 | ### Data frame example: |
16 | 16 | ### |
17 | 17 | ### {:timestamp [1 2 3 4 5 6] |
18 | 18 | ### :temperature-1 [75.1 75.2 75.4 75.5 75.5 75.4] |
19 | 19 | ### :temperature-2 [55.1 55.4 55.7 60.0 60.4 60.9]} |
20 | 20 | ### |
21 | | - |
22 | | -### TODO |
| 21 | +### By default, most charts will not do any anti-aliasing with the default styles and fonts. However, anti-aliased TTF |
| 22 | +### fonts are supported in any place where we accept a font, and all charts that benefit from it support super-sample |
| 23 | +### anti-aliasing for chart graphics. |
| 24 | +### |
| 25 | +### Features! |
| 26 | +### |
23 | 27 | ### [x] - horizontal legend should still be able to wrap vertically if too wide. |
24 | 28 | ### [x] - LABEL YOUR AXES! |
25 | 29 | ### [x] - wrap colors, padding, font, etc. into some kind of styling table to pass around consistently |
|
33 | 37 | ### [x] - heat map |
34 | 38 | ### [ ] - error bars on line chart |
35 | 39 | ### [ ] - fill between chart |
| 40 | +### [ ] - attributed text for captions and annotations |
36 | 41 | ### [x] - handle nils in y-columns for sparse data |
37 | 42 | ### [x] - easier custom chart annotations in the metric space (horizontal lines, vertical lines, etc.) |
38 | 43 | ### [ ] - captions and sub-titles |
|
150 | 155 | Use this information for calculating render transform. Should handle non-existant columns." |
151 | 156 | [data x-column y-columns |
152 | 157 | &opt |
153 | | - width height min-spacing |
154 | 158 | override-min-x override-max-x |
155 | 159 | override-min-y override-max-y] |
156 | 160 |
|
|
159 | 163 | (break [override-min-x override-max-x override-min-y override-max-y])) |
160 | 164 |
|
161 | 165 | # Calculate precise bounds for all x and y values |
| 166 | + (def min-x (or (extreme < (filter identity (get data x-column))) 0)) |
| 167 | + (def max-x (or (extreme > (filter identity (get data x-column))) 1)) |
162 | 168 | (var min-y math/inf) |
163 | 169 | (var max-y math/-inf) |
164 | | - (var min-x (or (extreme < (filter identity (get data x-column))) 0)) |
165 | | - (var max-x (or (extreme > (filter identity (get data x-column))) 1)) |
166 | 170 | (each c y-columns |
167 | 171 | (set min-y (min min-y (extreme < (filter identity (get data c [math/inf]))))) |
168 | 172 | (set max-y (max max-y (extreme > (filter identity (get data c [math/-inf])))))) |
|
255 | 259 | (setdyn *stroke-color* g/black) |
256 | 260 | (setdyn *text-color* g/black)) |
257 | 261 |
|
258 | | -(defn color-lerp |
| 262 | +(defn- color-lerp-internal |
259 | 263 | [a b t] |
260 | | - "Linearly interpolate between 2 colors in RGB space. Colors are srgb encoded as 32 bit unsigned integers." |
261 | | - (def [ar ag ab aa] (g/as-srgb a)) |
262 | | - (def [br bg bb ba] (g/as-srgb b)) |
| 264 | + (def [ar ag ab aa] a) |
| 265 | + (def [br bg bb ba] b) |
263 | 266 | (g/srgb |
264 | 267 | (lerp ar br t) |
265 | 268 | (lerp ag bg t) |
266 | 269 | (lerp ab bb t) |
267 | 270 | (lerp aa ba t))) |
268 | 271 |
|
| 272 | +(defn color-lerp |
| 273 | + [a b t] |
| 274 | + "Linearly interpolate between 2 colors in RGB space. Colors are srgb encoded as 32 bit unsigned integers." |
| 275 | + (color-lerp-internal (g/as-srgb a) (g/as-srgb b) t)) |
| 276 | + |
269 | 277 | (defn make-color-map |
270 | 278 | "Create a function that linearly interpolates between colors for colormapping." |
271 | 279 | [& colors] |
272 | 280 | (def n-colors (length colors)) |
273 | 281 | (def n-1-colors (- n-colors 1)) |
| 282 | + (def srgb-colors (map g/as-srgb colors)) |
274 | 283 | (fn :interp |
275 | 284 | [t] |
276 | 285 | (def t :shadow (clamp t 0 1)) |
|
281 | 290 | (def t-at-b (/ b-index n-1-colors)) |
282 | 291 | (def ab-interval (- t-at-b t-at-a)) |
283 | 292 | (def u (clamp (/ (- t t-at-a) ab-interval) 0 1)) |
284 | | - (color-lerp (in colors b-index) (in colors a-index) u))) |
| 293 | + # Sampling should not allocate. |
| 294 | + (color-lerp-internal (in srgb-colors b-index) (in srgb-colors a-index) u))) |
285 | 295 |
|
286 | 296 | (defn invert-color-map |
287 | 297 | "Create an inverted color-map from an existing color map." |
|
515 | 525 | * :x-suffix - if format-x not provided, allows easily adding a string suffix to x axis tick mark labels |
516 | 526 | * :y-suffix - if format-y not provided, allows easily adding a string suffix to y axis tick mark labels |
517 | 527 | * :x-ticks - Allow setting specific tick marks to be used marking the x axis rather than making a guess. |
| 528 | + * :y-ticks - Allow setting specific tick marks to be used marking the y axis rather than making a guess. |
518 | 529 | * :x-minor-ticks - How many minor tick marks, if any, to place between major tick marks on the x axis |
519 | 530 | * :y-minor-ticks - How many minor tick marks, if any, to place between major tick marks on the y axis |
520 | 531 | * :x-labels-vertical - Turn x labels vertical so more can fit on the axis |
521 | 532 | * :min-x-spacing - When guessing x ticks, allow setting a lower limit to the metric spacing between ticks |
522 | 533 | * :min-y-spacing - When guessing y ticks, allow setting a lower limit to the metric spacing between ticks |
523 | 534 | * :tick-length - how many pixels long to make major tick marks (minor tick marks are 1/2 major tick marks) |
| 535 | + * :transpose - Consider the x axis to be the vertical axis instead of the horizontal axis |
524 | 536 |
|
525 | 537 | Returns a 4-tuple [view:gfx2d/Image to-pixel-space:fn to-metric-space:fn outer-canvas:gfx2d/Image] |
526 | 538 |
|
|
535 | 547 | grid format-x format-y |
536 | 548 | x-label y-label tick-length |
537 | 549 | x-suffix x-prefix y-suffix y-prefix |
538 | | - x-ticks x-minor-ticks y-minor-ticks x-labels-vertical] |
| 550 | + x-ticks y-ticks |
| 551 | + x-minor-ticks y-minor-ticks x-labels-vertical transpose] |
| 552 | + |
| 553 | + # Recur with shifted arguments if transpose (before any defaults) |
| 554 | + (when transpose |
| 555 | + (def [view to-pix to-metric outer-canvas] |
| 556 | + (draw-axes |
| 557 | + :canvas canvas :width width :height height |
| 558 | + :x-label y-label :y-label x-label |
| 559 | + :padding padding :inner-padding inner-padding :font font |
| 560 | + :x-min y-min :x-max y-max :y-min x-min :y-max x-max |
| 561 | + :grid grid :format-x format-y :format-y format-x |
| 562 | + :x-suffix y-suffix :x-prefix y-prefix |
| 563 | + :y-suffix x-suffix :y-prefix x-prefix |
| 564 | + :x-ticks y-ticks :y-ticks x-ticks |
| 565 | + :x-minor-ticks y-minor-ticks :y-minor-ticks x-minor-ticks |
| 566 | + :x-labels-vertical x-labels-vertical # TODO - rename it or change behavior? |
| 567 | + :min-x-spacing min-y-spacing :min-y-spacing min-x-spacing |
| 568 | + :tick-length tick-length)) |
| 569 | + (defn to-pix-2 [x y] (to-pix y x)) |
| 570 | + (defn to-metric-2 [x y] (to-metric y x)) |
| 571 | + (break [view to-pix-2 to-metric-2 outer-canvas])) |
539 | 572 |
|
540 | 573 | (def [canvas width height] :shadow (canvas-and-dimensions canvas width height)) |
541 | 574 | (default padding (dyn *padding* default-padding)) |
|
596 | 629 |
|
597 | 630 | # Guess y axis ticks - used to calculate left and right padding |
598 | 631 | (def [yticks yformat y-axis-tick-label-width] |
599 | | - (guess-axis-ticks y-min y-max (- height top-padding bottom-padding) 20 true font y-prefix y-suffix min-y-spacing format-y)) |
| 632 | + (if y-ticks |
| 633 | + (do |
| 634 | + (def fmt (if format-y format-y string)) |
| 635 | + (var maxw 0) |
| 636 | + (each yt y-ticks |
| 637 | + (def [w _h] (text-measure (fmt yt) font 1)) |
| 638 | + (set maxw (max maxw w))) |
| 639 | + [nil nil maxw maxw]) |
| 640 | + (guess-axis-ticks y-min y-max (- height top-padding bottom-padding) 20 true font y-prefix y-suffix min-y-spacing format-y))) |
600 | 641 |
|
601 | 642 | # Calculate left and right padding once y-axis is guessed |
602 | 643 | (def outer-left-padding (+ padding y-axis-tick-label-width (if y-label (+ padding font-height) 0))) |
|
622 | 663 |
|
623 | 664 | # TODO - replace tick mark draw calls to g/plot with g/fill-rect to allow for thicker ticks |
624 | 665 |
|
625 | | - # Draw Y axis |
| 666 | + # Draw vertical axis |
626 | 667 | (assert yticks "unable to generate y ticks. Make your chart bigger?") |
627 | 668 | (each metric-y yticks |
628 | 669 | (def [_ pixel-y] (convert 0 metric-y)) |
|
634 | 675 | (g/plot canvas left-padding rounded-pixel-y (- width right-padding) rounded-pixel-y grid-color stipple-cycle stipple-on) |
635 | 676 | (g/plot canvas (+ tick-trim outer-left-padding) rounded-pixel-y (+ outer-left-padding tick-height) rounded-pixel-y grid-color))) |
636 | 677 |
|
637 | | - # Draw X axis - allow manual override for x tick marks |
| 678 | + # Draw horizontal axis - allow manual override for x tick marks |
638 | 679 | (def [xticks xformat] |
639 | 680 | (if x-ticks [x-ticks (if format-x format-x string)] |
640 | 681 | (guess-axis-ticks x-min x-max (- width left-padding right-padding) 20 x-labels-vertical font x-prefix x-suffix min-x-spacing format-x))) |
|
651 | 692 | (g/plot canvas rounded-pixel-x top-padding rounded-pixel-x (- height bottom-padding) grid-color stipple-cycle stipple-on) |
652 | 693 | (g/plot canvas rounded-pixel-x (- height outer-bottom-padding tick-trim) rounded-pixel-x (- height outer-bottom-padding tick-height) grid-color))) |
653 | 694 |
|
654 | | - # Draw minor x tick marks |
| 695 | + # Draw minor horizontal axis tick marks |
655 | 696 | (when (and x-minor-ticks (< 1 (length xticks))) |
656 | 697 | (def len (length xticks)) |
657 | 698 | (def dx-first (- (in xticks 1) (in xticks 0))) |
|
668 | 709 | :when (and (> x left-padding) (< x (- width right-padding)))] |
669 | 710 | (g/plot canvas x (- height outer-bottom-padding tick-height) x (- height outer-bottom-padding (div tick-height 2)) grid-color))) |
670 | 711 |
|
671 | | - # Draw minor y tick marks |
| 712 | + # Draw minor vertical axis tick marks |
672 | 713 | (when (and y-minor-ticks (< 1 (length yticks))) |
673 | 714 | (def len (length yticks)) |
674 | 715 | (def dy-first (- (in yticks 1) (in yticks 0))) |
|
733 | 774 | * :bar-padding - space between bars in bar-charts |
734 | 775 | * :stroke-thickness - thickness in pixels of the stroke of the graph when :line-type = :stroke |
735 | 776 | * :x-colors - for bar and scatter plots, optionally set per-point/per-bar colors with an function (f x y index) called on each point. |
| 777 | + * :transpose - When transpose is enabled, draw bar and area charts from the y-axis instead of the x-axis (make horizontal bar charts). Should be used with a transposed axes. |
736 | 778 |
|
737 | 779 | Returns the modified canvas image. |
738 | 780 | ``` |
|
750 | 792 | bar-padding |
751 | 793 | stroke-thickness |
752 | 794 | super-sample |
753 | | - color-map] |
| 795 | + color-map |
| 796 | + transpose] |
754 | 797 |
|
755 | 798 | (def [canvas canvas-width canvas-height] :shadow (canvas-and-dimensions canvas width height)) |
756 | 799 | (default to-pixel-space (fn :convert [x y] [x y])) |
|
781 | 824 | :stroke-thickness (* super-sample stroke-thickness) |
782 | 825 | :point-radius (* super-sample point-radius) |
783 | 826 | :line-style-per-column line-style-per-column |
784 | | - :line-style line-style) |
| 827 | + :line-style line-style |
| 828 | + :transpose transpose) |
785 | 829 | # The resize + blend must match, as well as the destination pixels! |
786 | 830 | # After resize, alpha is pre-multiplied |
787 | 831 | (g/resize-into temp-canvas new-canvas true) |
|
836 | 880 |
|
837 | 881 | :area |
838 | 882 | (do |
839 | | - (def min-x (first pts)) |
840 | | - (def max-x (get pts (- (length pts) 2))) |
841 | | - (def bottom-y 10000) |
842 | | - (g/fill-path canvas [;pts max-x bottom-y min-x bottom-y] graph-color)) |
| 883 | + (if transpose |
| 884 | + (do |
| 885 | + (def min-y (get pts 1)) |
| 886 | + (def max-y (last pts)) |
| 887 | + (def left-x -1) |
| 888 | + (g/fill-path canvas [;pts left-x max-y left-x min-y] graph-color)) |
| 889 | + (do |
| 890 | + (def min-x (first pts)) |
| 891 | + (def max-x (get pts (- (length pts) 2))) |
| 892 | + (def bottom-y (inc canvas-height)) |
| 893 | + (g/fill-path canvas [;pts max-x bottom-y min-x bottom-y] graph-color)))) |
843 | 894 |
|
844 | 895 | :bar |
845 | 896 | (do |
846 | 897 | (def [base-x base-y] (to-pixel-space 0 0)) |
847 | 898 | (var last-right nil) |
| 899 | + (var last-top nil) |
848 | 900 | (loop [i :range [0 (length pts) 2]] |
849 | 901 | (def j (div i 2)) |
850 | 902 | (def is-first (= 0 i)) |
851 | 903 | (def is-last (= i (- (length pts) 2))) |
852 | 904 | (def x (get pts i)) |
853 | 905 | (def y (get pts (+ 1 i))) |
854 | 906 | (def color (x-colors (get xs j) (get ys j) j)) |
855 | | - (def x-next (if-not is-last (get pts (+ i 2)))) |
856 | | - (def x-prev (if-not is-first (get pts (- i 2)))) |
857 | | - # First and last bars extrapolate bar width |
858 | | - (def x-next1 (if is-last (+ x x (- x-prev)) x-next)) |
859 | | - (def x-prev1 (if is-first (+ x x (- x-next)) x-prev)) |
860 | | - # Prefer to use `last-right` to keep pixel padding consistent. Otherwise, the bars look a little off due to rounding errors. |
861 | | - (def x-left (if last-right (+ last-right bar-padding) (math/ceil (mean [x x-prev1])))) |
862 | | - (def x-right (math/floor (mean [x x-next1]))) |
863 | | - (def width (- x-right x-left bar-padding)) |
864 | | - (set last-right (+ x-left width)) |
865 | | - (g/fill-rect canvas x-left base-y width (- y base-y) color)))) |
| 907 | + # TODO - clean this up, could definitely be simpler |
| 908 | + (if transpose |
| 909 | + (do # iterating pos -> neg |
| 910 | + (def y-next (if-not is-last (get pts (+ i 3)))) |
| 911 | + (def y-prev (if-not is-first (get pts (- i 1)))) |
| 912 | + (def y-next1 (if is-last (+ y y (- y-prev)) y-next)) |
| 913 | + (def y-prev1 (if is-first (+ y y (- y-next)) y-prev)) |
| 914 | + (def y-bot (if last-top last-top (math/ceil (mean [y y-prev1])))) |
| 915 | + (def y-top (math/floor (mean [y y-next1]))) |
| 916 | + (def height (- y-bot y-top bar-padding)) |
| 917 | + (set last-top y-top) |
| 918 | + (g/fill-rect canvas base-x y-top (- x base-x) height color)) |
| 919 | + (do # iterating neg -> pos |
| 920 | + (def x-next (if-not is-last (get pts (+ i 2)))) |
| 921 | + (def x-prev (if-not is-first (get pts (- i 2)))) |
| 922 | + # First and last bars extrapolate bar width |
| 923 | + (def x-next1 (if is-last (+ x x (- x-prev)) x-next)) |
| 924 | + (def x-prev1 (if is-first (+ x x (- x-next)) x-prev)) |
| 925 | + # Prefer to use `last-right` to keep pixel padding consistent. Otherwise, the bars look a little off due to rounding errors. |
| 926 | + (def x-left (if last-right (+ last-right bar-padding) (math/ceil (mean [x x-prev1])))) |
| 927 | + (def x-right (math/floor (mean [x x-next1]))) |
| 928 | + (def width (- x-right x-left bar-padding)) |
| 929 | + (set last-right (+ x-left width)) |
| 930 | + (g/fill-rect canvas x-left base-y width (- y base-y) color)))))) |
866 | 931 |
|
867 | 932 | # Plot points |
868 | 933 | (when circle-points |
|
942 | 1007 | save-as |
943 | 1008 | legend-map |
944 | 1009 | tick-length |
945 | | - line-style line-style-per-column |
| 1010 | + line-style line-style-per-column bar-padding |
946 | 1011 | x-label y-label |
947 | 1012 | x-suffix x-prefix y-suffix y-prefix |
948 | 1013 | x-column y-column |
949 | 1014 | x-ticks x-minor-ticks y-minor-ticks |
950 | | - x-labels-vertical] |
| 1015 | + x-labels-vertical |
| 1016 | + transpose] |
951 | 1017 |
|
952 | 1018 | # Check parameters and set defaults. |
953 | 1019 | (assert data) |
|
1003 | 1069 |
|
1004 | 1070 | # Draw axes |
1005 | 1071 | (def [x-min x-max y-min y-max] :shadow |
1006 | | - (let [{:width view-width :height view-height} (g/unpack view)] |
1007 | | - (calculate-data-bounds data x-column y-columns |
1008 | | - view-width view-height 20 |
1009 | | - x-min x-max y-min y-max))) |
| 1072 | + (calculate-data-bounds data x-column y-columns |
| 1073 | + x-min x-max y-min y-max)) |
1010 | 1074 | (def [graph-view to-pixel-space _to-metric-space] |
1011 | | - (draw-axes :canvas view |
1012 | | - :padding padding :inner-padding inner-padding |
1013 | | - :font font |
1014 | | - :grid grid |
1015 | | - :format-x format-x :format-y format-y |
1016 | | - :x-suffix x-suffix :x-prefix x-prefix |
1017 | | - :y-suffix y-suffix :y-prefix y-prefix |
1018 | | - :x-min x-min :x-max x-max |
1019 | | - :y-min y-min :y-max y-max |
1020 | | - :x-ticks x-ticks :tick-length tick-length |
1021 | | - :x-label x-label :y-label y-label |
1022 | | - :x-minor-ticks x-minor-ticks |
1023 | | - :y-minor-ticks y-minor-ticks |
1024 | | - :x-labels-vertical x-labels-vertical)) |
| 1075 | + (draw-axes |
| 1076 | + :canvas view |
| 1077 | + :padding padding :inner-padding inner-padding |
| 1078 | + :font font |
| 1079 | + :grid grid |
| 1080 | + :format-x format-x :format-y format-y |
| 1081 | + :x-suffix x-suffix :x-prefix x-prefix |
| 1082 | + :y-suffix y-suffix :y-prefix y-prefix |
| 1083 | + :x-min x-min :x-max x-max |
| 1084 | + :y-min y-min :y-max y-max |
| 1085 | + :x-ticks x-ticks :tick-length tick-length |
| 1086 | + :x-label x-label :y-label y-label |
| 1087 | + :x-minor-ticks x-minor-ticks |
| 1088 | + :y-minor-ticks y-minor-ticks |
| 1089 | + :x-labels-vertical x-labels-vertical |
| 1090 | + :transpose transpose)) |
1025 | 1091 |
|
1026 | 1092 | # Render graph lines |
1027 | 1093 | (plot-line-graph |
|
1036 | 1102 | :super-sample super-sample |
1037 | 1103 | :circle-points (or circle-points scatter) |
1038 | 1104 | :stroke-thickness stroke-thickness |
1039 | | - :point-radius point-radius) |
| 1105 | + :point-radius point-radius |
| 1106 | + :bar-padding bar-padding |
| 1107 | + :transpose transpose) |
1040 | 1108 |
|
1041 | 1109 | # Draw internal legend if selected |
1042 | 1110 | (when (index-of legend [:top-left :top-right :bottom-left :bottom-right]) |
|
0 commit comments