Skip to content

Commit 993887a

Browse files
committed
Add stroked stippled lines.
Used to improve the weather chart example, and make stroked lines more useful for charting. However, there are still some issues with the stippling code, it needs to be better tested and reworked, especially when path segments are very close together.
1 parent af563c6 commit 993887a

6 files changed

Lines changed: 133 additions & 32 deletions

File tree

examples/charts/weather-chart.janet

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"latitude=" (string/format "%.3f" (get location :latitude))
4343
"&longitude=" (string/format "%.3f" (get location :longitude))
4444
"&past_days=" past-days
45+
"&timezone=Etc/UTC"
4546
"&hourly=" (string/join y-columns ",")
4647
"&temperature_unit=fahrenheit"))
4748
(print "Getting weather data from " url " ...")
@@ -56,6 +57,29 @@
5657
(def x-ticks (seq [[i x] :pairs timestamps :when (string/has-suffix? "T00:00" x)] i))
5758
(defn format-x [x] (slice (first (string/split "T" (get timestamps (math/round x) ""))) 5))
5859

60+
# Find the "now" location for a horizontal line
61+
(def ts-parse (peg/compile ~{:n (/ ':d+ ,scan-number)
62+
:main (* :n "-" :n "-" :n "T" :n ":" :n)}))
63+
(def {:year Y :month-day D :hours H :month M} (os/date))
64+
(def now-index (find-index |(if-let [[y m d h s] (peg/match ts-parse $)] (and (= y Y) (= m (inc M))
65+
(= d (inc D)) (= h H)))
66+
timestamps))
67+
68+
# Then split our forecase data into before and after now.
69+
(each col y-columns
70+
(def historic-data (array/slice (get data-frame col)))
71+
(def forecast-data (array/slice (get data-frame col)))
72+
(for i 0 (length historic-data)
73+
(when (< i now-index) (set (forecast-data i) nil))
74+
(when (> i now-index) (set (historic-data i) nil)))
75+
(put data-frame col historic-data)
76+
(put data-frame (keyword col "-forecast") forecast-data))
77+
(def extended-y-columns [;y-columns ;(map |(keyword $ "-forecast") y-columns)])
78+
(def styles (tabseq [c :in extended-y-columns]
79+
c (if (string/has-suffix? "-forecast" c)
80+
:stroke-stipple
81+
:stroke)))
82+
5983
# So fetch (disable this for black-on-white instead of white-on-black, looks more professional, less cool)
6084
(charts/dark-mode)
6185

@@ -68,16 +92,20 @@
6892
:height (* 2 540)
6993
:data data-frame
7094
:x-column :time
71-
:y-column y-columns
95+
:y-column extended-y-columns
7296
:color-map {:temperature_2m 0xFF0000dd
7397
:wind_speed_10m 0xFFdd0000
74-
:relative_humidity_2m 0xFF96AF00}
98+
:relative_humidity_2m 0xFF96AF00
99+
:temperature_2m-forecast 0xFF0000dd
100+
:wind_speed_10m-forecast 0xFFdd0000
101+
:relative_humidity_2m-forecast 0xFF96AF00}
75102
:y-label "°F / kmph / %"
76103
:legend-map {:temperature_2m "Temperature (°F)"
77104
:wind_speed_10m "Wind Speed (km/h)"
78105
:relative_humidity_2m "Humidity (%)"
79106
:precipitation_probability "Precipitation Probability (%)"}
80-
:line-style :stroke #{:temperature_2m :stroke}
107+
:legend-labels y-columns
108+
:line-style-per-column styles
81109
:x-ticks x-ticks
82110
:format-x format-x
83111
# :transpose true
@@ -89,13 +117,17 @@
89117
:padding 40 # default padding is 16, larger padding looks better for larger fonts and images.
90118
:super-sample 4 # Super-sampled anti-aliasing is a great way to get nice charts.
91119
# It is a bit slow to render, especially for large images (4x super-sampled is 16 times the pixels). Going beyond 4 is usually superfluous.
92-
:stroke-thickness 2
120+
:stroke-thickness 1
93121
:grid :solid # grid can be :none (nil), :solid, or :stipple
94122
#:grid :stipple
95123
:legend :top # legend can be :none (nil), :top, :top-left, :top-right, :bottom-left, :bottom-right
96124
#:legend :top-right
97125
#:x-minor-ticks 12
98126
#:y-minor-ticks 10
127+
:annotate (fn [view to-pixel to-metric]
128+
(def [x-now _] (to-pixel now-index 0))
129+
(gfx2d/draw-simple-text view (+ 2 x-now) 2 "Now" gfx2d/white :olive 2 2)
130+
(gfx2d/plot view x-now 0 x-now 10000 gfx2d/white))
99131
:save-as output)
100132

