-
Notifications
You must be signed in to change notification settings - Fork 0
/
app.R
1301 lines (1061 loc) · 50.1 KB
/
app.R
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Helsinki Region Travel Time comparison application
# Helsinki Region Travel Time Matrix 2018 <--> My thesis survey results
# 15.10.2020
# Sampo Vesanen
# Known issues:
# - A late addition to the application is making sure all values have two
# decimal places. This is all good for the tooltip values and the values
# printed on the map proper. However, the legend scale is a bit tricky.
# Essential cut() function used in CreateEqualColumn() reduces decimal places
# and I don't know how to prevent this. As a consequence, I have to add the
# decimal places back to the legend with regex, resulting in situations where
# the largest value shown on map can be larger than the top value in the
# color scale. I don't think there are other caveats.
# - .loadingdiv is bound by .col-sm-9, the mainPanel() content. For this
# reason, on first load the loading appears now on top of the screen. As
# the map is loaded, loading text moves to th center of the screen. I could
# not find a solution to this for the time being. This only appeared after
# update to R 4.0.0 and updating several libraries. Before, girafeOutput()
# did not need width and height, and the loading div was placed in the center
# from the start.
# - Of minor importance: select#postal_label_choice does not scale with window
# size
# - Saarijarvi island in Espoo is not shown correctly when using the inland water
# layer. We can't use the function ggspatial::geom_spatial_polygon() in
# shinyapps.io because the platform does not like ggpspatial dependency lwgeom
# for whatever reason.
# - postal_labels_choice does not scale with window size
# - Of minor importance: The JavaScript additions to dropdown menus are quite
# shoddy. User can see that they are added each time menu opens. Also, fill
# icons in "color scheme" menu do not appear on the first opening. In fact,
# they only appear after first opening "visualise data" menu once.
# - There is a rare issue where the application complains about non-unique
# breaks. This occurs inside CreateEqualColumn(), where a column presumably
# does not have enough unique values to create the amount of symbology classes
# the app requests. To counter this, reactive object checkSliderInput() is
# created. This handles the error, but not before the map view disappears
# and the error message is shown in top left corner. For now, I do not intend
# to fix this outcome which lasts on the user's screen maybe 15 seconds. The
# only erroneous combination I've discovered so far is 02860 Siikajarvi,
# ttm18_all_pct and 9-11 symbology classes.
#### 1 Initialise --------------------------------------------------------------
library(shiny)
library(shinythemes)
library(ggplot2)
library(dplyr)
library(htmltools)
library(rgdal)
library(shinyjs)
library(ggiraph)
library(data.table)
library(rgeos)
library(sf)
library(shinyWidgets)
library(ggsn)
library(fst)
library(ggnewscale)
library(classInt)
library(sp)
library(rlang)
library(stringr)
# App version
app_v <- "0071.postal (15.10.2020)"
# Data directories
munspath <- "appdata/hcr_muns.shp"
othermunspath <- "appdata/other_muns.shp"
subdivpath <- "appdata/PKS_suuralue.kml"
waterpath <- "appdata/ua2012_water.shp"
roadpath <- "appdata/mainroads.shp"
# Thesis' processed data
recordspath <- "appdata/shinyrecords.csv"
postal_path <- "appdata/shinypostal.csv"
gridzipcodes <- "appdata/grid_for_r.csv"
fst_postal_fp <- "TTM18_postal"
# Directives
csspath <- "app_style.css"
jspath <- "app_script.js"
tooltip_path <- "app_tooltip.html"
info_path <- "app_info.html"
# Source functions and postal code variables
source("app_funcs.R")
#### 2 Import data layers ------------------------------------------------------
#### 2.1 Walking center polygon ------------------------------------------------
# use this CRS information throughout the app
app_crs <- sp::CRS("+init=epsg:3067")
# TTM18 Helsinki walking center polygon. Use fortified version of the walking
# center for visualisation
walkingHki <-
data.frame(
long = c(387678.024778, 387891.53396, 383453.380944, 383239.871737, 387678.024778),
lat = c(6675360.99039, 6670403.35286, 6670212.21613, 6675169.85373, 6675360.99039)) %>%
sf::st_as_sf(coords = c("long", "lat"), crs = app_crs) %>%
dplyr::summarise(geometry = sf::st_combine(geometry)) %>%
sf::st_cast("POLYGON")
# Fortify
walk_f <-
sf::st_coordinates(walkingHki)[, 1:2] %>%
sp::Polygon(.) %>%
ggplot2::fortify(.) %>%
dplyr::mutate(label = "walk")
#### 2.2 Municipality borders --------------------------------------------------
# Get municipality borders. Fortify SP DataFrame for ggplot2. Remove unnecessary
# columns to save memory.
# Shapefile data is Regional population density 2012, Statistics Finland.
# http://urn.fi/urn:nbn:fi:csc-kata00001000000000000226.
muns_f <-
rgdal::readOGR(munspath, stringsAsFactors = TRUE) %>%
sp::spTransform(., app_crs)
muns_f <-
merge(ggplot2::fortify(muns_f), as.data.frame(muns_f), by.x = "id", by.y = 0)
# Bordering municipalities. Does not require any of the shapefile attribute data.
othermuns_f <-
rgdal::readOGR(othermunspath, stringsAsFactors = TRUE) %>%
sp::spTransform(., app_crs) %>%
ggplot2::fortify(.)
#### 2.3 Postal code areas -----------------------------------------------------
postal <-
read.csv(file = postal_path,
header = TRUE,
sep = ",",
colClasses = c(zipcode = "factor", kunta = "factor",
geometry = "character"),
stringsAsFactors = TRUE) %>%
dplyr::select(c(2, 3, 6, 108))
# "postal" geometries are in well-known text format. Some processing is needed
# to utilise these polygons in R. readWKT() uses rgeos.
geometries <- lapply(postal[, "geometry"], "readWKT", p4s = app_crs)
sp_tmp_ID <- mapply(sp::spChFIDs, geometries, as.character(postal[, 1]))
row.names(postal) <- postal[, 1]
# Preserve SpatialPolygons version of "postal" for the spatial join
postal <- sp::SpatialPolygonsDataFrame(
sp::SpatialPolygons(unlist(lapply(sp_tmp_ID, function(x) x@polygons)),
proj4string = app_crs), data = postal)
# Fortify and preserve Polygon attribute data
postal_f <-
merge(ggplot2::fortify(postal),
as.data.frame(postal),
by.x = "id",
by.y = 0) %>%
dplyr::select(-geometry)
# This dataframe, produced in Python, helps find the zipcodes for each YKR_ID.
# 99999 is outside of research area.
ykrid_zipcodes <-
read.csv(file = gridzipcodes,
header = TRUE,
sep = ",",
colClasses = c(YKR_ID = "integer", zipcode = "character"),
stringsAsFactors = TRUE) %>%
dplyr::select(-X)
### 2.4 Subdivisions -----------------------------------------------------------
subdiv <- rgdal::readOGR(subdivpath,
use_iconv = TRUE,
encoding = "UTF-8",
stringsAsFactors = TRUE) %>%
sp::spTransform(., app_crs)
subdiv_f <-
merge(ggplot2::fortify(subdiv),
as.data.frame(subdiv),
by.x = "id",
by.y = 0)
subdiv_f <- subdiv_f[, -9]
# Align area names with thesisdata$subdiv
levels(subdiv_f$Name) <- c("Vantaa Aviapolis", "Helsinki Southern",
"Vantaa Hakunila", "Helsinki Eastern",
"Helsinki Southeastern", "Kauniainen",
"Helsinki Central", "Vantaa Kivisto",
"Helsinki Northeastern", "Vantaa Koivukyla",
"Vantaa Korso", "Helsinki Western",
"Vantaa Myyrmaki", "Helsinki Ostersundom",
"Helsinki Northern", "Espoo Pohjois-Espoo",
"Espoo Suur-Espoonlahti", "Espoo Suur-Kauklahti",
"Espoo Suur-Leppavaara", "Espoo Suur-Matinkyla",
"Espoo Suur-Tapiola", "Vantaa Tikkurila",
"Espoo Vanha-Espoo")
subdiv_f$Name <- factor(subdiv_f$Name, levels = sort(levels(subdiv_f$Name)))
# Reorder dataframe by subdivision
subdiv_f <- subdiv_f[order(subdiv_f$Name), ]
# 2.5 Thesis survey data -------------------------------------------------------
# Import Python processed thesis survey data. This data is later (chapter 4.2)
# joined with the currently fetched YKR ID Travel Time Matrix 2018 data.
thesisdata <-
read.csv(file = recordspath,
header = TRUE,
sep = ",",
colClasses = c(zipcode = "factor"),
stringsAsFactors = TRUE) %>%
dplyr::select(zipcode, parktime, walktime, timeofday) %>%
dplyr::filter(parktime <= 59,
walktime <= 59) %>%
dplyr::group_by(zipcode) %>%
# Select timings for thesis_ columns
dplyr::mutate(r_sfp = case_when(timeofday == 1 ~ parktime, TRUE ~ NA_integer_),
m_sfp = case_when(timeofday == 2 ~ parktime, TRUE ~ NA_integer_),
all_sfp = parktime,
r_wtd = case_when(timeofday == 1 ~ walktime, TRUE ~ NA_integer_),
m_wtd = case_when(timeofday == 2 ~ walktime, TRUE ~ NA_integer_),
all_wtd = walktime) %>%
# Calculate summaries by zipcode, then round to two decimals
dplyr::summarise(thesis_r_sfp = mean(r_sfp, na.rm = TRUE),
thesis_m_sfp = mean(m_sfp, na.rm = TRUE),
thesis_all_sfp = mean(all_sfp, na.rm = TRUE),
thesis_r_wtd = mean(r_wtd, na.rm = TRUE),
thesis_m_wtd = mean(m_wtd, na.rm = TRUE),
thesis_all_wtd = mean(all_wtd, na.rm = TRUE),
vals_in_zip = length(zipcode)) %>%
dplyr::mutate_if(is.numeric, round, 2)
# NaNs are introduced in calculation of mean. Change to NA. Do not apply changes
# to column zipcode
thesisdata[, -1] <- data.frame(
sapply(thesisdata[, -1], function(x) ifelse(is.nan(x), NA, x)))
#### 2.6 Water and roads -------------------------------------------------------
# Main roads
roads_f <-
rgdal::readOGR(roadpath, stringsAsFactors = TRUE) %>%
sp::spTransform(., app_crs) %>%
ggplot2::fortify(.)
# UA2012 inland water. Using geom_spatial_polygon() from ggspatial would fix
# the island in Saarijarvi, Espoo, but shinyapps.io does not like the ggspatial
# dependency lwgeom (tested in August 2020).
water_f <-
rgdal::readOGR(waterpath, stringsAsFactors = TRUE) %>%
sp::spTransform(., app_crs) %>%
ggplot2::fortify(.)
#### 2.7 Label comparison plot features ----------------------------------------
# Create labels for zipcodes
zipcode_lbl <- GetCentroids(postal_f, "zipcode", "zipcode")
muns_lbl <- GetCentroids(muns_f, "nimi", "nimi")
subdiv_lbl <- GetCentroids(subdiv_f, "Name", "Name")
# Finetune locations for certain labels in zipcode_lbl. GetCentroids saves the
# second parameter as rownames. We can use that to reliably find correct rows
# to finetune.
zipcode_lbl["00250", 1] %+=% 500 # Taka-Toolo
zipcode_lbl["00980", 1] %-=% 850 # Etela-Vuosaari
zipcode_lbl["01640", 2] %-=% 300 # Hamevaara
zipcode_lbl["01730", 2] %-=% 500 # Vantaanpuisto
zipcode_lbl["02380", 1] %+=% 2400 # Suvisaaristo
zipcode_lbl["02820", 1] %+=% 1000 # Nupuri-Nuuksio
# Remove municipality names from subdivision annotations
subdiv_lbl$label <- gsub(".* ", "", unique(subdiv_f$Name))
rownames(subdiv_lbl) <- gsub(".* ", "", rownames(subdiv_lbl))
# Manually move labels several subdivision labels to better positions.
subdiv_lbl["Suur-Espoonlahti", "lat"] <- subdiv_lbl["Southern", "lat"]
subdiv_lbl["Suur-Espoonlahti", "long"] <- subdiv_lbl["Pohjois-Espoo", "long"]
subdiv_lbl["Southeastern", "lat"] <- subdiv_lbl["Southern", "lat"] + 1500
subdiv_lbl["Southeastern", "long"] <- subdiv_lbl["Korso", "long"]
subdiv_lbl["Southern", "lat"] %+=% 3000
subdiv_lbl["Ostersundom", "lat"] %-=% 500
subdiv_lbl["Ostersundom", "long"] %-=% 400
subdiv_lbl["Suur-Matinkyla", "lat"] %-=% 500
# In this named vector the first part is the name of the new, classified
# column. Second part is the original column name where the classification
# was calculated from.
vis_cols <- c("ttm18_r_avg" = "ttm_r_avg",
"ttm18_m_avg" = "ttm_m_avg",
"ttm18_all_avg" = "ttm_all_avg",
"ttm18_r_drivetime" = "ttm_r_drivetime",
"ttm18_m_drivetime" = "ttm_m_drivetime",
"ttm18_all_drivetime" = "ttm_all_drivetime",
"ttm18_r_pct" = "ttm_r_pct",
"ttm18_m_pct" = "ttm_m_pct",
"ttm18_all_pct" = "ttm_all_pct",
"msc_r_sfp" = "thesis_r_sfp",
"msc_m_sfp" = "thesis_m_sfp",
"msc_all_sfp" = "thesis_all_sfp",
"msc_r_wtd" = "thesis_r_wtd",
"msc_m_wtd" = "thesis_m_wtd",
"msc_all_wtd" = "thesis_all_wtd",
"msc_r_drivetime" = "thesis_r_drivetime",
"msc_m_drivetime" = "thesis_m_drivetime",
"msc_all_drivetime" = "thesis_all_drivetime",
"msc_r_pct" = "thesis_r_pct",
"msc_m_pct" = "thesis_m_pct",
"msc_all_pct" = "thesis_all_pct",
"compare_r_sfp" = "comp_r_sfp",
"compare_m_sfp" = "comp_m_sfp",
"compare_all_sfp" = "comp_all_sfp",
"compare_r_wtd" = "comp_r_wtd",
"compare_m_wtd" = "comp_m_wtd",
"compare_all_wtd" = "comp_all_wtd",
"compare_r_drivetime" = "comp_r_drivetime",
"compare_m_drivetime" = "comp_m_drivetime",
"compare_all_drivetime" = "comp_all_drivetime",
"compare_r_pct" = "comp_r_pct",
"compare_m_pct" = "comp_m_pct",
"compare_all_pct" = "comp_all_pct")
#### 3 fst operations ----------------------------------------------------------
#### 3.1 Prepare reactive fetch of TTM18 data ----------------------------------
# NB! The execution of this code will fail at this point if TTM18 data is not
# converted to fst. Please make sure you have a local copy of the fst format
# dataset of TTM18 aggregated to postal code areas.
# Get all of the fst-format TTM18 aggregated to postal code area level
all_postal_fst <- list.files(path = fst_postal_fp,
pattern = ".fst$",
recursive = TRUE,
full.names = TRUE)
### 3.2 Prepare values for locked up class breaks ------------------------------
# In this section prepare all values for the case where map symbology needs to
# remain the same with different settings.
# Get all possible datasets from fst files, then group by destination postal
# code area, and get only the first row for each zipcode
for (i in 1:length(all_postal_fst)) {
if (i == 1) {
# As these fst files are full of multiples, select the first row of each fst
# destination postal code areas
bigdf <-
Reactive_TTM_fetch(all_postal_fst[i], thesisdata, postal_f) %>%
dplyr::group_by(id) %>%
dplyr::filter(row_number() == 1)
} else {
thisRound <-
Reactive_TTM_fetch(all_postal_fst[i], thesisdata, postal_f) %>%
dplyr::group_by(id) %>%
dplyr::filter(row_number() == 1)
bigdf <- rbind(bigdf, thisRound)
}
}
# Get value ranges of the column groups
ttm_avg_range <-
range(c(bigdf$ttm_r_avg, bigdf$ttm_m_avg, bigdf$ttm_all_avg),
na.rm = TRUE)
ttm_drivetime_range <-
range(c(bigdf$ttm_r_drivetime, bigdf$ttm_m_drivetime, bigdf$ttm_all_drivetime),
na.rm = TRUE)
ttm_pct_range <-
range(c(bigdf$ttm_r_pct, bigdf$ttm_m_pct, bigdf$ttm_all_pct),
na.rm = TRUE)
thesis_sfp_range <-
range(c(bigdf$thesis_r_sfp, bigdf$thesis_m_sfp, bigdf$thesis_all_sfp),
na.rm = TRUE)
thesis_wtd_range <-
range(c(bigdf$thesis_r_wtd, bigdf$thesis_m_wtd, bigdf$thesis_all_wtd),
na.rm = TRUE)
thesis_drivetime_range <-
range(c(bigdf$thesis_r_drivetime, bigdf$thesis_m_drivetime, bigdf$thesis_all_drivetime),
na.rm = TRUE)
thesis_pct_range <-
range(c(bigdf$thesis_r_pct, bigdf$thesis_m_pct, bigdf$thesis_all_pct),
na.rm = TRUE)
comp_sfp_range <-
range(c(bigdf$comp_r_sfp, bigdf$comp_m_sfp, bigdf$comp_all_sfp),
na.rm = TRUE)
comp_wtd_range <-
range(c(bigdf$comp_r_wtd, bigdf$comp_m_wtd, bigdf$comp_all_wtd),
na.rm = TRUE)
comp_drivetime_range <-
range(c(bigdf$comp_r_drivetime, bigdf$comp_m_drivetime, bigdf$comp_all_drivetime),
na.rm = TRUE)
comp_pct_range <-
range(c(bigdf$comp_r_pct, bigdf$comp_m_pct, bigdf$comp_all_pct),
na.rm = TRUE)
# Create sequences from column ranges. These will be used to create the equal
# interval classes.
ttm_avg_vals <-
seq(from = ttm_avg_range[1],
to = ttm_avg_range[2],
length.out = nrow(postal_f))
ttm_drivetime_vals <-
seq(from = ttm_drivetime_range[1],
to = ttm_drivetime_range[2],
length.out = nrow(postal_f))
ttm_pct_vals <-
seq(from = ttm_pct_range[1],
to = ttm_pct_range[2],
length.out = nrow(postal_f))
thesis_sfp_vals <-
seq(from = thesis_sfp_range[1],
to = thesis_sfp_range[2],
length.out = nrow(postal_f))
thesis_wtd_vals <-
seq(from = thesis_wtd_range[1],
to = thesis_wtd_range[2],
length.out = nrow(postal_f))
thesis_drivetime_vals <-
seq(from = thesis_drivetime_range[1],
to = thesis_drivetime_range[2],
length.out = nrow(postal_f))
thesis_pct_vals <-
seq(from = thesis_pct_range[1],
to = thesis_pct_range[2],
length.out = nrow(postal_f))
comp_sfp_vals <-
seq(from = comp_sfp_range[1],
to = comp_sfp_range[2],
length.out = nrow(postal_f))
comp_wtd_vals <-
seq(from = comp_wtd_range[1],
to = comp_wtd_range[2],
length.out = nrow(postal_f))
comp_drivetime_vals <-
seq(from = comp_drivetime_range[1],
to = comp_drivetime_range[2],
length.out = nrow(postal_f))
comp_pct_vals <-
seq(from = comp_pct_range[1],
to = comp_pct_range[2],
length.out = nrow(postal_f))
#### 4 Travel Time Comparison ShinyApp -----------------------------------------
server <- function(input, output, session) {
#### 4.1 Reactive elements ---------------------------------------------------
# Launch tooltip legend jQuery UI dialog
shiny::observeEvent(input$info_dialog_btn, {
# The div id='abbr-info' is loaded in "6.4 ShinyApp header", the div itself
# is the separate html file indicated in variable "info_path". Dialog
# window properties are located in .js.
# NB! This contains a brutish solution to the dialog content jumping straight
# to bottom on open. Wait 310 ms (tested to be approx. lowest duration for
# this to work) after opening dialog, then jQuery scrollTop()
shinyjs::runjs("$('#abbr-info').dialog('open');
setTimeout(function() {$('#abbr-info').scrollTop(0);}, 310);")
})
# This reactive object is created to enable listening of multiple inputs user
# can trigger. Triggering an input may require lowering the classes breaks
# inside the function CreateEqualColumn(), otherwise we will get an error
# ""
checkSliderInput <- reactive({
list(input$classIntervals_n, input$calcZip, input$fill_column)
})
shiny::observeEvent(checkSliderInput(), {
# Test classInt::classIntervals() and change classIntervals_n SliderInput
# value if statement holds. Suppress warnings in the test.
inputdf <- thisTTM()
classes_test <- suppressWarnings(
classInt::classIntervals(inputdf[, vis_cols[[input$fill_column]]],
n = input$classIntervals_n,
style = "equal"))
# Object returned from classIntervals() has an attribute "nobs" which I
# use to detect cases where too large input$classIntervals_n is inputted to
# CreateEqualColumn() function.
# The minus one should prevent the breaks error from showing.
if(attributes(classes_test)$nobs - 1 < input$classIntervals_n) {
updateSliderInput(session,
"classIntervals_n",
value = attributes(classes_test)$nobs - 1)
}
})
# Validate ykr-id in the numeric field "input$calcZip"
validate_zipcode <- shiny::eventReactive(input$calcZip, {
# %then% allows only one error message at a time
shiny::validate(
shiny::need(!is.na(as.numeric(input$zipcode)), "Can't contain letters") %then%
shiny::need(nchar(input$zipcode) == 5, "Five digits pls") %then%
shiny::need(input$zipcode %in% unique(ykrid_zipcodes$zipcode),
"Not a valid postal code")
)
input$zipcode
})
# Locked class intervals, option: parameters of the same type (r-m-all trios)
# and all postal code areas.
# Get correct locked classes value range out of this reactive object
locked_class_breaks_all <- shiny::reactive({
if(input$fill_column %in% c("ttm18_r_avg", "ttm18_m_avg", "ttm18_all_avg")) {
ttm_avg_vals
} else if (input$fill_column %in% c("msc_r_sfp", "msc_m_sfp", "msc_all_sfp")) {
thesis_sfp_vals
} else if (input$fill_column %in% c("msc_r_wtd", "msc_m_wtd", "msc_all_wtd")) {
thesis_wtd_vals
} else if (input$fill_column %in% c("ttm18_r_drivetime", "ttm18_m_drivetime",
"ttm18_all_drivetime")) {
ttm_drivetime_vals
} else if (input$fill_column %in% c("msc_r_drivetime", "msc_m_drivetime",
"msc_all_drivetime")) {
thesis_drivetime_vals
} else if (input$fill_column %in% c("ttm18_r_pct", "ttm18_m_pct",
"ttm18_all_pct")) {
ttm_pct_vals
} else if (input$fill_column %in% c("msc_r_pct", "msc_m_pct",
"msc_all_pct")) {
thesis_pct_vals
} else if (input$fill_column %in% c("compare_r_sfp", "compare_m_sfp",
"compare_all_sfp")) {
comp_sfp_vals
} else if (input$fill_column %in% c("compare_r_wtd", "compare_m_wtd",
"compare_all_wtd")) {
comp_wtd_vals
} else if (input$fill_column %in% c("compare_r_drivetime", "compare_m_drivetime",
"compare_all_drivetime")) {
comp_drivetime_vals
} else if (input$fill_column %in% c("compare_r_pct", "compare_m_pct",
"compare_all_pct")) {
comp_pct_vals
}
})
# Locked class intervals, option: parameters of the same type (r-m-all trios)
locked_class_breaks_params <- shiny::reactive({
# Get only the three columns which belong to the current trio of rush hour,
# midday, and all values. use the result of columnFinder() to get correct
# column names out of named vector "vis_cols"
inputdata <- thisTTM()
currentColumns <- columnFinder(input$fill_column, names(vis_cols))
thisDf <- inputdata[, vis_cols[currentColumns]]
# Calculate the value range from smallest value to largest and use
# "theseVals" in CreateEqualColumn() function
thisRange <- range(c(thisDf[, 1], thisDf[, 2], thisDf[, 3]), na.rm = TRUE)
theseVals <- seq(from = thisRange[1],
to = thisRange[2],
length.out = nrow(postal_f))
theseVals
})
# helper_output_zip() and helper_output_symbology(): Print helpful text for
# the user
helper_output_zip <- shiny::reactive({
inputdata <- thisTTM()
thisVal <- inputdata[inputdata$zipcode == validate_zipcode(), ][1, ]
help_output <- paste(
"<p class='helper-div'>",
"<b>Current origin postal code area:</b><br>",
"<i class='icon hourglass-start'></i>",
thisVal[["zipcode"]], " ", thisVal[["nimi"]],
"</p>", sep = "")
help_output
})
helper_output_symbology <- shiny::reactive({
thisVal <- GetSymbologyHelp(input$fill_column)
help_output <- paste(
"<div class='helper-div'>",
"<b>Current symbology selection key:</b><br>",
thisVal,
"</div>", sep = "")
help_output
})
#### Reactive fetch of aggregated TTM18 data
# This is the reactively built Helsinki Region Travel Time Matrix 2018 for the
# origin id inserted by the user. User's chosen YKR_ID value is
# validate_zipcode().
thisTTM <- shiny::reactive({
# Use validate_zipcode() to find the filepath for the needed aggregated
# TTM18 fst file. Reactive_TTM_fetch() then lifts the heavy load.
postal_loc <- grepl(validate_zipcode(), all_postal_fst, fixed = TRUE)
this_fp <- all_postal_fst[postal_loc]
result <- Reactive_TTM_fetch(this_fp, thesisdata, postal_f)
result
})
# equalIntervalsColumn() calculates new class intervals when an input change is
# detected on input$fill_column or amount of classes is changed in
# input$classIntervals_n.
# Also check the state of the locked_breaks dropdown menu, as different behaviour
# is required from CreateEqualColumn() based on the state.
equalIntervalsColumn <- reactive({
inputdata <- thisTTM()
# Check for locked classes dropdown menu state
if(input$locked_breaks == "off") {
# Normal behaviour
res <- CreateEqualColumn(inputdata,
vis_cols[[input$fill_column]],
input$fill_column,
input$classIntervals_n)
} else if (input$locked_breaks == "params") {
# User has selected locked classes: parameters
res <- CreateEqualColumn(inputdata,
vis_cols[[input$fill_column]],
input$fill_column,
input$classIntervals_n,
locked_class_breaks_params())
} else if (input$locked_breaks == "all") {
# User has selected locked classes: parameters and all postal code areas
res <- CreateEqualColumn(inputdata,
vis_cols[[input$fill_column]],
input$fill_column,
input$classIntervals_n,
locked_class_breaks_all())
}
res
})
#### 4.2 ShinyApp outputs ----------------------------------------------------
#### 4.2.1 Da plot -----------------------------------------------------------
output$researcharea <- renderGirafe({
# Reactive value: Insert equal intervals column for ggplot mapping.
inputdata <- equalIntervalsColumn()
# Get the origin zipcode for mapping
originzip <- postal_f[postal_f["zipcode"] == validate_zipcode(), ]
originname <- as.character(originzip$nimi[1])
# Format legend labels (Equal breaks classes). Remove [, ], (, and ). Also
# add list dash. Create named vector for the origin zipcode legend entry.
# \U2012 is endash.
l_labels <-
gsub("(])|(\\()|(\\[)", "", levels(inputdata[, input$fill_column])) %>%
gsub(",", " \U2012 ", .)
legendname <- GetLegendName(input$fill_column, originzip)
# The sum of individual factor levels. Check for locked classes menu state.
# If classes breaks are locked, additional data is needed to calculate
# frequency of class occurrence.
if(input$locked_breaks == "off") {
# Normal behaviour
l_labels <- AddLevelCounts(inputdata,
vis_cols[[input$fill_column]],
input$fill_column,
input$classIntervals_n,
l_labels)
} else if (input$locked_breaks == "params") {
# Classes are locked, option: parameters
l_labels <- AddLevelCounts(inputdata,
vis_cols[[input$fill_column]],
input$fill_column,
input$classIntervals_n,
l_labels,
locked_class_breaks_params())
} else if (input$locked_breaks == "all") {
# Classes are locked, option: parameters and all postal code areas
l_labels <- AddLevelCounts(inputdata,
vis_cols[[input$fill_column]],
input$fill_column,
input$classIntervals_n,
l_labels,
locked_class_breaks_all())
}
# Add two decimal places to all label values. Do this by disassembling
# "l_labels" and determining which values need decimals added.
l_labels <- stringr::str_extract_all(l_labels, "\\(?[-0-9,.]+\\)?", simplify = TRUE) %>%
as.data.frame()
l_labels <- apply(l_labels, 1, function(x) {
paste(sprintf("%0.2f", as.numeric(x[1])), " \U2012 ",
sprintf("%0.2f", as.numeric(x[2])), " [", x[3], "]",
sep = "")
})
# Origin id legend label
o_label <- setNames("purple",
originzip[, "nimi"] %>%
unique() %>%
as.character())
# current_subdiv_lbl is created so that any amount of values can be removed
# when necessary from the dataframe. By using a duplicate of subdiv_lbl,
# labels can also be returned into view.
current_subdiv_lbl <- data.frame(subdiv_lbl)
# Get the tooltip from a separate HTML file. Get rid of indentation and
# HTML comments in the function ReadAndClean().
tooltip_content <- ReadAndClean(tooltip_path)
#### 4.2.1.1 Define ggplot obligatory elements ----
g <- ggplot(data = inputdata) +
geom_polygon_interactive(
color = alpha("black", input$postal_vis),
size = 0.3,
aes_string("long", "lat",
group = "group",
fill = input$fill_column,
tooltip = substitute(
sprintf(tooltip_content,
from_zip, originname,
zipcode, nimi,
ttm_sfp, ttm_sfp, ttm_sfp,
ttm_wtd, ttm_wtd, ttm_wtd,
ttm_r_avg, ttm_m_avg, ttm_all_avg,
ttm_r_drivetime, ttm_m_drivetime, ttm_all_drivetime,
ttm_r_pct, ttm_m_pct, ttm_all_pct,
zipcode, vals_in_zip,
thesis_r_sfp, thesis_m_sfp, thesis_all_sfp,
thesis_r_wtd, thesis_m_wtd, thesis_all_wtd,
thesis_r_drivetime, thesis_m_drivetime, thesis_all_drivetime,
thesis_r_pct, thesis_m_pct, thesis_all_pct,
comp_r_sfp, comp_m_sfp, comp_all_sfp,
comp_r_wtd, comp_m_wtd, comp_all_wtd,
comp_r_drivetime, comp_m_drivetime, comp_all_drivetime,
comp_r_pct, comp_m_pct, comp_all_pct))
)) +
# Equal interval classes colouring and labels. drop = FALSE is very
# important in most of the cases of input$fill_column. Levels may end up
# appearing zero times in the data, and that would get them erased from
# the legend and mix everything up. Get actual colouring from user in
# input$brewerpal
scale_fill_brewer(palette = input$brewerpal,
name = paste0(legendname, collapse = ""),
direction = -1,
labels = l_labels,
na.value = "darkgrey",
drop = FALSE) +
#### Origin postal code area element and legend entry
# Plot origin YKR_ID, the starting position for TTM18
geom_polygon(data = originzip,
aes(long, lat, group = group, color = nimi),
fill = NA,
size = 1.2) +
# Get a legend entry for origin ykr id
scale_color_manual(name = "Origin postal\ncode area",
values = o_label,
labels = names(o_label)) +
# Define map extent manually
coord_fixed(xlim = c(min(inputdata$lon) + 1500, max(inputdata$lon) - 1500),
ylim = c(min(inputdata$lat) + 1300, max(inputdata$lat) - 1300)) +
# Scale bar and north arrow. Because we defined visible map extent above,
# manually set scalebar location.
ggsn::scalebar(data = inputdata,
dist_unit = "km",
dist = 2,
st.dist = 0.01,
st.size = 4.75,
height = 0.01,
transform = FALSE,
anchor = c(
x = max(inputdata$lon) - 1000,
y = min(inputdata$lat) + 400)) +
ggsn::north(inputdata,
location = "topright",
scale = 0.04,
symbol = 10) +
# Legend settings. Remove axis titles, text, and ticks.
theme(legend.title = element_text(size = 15),
legend.text = element_text(size = 14),
axis.title = element_blank(),
axis.text = element_blank(),
axis.ticks = element_blank())
#### 4.2.1.2 If statements for on-off switches ----
# Roads and water in case we want them mapped
if(input$show_water == TRUE) {
g <- g + ggspatial::geom_spatial_polygon(
data = water,
crs = 3067,
aes(long, lat, group = group),
color = alpha("blue", 0.9),
fill = "lightblue",
size = 0.4)
}
if(input$show_roads == TRUE) {
g <- g + geom_path(
data = roads_f,
aes(long, lat, group = group),
color = "#757575",
size = 0.9)
}
# Plot municipality boundaries on the map
if(input$show_muns == TRUE) {
# Add boundaries of neighboring municipalities for visual reference
g <- g + geom_polygon(data = othermuns_f,
aes(long, lat, group = group),
color = alpha("black", 0.3),
fill = "NA",
size = 0.8) +
# Study area municipality boundaries
geom_polygon(data = muns_f,
aes(long, lat, group = group),
color = alpha("black", 0.9),
fill = "NA",
size = 1.0)
}
# Plot municipality subdivision boundaries on the map
if(input$show_subdiv == TRUE) {
# Municipality boundaries
g <- g + geom_polygon(data = subdiv_f,
aes(long, lat, group = group),
color = alpha("black", 0.6),
fill = "NA",
size = 0.6)
}
# Plot walking center boundaries
if(input$show_walk == TRUE) {
# ggnewscale makes it possible to map additional legends with same
# properties, in this case a new scale_color (origin postal code area
# legend entry is already occupying the slot).
# New legend entry for walking center of Helsinki
g <- g + ggnewscale::new_scale_color() +
geom_polygon(data = walk_f,
aes(long, lat, color = label),
fill = NA,
linetype = "longdash",
key_glyph = "polygon",
size = 0.9) +
scale_color_manual(name = NULL,
values = setNames("#6b01ab", "walk"),
labels = "Helsinki walking\ncenter (TTM18)") +
# Modify legend symbol: rectangle to square by making the symbol larger
guides(colour = guide_legend(override.aes = list(size = 12)))
}
# Plot postal code area labels
if(input$postal_label_choice != "Off") {
if(input$postal_label_choice == "Current symbology") {
# Fetch current symbology values. For the join there has to be a column
# of same name in both dataframes, therefore mutate() one into thisTTM.
this_zipcode_lbl <-
dplyr::left_join(zipcode_lbl,
thisTTM() %>%
dplyr::mutate(label = as.character(zipcode)) %>%
dplyr::select(label, zipcode,
!!rlang::sym(vis_cols[[input$fill_column]])),
by = "label") %>%
dplyr::mutate(label = !!rlang::sym(vis_cols[[input$fill_column]])) %>%
dplyr::distinct(zipcode, .keep_all = TRUE) %>%
dplyr::select(c(long, lat, label))
# Remove rows that have NAs in "label", otherwise they are printed on
# the map
this_zipcode_lbl <- this_zipcode_lbl[!is.na(this_zipcode_lbl$label), ]
# We need to generate this annotation separately for "Current symbology"
# and "postal code areas" because we can't force two decimal places for
# postal codes.
g <- g + with(this_zipcode_lbl,
annotate(geom = "label",
x = long,
y = lat,
label = sprintf("%0.2f", label),
label.size = NA,
fill = alpha("white", 0.5),
size = 4))
} else {
# if "Postal codes", use the object "zipcode_lbl" produced in 2.7.
this_zipcode_lbl <- zipcode_lbl
# Add zipcode labels
g <- g + with(this_zipcode_lbl,
annotate(geom = "label",
x = long,
y = lat,
label = label,
label.size = NA,
fill = alpha("white", 0.5),
size = 4))
}
}
# Plot postal code area labels
if(input$show_muns_labels == TRUE) {
# Disable Kauniainen label on subdiv when muns labels visible
if(input$show_subdiv_labels == TRUE) {
current_subdiv_lbl["Kauniainen", "label"] <- NA
}
# Add zipcode labels