Skip to content

Commit 72fc85d

Browse files
committed
Add :transpose to line charts.
Allows also for horizontal bar charts.
1 parent 454accc commit 72fc85d

1 file changed

Lines changed: 124 additions & 56 deletions

File tree

spork/charts.janet

Lines changed: 124 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,21 @@
99
### libraries like plPlot may be more suitable. However, out-of-the-box chart generation with minimal
1010
### dependencies is very useful to have.
1111
###
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.
1414
###
1515
### Data frame example:
1616
###
1717
### {:timestamp [1 2 3 4 5 6]
1818
### :temperature-1 [75.1 75.2 75.4 75.5 75.5 75.4]
1919
### :temperature-2 [55.1 55.4 55.7 60.0 60.4 60.9]}
2020
###
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+
###
2327
### [x] - horizontal legend should still be able to wrap vertically if too wide.
2428
### [x] - LABEL YOUR AXES!
2529
### [x] - wrap colors, padding, font, etc. into some kind of styling table to pass around consistently
@@ -33,6 +37,7 @@
3337
### [x] - heat map
3438
### [ ] - error bars on line chart
3539
### [ ] - fill between chart
40+
### [ ] - attributed text for captions and annotations
3641
### [x] - handle nils in y-columns for sparse data
3742
### [x] - easier custom chart annotations in the metric space (horizontal lines, vertical lines, etc.)
3843
### [ ] - captions and sub-titles
@@ -150,7 +155,6 @@
150155
Use this information for calculating render transform. Should handle non-existant columns."
151156
[data x-column y-columns
152157
&opt
153-
width height min-spacing
154158
override-min-x override-max-x
155159
override-min-y override-max-y]
156160

@@ -159,10 +163,10 @@
159163
(break [override-min-x override-max-x override-min-y override-max-y]))
160164

161165
# 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))
162168
(var min-y math/inf)
163169
(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))
166170
(each c y-columns
167171
(set min-y (min min-y (extreme < (filter identity (get data c [math/inf])))))
168172
(set max-y (max max-y (extreme > (filter identity (get data c [math/-inf]))))))
@@ -255,22 +259,27 @@
255259
(setdyn *stroke-color* g/black)
256260
(setdyn *text-color* g/black))
257261

258-
(defn color-lerp
262+
(defn- color-lerp-internal
259263
[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)
263266
(g/srgb
264267
(lerp ar br t)
265268
(lerp ag bg t)
266269
(lerp ab bb t)
267270
(lerp aa ba t)))
268271

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+
269277
(defn make-color-map
270278
"Create a function that linearly interpolates between colors for colormapping."
271279
[& colors]
272280
(def n-colors (length colors))
273281
(def n-1-colors (- n-colors 1))
282+
(def srgb-colors (map g/as-srgb colors))
274283
(fn :interp
275284
[t]
276285
(def t :shadow (clamp t 0 1))
@@ -281,7 +290,8 @@
281290
(def t-at-b (/ b-index n-1-colors))
282291
(def ab-interval (- t-at-b t-at-a))
283292
(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)))
285295

286296
(defn invert-color-map
287297
"Create an inverted color-map from an existing color map."
@@ -515,12 +525,14 @@
515525
* :x-suffix - if format-x not provided, allows easily adding a string suffix to x axis tick mark labels
516526
* :y-suffix - if format-y not provided, allows easily adding a string suffix to y axis tick mark labels
517527
* :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.
518529
* :x-minor-ticks - How many minor tick marks, if any, to place between major tick marks on the x axis
519530
* :y-minor-ticks - How many minor tick marks, if any, to place between major tick marks on the y axis
520531
* :x-labels-vertical - Turn x labels vertical so more can fit on the axis
521532
* :min-x-spacing - When guessing x ticks, allow setting a lower limit to the metric spacing between ticks
522533
* :min-y-spacing - When guessing y ticks, allow setting a lower limit to the metric spacing between ticks
523534
* :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
524536
525537
Returns a 4-tuple [view:gfx2d/Image to-pixel-space:fn to-metric-space:fn outer-canvas:gfx2d/Image]
526538
@@ -535,7 +547,28 @@
535547
grid format-x format-y
536548
x-label y-label tick-length
537549
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]))
539572

540573
(def [canvas width height] :shadow (canvas-and-dimensions canvas width height))
541574
(default padding (dyn *padding* default-padding))
@@ -596,7 +629,15 @@
596629

597630
# Guess y axis ticks - used to calculate left and right padding
598631
(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)))
600641

601642
# Calculate left and right padding once y-axis is guessed
602643
(def outer-left-padding (+ padding y-axis-tick-label-width (if y-label (+ padding font-height) 0)))
@@ -622,7 +663,7 @@
622663

623664
# TODO - replace tick mark draw calls to g/plot with g/fill-rect to allow for thicker ticks
624665