101133
(print "Wrote chart to " output)

spork/charts.janet

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@
376376
(errorf "unknown color map %v - expect function, array, tuple, table, struct, number, table, or keyword, got %v" cmap)))
377377

378378
###
379-
### Argument groups
379+
### Legends
380380
###
381381

382382
(defn draw-legend
@@ -973,7 +973,7 @@
973973

974974
# Plot lines between points
975975
(def line-style2 (get line-style-per-column ycol line-style))
976-
(enum line-style2 :plot :stipple :fine-stipple :stroke :bar :multi-bar :none :area)
976+
(enum line-style2 :plot :stipple :fine-stipple :stroke :stroke-stipple :bar :multi-bar :none :area)
977977
(def multi-bar (= line-style2 :multi-bar)) # multi-bar and bar share most of the same code
978978
(case (if multi-bar :bar line-style2)
979979

@@ -1002,6 +1002,10 @@
10021002
(do
10031003
(g/stroke-path canvas pts graph-color stroke-thickness))
10041004

1005+
:stroke-stipple
1006+
(do
1007+
(g/stroke-path canvas pts graph-color stroke-thickness false (* 8 stroke-thickness) (* 4 stroke-thickness)))
1008+
10051009
:area
10061010
(do
10071011
(if transpose
@@ -1135,12 +1139,15 @@
11351139
* :scatter - set to true to disable lines connecting points
11361140
* :legend - set to true to add a legend to the top of the chart
11371141
* :legend-map - a dictionary mapping column names to pretty text for the chart
1142+
* :legend-labels - optional list of y-column names to display in the legend
11381143
* :legend-padding - extra padding around legend area
11391144
* :point-radius - radius of points when drawing a scatter plot
11401145
* :line-style - How to draw lines. Can be one of :stroke, :plot, :none, :bar, :area, or :stipple. Default is :plot.
11411146
* :line-style-per-column - Optional dictionary to override line style by y-column name.
11421147
* :super-sample - Super Sample anti-aliasing for chart lines. Is a bit slow, but makes smooth plots. Works best with :stroke and :bar
11431148
* :stroke-thickness - thickness in pixels of the stroke of the graph when :line-type = :stroke
1149+
* :annotate - an optional function to add annotations to a rendered chart. This function should take 3 arguments:
1150+
(fn callback [view to-pixel to-metric] ...)
11441151
11451152
Axis Boundaries
11461153
* :x-min - minimum x coordinate on chart
@@ -1155,7 +1162,7 @@
11551162
x-min x-max y-min y-max
11561163
padding inner-padding inner-padding-x inner-padding-y title
11571164
circle-points
1158-
scatter grid legend super-sample stroke-thickness
1165+
scatter grid legend legend-labels super-sample stroke-thickness
11591166
format-x format-y
11601167
save-as
11611168
legend-map legend-padding
@@ -1168,6 +1175,7 @@
11681175
x-grid-ticks y-grid-ticks
11691176
x-labels-vertical
11701177
grid-between-x grid-between-y
1178+
annotate
11711179
transpose]
11721180

11731181
# Check parameters and set defaults.
@@ -1203,7 +1211,7 @@
12031211

12041212
# Check enums
12051213
(enum grid :none :solid :stipple :fine-stipple)
1206-
(enum line-style :plot :stipple :fine-stipple :stroke :bar :multi-bar :none :area) # - allow for dictionary of styles
1214+
(enum line-style :plot :stipple :fine-stipple :stroke :stroke-stipple :bar :multi-bar :none :area) # - allow for dictionary of styles
12071215
(enum legend :none :top :top-left :top-right :bottom-left :bottom-right)
12081216

12091217
# Allow variadic shorthand
@@ -1224,14 +1232,15 @@
12241232

12251233
# Add legend if legend = :top. This makes a horizontal legend just below the title with no extra framing
12261234
(default legend-padding (max 4 (div padding 4)))
1235+
(default legend-labels y-columns)
12271236
(when (= legend :top)
12281237
(+= title-padding (div padding 2))
12291238
(def view-width (- width padding padding))
1230-
(def [lw lh] (draw-legend nil :font font :padding legend-padding :labels y-columns :legend-map legend-map :view-width view-width))
1239+
(def [lw lh] (draw-legend nil :font font :padding legend-padding :labels legend-labels :legend-map legend-map :view-width view-width))
12311240
(def legend-view (g/viewport canvas (math/floor (* (- width lw) 0.5)) title-padding lw lh true))
12321241
(+= title-padding lh)
12331242
(-= title-padding (math/floor (* 0.5 padding))) # just looks a bit better
1234-
(draw-legend legend-view :font font :padding legend-padding :labels y-columns :color-map color-map
1243+
(draw-legend legend-view :font font :padding legend-padding :labels legend-labels :color-map color-map
12351244
:legend-map legend-map :text-color text-color :view-width view-width))
12361245

12371246
# Crop title section out of place where axis and charting will draw
@@ -1243,7 +1252,7 @@
12431252
(if transpose y-columns x-column)
12441253
(if transpose x-column y-columns)
12451254
x-min x-max y-min y-max))
1246-
(def [graph-view to-pixel-space _to-metric-space]
1255+
(def [graph-view to-pixel-space to-metric-space]
12471256
(draw-axes
12481257
:canvas view
12491258
:padding padding :inner-padding inner-padding
@@ -1282,9 +1291,11 @@
12821291
:bar-padding bar-padding
12831292
:transpose transpose)
12841293

1294+
(when annotate (annotate graph-view to-pixel-space to-metric-space))
1295+
12851296
# Draw internal legend if selected
12861297
(when (index-of legend [:top-left :top-right :bottom-left :bottom-right])
1287-
(def [lw lh] (draw-legend nil :font font :padding legend-padding :labels y-columns :legend-map legend-map :frame false))
1298+
(def [lw lh] (draw-legend nil :font font :padding legend-padding :labels legend-labels :legend-map legend-map :frame false))
12881299
(def {:width gw :height gh} (g/unpack graph-view))
12891300
(def legend-view
12901301
(case legend
@@ -1294,10 +1305,9 @@
12941305
:bottom-right (g/viewport graph-view (- gw lw padding) (- gh lh padding) lw lh true)))
12951306
(when (not= :none background-color)
12961307
(g/fill-rect legend-view 0 0 lw lh background-color))
1297-
(draw-legend legend-view :font font :padding legend-padding :labels y-columns :view-width 0
1308+
(draw-legend legend-view :font font :padding legend-padding :labels legend-labels :view-width 0
12981309
:color-map color-map :legend-map legend-map :frame true))
12991310

1300-
# Save to file
13011311
(when save-as
13021312
(g/save save-as canvas))
13031313

spork/gfx2d-codegen.janet

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@
254254

255255
(function v/lerp :static :inline
256256
[a:V2 b:V2 t:double] -> V2
257-
(return (v/+ (v/s* t a) (v/s* (- 1 t) b))))
257+
(return (v/+ (v/s* (- 1 t) a) (v/s* t b))))
258258

259259
(function v/rotate-cw-90 :static :inline
260260
[a:V2] -> V2
@@ -1554,27 +1554,54 @@
15541554

15551555
(cfunction stroke-path
15561556
"Stroke a line along a path"
1557-
[img:*Image points:indexed ,;shader-args &opt thickness:double=1 join-end:bool=0] -> *Image
1557+
[img:*Image points:indexed ,;shader-args &opt thickness:double=1 join-end:bool=0 stipple-cycle:int=0 stipple-on:int=0] -> *Image
15581558
(var npoints:int 0)
15591559
(var (vs 'V2))
15601560
(if join-end
15611561
(set vs (indexed-to-vs-join-end points &npoints))
15621562
(set vs (indexed-to-vs points &npoints)))
1563-
(each-i 1 npoints
1564-
(def A:V2 (aref vs (- i 1)))
1565-
(def B:V2 (aref vs i))
1566-
(def AB:V2 (v/- B A))
1567-
(def leg:V2 (v/s* thickness (v/rotate-cw-90 (v/norm AB))))
1568-
(def p1:V2 (v/+ A leg))
1569-
(def p2:V2 (v/- A leg))
1570-
(def p3:V2 (v/- B leg))
1571-
(def p4:V2 (v/+ B leg))
1572-
(def (ps (array V2)) @[p1 p2 p3 p4 p1])
1573-
(fill-path-impl img ps 5 ,;shader-params))
1574-
(each-i 0 npoints
1575-
(def P:V2 (aref vs i))
1576-
(circle img P.x P.y (+ 0.25 thickness) ,;shader-params))
1577-
(janet-sfree vs:*V2) # self-test for mangling of type-grafted symbols
1563+
(if (or (>= 0 stipple-cycle) (>= stipple-on stipple-cycle))
1564+
(do # solid stroke
1565+
(each-i 1 npoints
1566+
(def A:V2 (aref vs (- i 1)))
1567+
(def B:V2 (aref vs i))
1568+
(def AB:V2 (v/- B A))
1569+
(def ABlen:float (v/len AB))
1570+
(if (= 0 ABlen) (continue))
1571+
(def leg:V2 (v/s* thickness (v/rotate-cw-90 (v/norm AB))))
1572+
(def p1:V2 (v/+ A leg))
1573+
(def p2:V2 (v/- A leg))
1574+
(def p3:V2 (v/- B leg))
1575+
(def p4:V2 (v/+ B leg))
1576+
(def (ps (array V2)) @[p1 p2 p3 p4 p1])
1577+
(fill-path-impl img ps 5 ,;shader-params))
1578+
(each-i 0 npoints
1579+
(def P:V2 (aref vs i))
1580+
(circle img P.x P.y (+ 0.25 thickness) ,;shader-params)))
1581+
(do # stippled stroke
1582+
(var pixel-lookback:float 0)
1583+
(each-i 1 npoints
1584+
(def A:V2 (aref vs (- i 1)))
1585+
(def B:V2 (aref vs i))
1586+
(def AB:V2 (v/- B A))
1587+
(def ABlen:float (v/len AB))
1588+
(if (= 0 ABlen) (continue))
1589+
(def leg:V2 (v/s* thickness (v/rotate-cw-90 (v/norm AB))))
1590+
(for [(def t:float (- (/ pixel-lookback ABlen))) (< t 1) (+= t (/ stipple-cycle ABlen))]
1591+
(def t-end:float (+ t (/ stipple-on ABlen)))
1592+
(def t1:double (- (clamp t 0 1) (/ 1 ABlen)))
1593+
(def t2:double (clamp t-end 0 1))
1594+
(when (and (not= t1 t2) (or (> t 0) (> t-end 0)))
1595+
(def C:V2 (v/lerp A B t1))
1596+
(def D:V2 (v/lerp A B t2))
1597+
(def p1:V2 (v/+ C leg))
1598+
(def p2:V2 (v/- C leg))
1599+
(def p3:V2 (v/- D leg))
1600+
(def p4:V2 (v/+ D leg))
1601+
(def (ps (array V2)) @[p1 p2 p3 p4 p1])
1602+
(fill-path-impl img ps 5 ,;shader-params))
1603+
(set pixel-lookback (* ABlen (- 1 t)))))))
1604+
(janet-sfree vs:*V2) # cjanet self-test for mangling of type-grafted symbols
15781605
(return img))
15791606

15801607
###
@@ -1888,7 +1915,7 @@
18881915
# Always do internal measurement non-rotated, and then rotate output during sampling.
18891916
(def measure:TextMeasure (measure-text-impl font cursor scale 0))
18901917
# Shift origin to match simple text - by default, text origin is upper most glyph. Instead, should be ascender line.
1891-
(def from-glyphtop:int (? (= 1 (band orientation 2r1100)) 1 0))
1918+
(def from-glyphtop:int (? (= 2r100 (band orientation 2r1100)) 1 0))
18921919
(var fx:float (- measure.xmin))
18931920
(var fy:float (? from-glyphtop (- measure.ymin) (* font->ascent scale font->scale font->glyph-space-to-pixel)))
18941921
(while *cursor

test/gold/bezier-stipple-2.png

4.79 KB
Loading

test/gold/bezier-stipple.png

4 KB
Loading

test/suite-gfx2d.janet

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,38 @@
228228

229229
(test-stroke-bezier)
230230

231+
(defn test-stroke-bezier-stipple
232+
[]
233+
(def width 256)
234+
(def height 256)
235+
(def canvas (blank width height 3))
236+
(def control-points [10 10
237+
10 (- height 10)
238+
(- width 10) (- height 10)
239+
(- width 10) 10])
240+
(def points (bezier-path control-points 0.04))
241+
(stroke-path canvas [0 0 (/ width 2) (/ height 2) width height] red 4 false 20 10)
242+
(stroke-path canvas points green 4 false 20 10)
243+
(check-image canvas "bezier-stipple.png"))
244+
245+
(test-stroke-bezier-stipple)
246+
247+
(defn test-stroke-bezier-stipple-2
248+
[]
249+
(def width 256)
250+
(def height 256)
251+
(def canvas (blank width height 3))
252+
(def control-points [10 10
253+
10 (- height 10)
254+
(- width 10) (- height 10)
255+
(- width 10) 10])
256+
(def points (bezier-path control-points 0.004))
257+
(stroke-path canvas [0 0 (/ width 2) (/ height 2) width height] red 4 false 10 5)
258+
(stroke-path canvas points green 4 false 10 5)
259+
(check-image canvas "bezier-stipple-2.png"))
260+
261+
(test-stroke-bezier-stipple-2)
262+
231263
(defn test-fill-bezier
232264
[]
233265
(def width 256)

0 commit comments

Comments
 (0)