625-
# Draw Y axis
666+
# Draw vertical axis
626667
(assert yticks "unable to generate y ticks. Make your chart bigger?")
627668
(each metric-y yticks
628669
(def [_ pixel-y] (convert 0 metric-y))
@@ -634,7 +675,7 @@
634675
(g/plot canvas left-padding rounded-pixel-y (- width right-padding) rounded-pixel-y grid-color stipple-cycle stipple-on)
635676
(g/plot canvas (+ tick-trim outer-left-padding) rounded-pixel-y (+ outer-left-padding tick-height) rounded-pixel-y grid-color)))
636677

637-
# Draw X axis - allow manual override for x tick marks
678+
# Draw horizontal axis - allow manual override for x tick marks
638679
(def [xticks xformat]
639680
(if x-ticks [x-ticks (if format-x format-x string)]
640681
(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,7 +692,7 @@
651692
(g/plot canvas rounded-pixel-x top-padding rounded-pixel-x (- height bottom-padding) grid-color stipple-cycle stipple-on)
652693
(g/plot canvas rounded-pixel-x (- height outer-bottom-padding tick-trim) rounded-pixel-x (- height outer-bottom-padding tick-height) grid-color)))
653694

654-
# Draw minor x tick marks
695+
# Draw minor horizontal axis tick marks
655696
(when (and x-minor-ticks (< 1 (length xticks)))
656697
(def len (length xticks))
657698
(def dx-first (- (in xticks 1) (in xticks 0)))
@@ -668,7 +709,7 @@
668709
:when (and (> x left-padding) (< x (- width right-padding)))]
669710
(g/plot canvas x (- height outer-bottom-padding tick-height) x (- height outer-bottom-padding (div tick-height 2)) grid-color)))
670711

671-
# Draw minor y tick marks
712+
# Draw minor vertical axis tick marks
672713
(when (and y-minor-ticks (< 1 (length yticks)))
673714
(def len (length yticks))
674715
(def dy-first (- (in yticks 1) (in yticks 0)))
@@ -733,6 +774,7 @@
733774
* :bar-padding - space between bars in bar-charts
734775
* :stroke-thickness - thickness in pixels of the stroke of the graph when :line-type = :stroke
735776
* :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.
736778
737779
Returns the modified canvas image.
738780
```
@@ -750,7 +792,8 @@
750792
bar-padding
751793
stroke-thickness
752794
super-sample
753-
color-map]
795+
color-map
796+
transpose]
754797

755798
(def [canvas canvas-width canvas-height] :shadow (canvas-and-dimensions canvas width height))
756799
(default to-pixel-space (fn :convert [x y] [x y]))
@@ -781,7 +824,8 @@
781824
:stroke-thickness (* super-sample stroke-thickness)
782825
:point-radius (* super-sample point-radius)
783826
:line-style-per-column line-style-per-column
784-
:line-style line-style)
827+
:line-style line-style
828+
:transpose transpose)
785829
# The resize + blend must match, as well as the destination pixels!
786830
# After resize, alpha is pre-multiplied
787831
(g/resize-into temp-canvas new-canvas true)
@@ -836,33 +880,54 @@
836880

837881
:area
838882
(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))))
843894

844895
:bar
845896
(do
846897
(def [base-x base-y] (to-pixel-space 0 0))
847898
(var last-right nil)
899+
(var last-top nil)
848900
(loop [i :range [0 (length pts) 2]]
849901
(def j (div i 2))
850902
(def is-first (= 0 i))
851903
(def is-last (= i (- (length pts) 2)))
852904
(def x (get pts i))
853905
(def y (get pts (+ 1 i)))
854906
(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))))))
866931

867932
# Plot points
868933
(when circle-points
@@ -942,12 +1007,13 @@
9421007
save-as
9431008
legend-map
9441009
tick-length
945-
line-style line-style-per-column
1010+
line-style line-style-per-column bar-padding
9461011
x-label y-label
9471012
x-suffix x-prefix y-suffix y-prefix
9481013
x-column y-column
9491014
x-ticks x-minor-ticks y-minor-ticks
950-
x-labels-vertical]
1015+
x-labels-vertical
1016+
transpose]
9511017

9521018
# Check parameters and set defaults.
9531019
(assert data)
@@ -1003,25 +1069,25 @@
10031069

10041070
# Draw axes
10051071
(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))
10101074
(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))
10251091

10261092
# Render graph lines
10271093
(plot-line-graph
@@ -1036,7 +1102,9 @@
10361102
:super-sample super-sample
10371103
:circle-points (or circle-points scatter)
10381104
:stroke-thickness stroke-thickness
1039-
:point-radius point-radius)
1105+
:point-radius point-radius
1106+
:bar-padding bar-padding
1107+
:transpose transpose)
10401108

10411109
# Draw internal legend if selected
10421110
(when (index-of legend [:top-left :top-right :bottom-left :bottom-right])

0 commit comments

Comments
 (0)