www.tuhocr.comĐể vẽ đồ thị hai trục tung thì cách tiếp cận trong base-R graphics là vẽ lần lượt từng đồ thị gán lên nhau (painter mode).
airquality## Ozone Solar.R Wind Temp Month Day
## 1 41 190 7.4 67 5 1
## 2 36 118 8.0 72 5 2
## 3 12 149 12.6 74 5 3
## 4 18 313 11.5 62 5 4
## 5 NA NA 14.3 56 5 5
## 6 28 NA 14.9 66 5 6
## 7 23 299 8.6 65 5 7
## 8 19 99 13.8 59 5 8
## 9 8 19 20.1 61 5 9
## 10 NA 194 8.6 69 5 10
## 11 7 NA 6.9 74 5 11
## 12 16 256 9.7 69 5 12
## 13 11 290 9.2 66 5 13
## 14 14 274 10.9 68 5 14
## 15 18 65 13.2 58 5 15
## 16 14 334 11.5 64 5 16
## 17 34 307 12.0 66 5 17
## 18 6 78 18.4 57 5 18
## 19 30 322 11.5 68 5 19
## 20 11 44 9.7 62 5 20
## 21 1 8 9.7 59 5 21
## 22 11 320 16.6 73 5 22
## 23 4 25 9.7 61 5 23
## 24 32 92 12.0 61 5 24
## 25 NA 66 16.6 57 5 25
## 26 NA 266 14.9 58 5 26
## 27 NA NA 8.0 57 5 27
## 28 23 13 12.0 67 5 28
## 29 45 252 14.9 81 5 29
## 30 115 223 5.7 79 5 30
## 31 37 279 7.4 76 5 31
## 32 NA 286 8.6 78 6 1
## 33 NA 287 9.7 74 6 2
## 34 NA 242 16.1 67 6 3
## 35 NA 186 9.2 84 6 4
## 36 NA 220 8.6 85 6 5
## 37 NA 264 14.3 79 6 6
## 38 29 127 9.7 82 6 7
## 39 NA 273 6.9 87 6 8
## 40 71 291 13.8 90 6 9
## 41 39 323 11.5 87 6 10
## 42 NA 259 10.9 93 6 11
## 43 NA 250 9.2 92 6 12
## 44 23 148 8.0 82 6 13
## 45 NA 332 13.8 80 6 14
## 46 NA 322 11.5 79 6 15
## 47 21 191 14.9 77 6 16
## 48 37 284 20.7 72 6 17
## 49 20 37 9.2 65 6 18
## 50 12 120 11.5 73 6 19
## 51 13 137 10.3 76 6 20
## 52 NA 150 6.3 77 6 21
## 53 NA 59 1.7 76 6 22
## 54 NA 91 4.6 76 6 23
## 55 NA 250 6.3 76 6 24
## 56 NA 135 8.0 75 6 25
## 57 NA 127 8.0 78 6 26
## 58 NA 47 10.3 73 6 27
## 59 NA 98 11.5 80 6 28
## 60 NA 31 14.9 77 6 29
## 61 NA 138 8.0 83 6 30
## 62 135 269 4.1 84 7 1
## 63 49 248 9.2 85 7 2
## 64 32 236 9.2 81 7 3
## 65 NA 101 10.9 84 7 4
## 66 64 175 4.6 83 7 5
## 67 40 314 10.9 83 7 6
## 68 77 276 5.1 88 7 7
## 69 97 267 6.3 92 7 8
## 70 97 272 5.7 92 7 9
## 71 85 175 7.4 89 7 10
## 72 NA 139 8.6 82 7 11
## 73 10 264 14.3 73 7 12
## 74 27 175 14.9 81 7 13
## 75 NA 291 14.9 91 7 14
## 76 7 48 14.3 80 7 15
## 77 48 260 6.9 81 7 16
## 78 35 274 10.3 82 7 17
## 79 61 285 6.3 84 7 18
## 80 79 187 5.1 87 7 19
## 81 63 220 11.5 85 7 20
## 82 16 7 6.9 74 7 21
## 83 NA 258 9.7 81 7 22
## 84 NA 295 11.5 82 7 23
## 85 80 294 8.6 86 7 24
## 86 108 223 8.0 85 7 25
## 87 20 81 8.6 82 7 26
## 88 52 82 12.0 86 7 27
## 89 82 213 7.4 88 7 28
## 90 50 275 7.4 86 7 29
## 91 64 253 7.4 83 7 30
## 92 59 254 9.2 81 7 31
## 93 39 83 6.9 81 8 1
## 94 9 24 13.8 81 8 2
## 95 16 77 7.4 82 8 3
## 96 78 NA 6.9 86 8 4
## 97 35 NA 7.4 85 8 5
## 98 66 NA 4.6 87 8 6
## 99 122 255 4.0 89 8 7
## 100 89 229 10.3 90 8 8
## 101 110 207 8.0 90 8 9
## 102 NA 222 8.6 92 8 10
## 103 NA 137 11.5 86 8 11
## 104 44 192 11.5 86 8 12
## 105 28 273 11.5 82 8 13
## 106 65 157 9.7 80 8 14
## 107 NA 64 11.5 79 8 15
## 108 22 71 10.3 77 8 16
## 109 59 51 6.3 79 8 17
## 110 23 115 7.4 76 8 18
## 111 31 244 10.9 78 8 19
## 112 44 190 10.3 78 8 20
## 113 21 259 15.5 77 8 21
## 114 9 36 14.3 72 8 22
## 115 NA 255 12.6 75 8 23
## 116 45 212 9.7 79 8 24
## 117 168 238 3.4 81 8 25
## 118 73 215 8.0 86 8 26
## 119 NA 153 5.7 88 8 27
## 120 76 203 9.7 97 8 28
## 121 118 225 2.3 94 8 29
## 122 84 237 6.3 96 8 30
## 123 85 188 6.3 94 8 31
## 124 96 167 6.9 91 9 1
## 125 78 197 5.1 92 9 2
## 126 73 183 2.8 93 9 3
## 127 91 189 4.6 93 9 4
## 128 47 95 7.4 87 9 5
## 129 32 92 15.5 84 9 6
## 130 20 252 10.9 80 9 7
## 131 23 220 10.3 78 9 8
## 132 21 230 10.9 75 9 9
## 133 24 259 9.7 73 9 10
## 134 44 236 14.9 81 9 11
## 135 21 259 15.5 76 9 12
## 136 28 238 6.3 77 9 13
## 137 9 24 10.9 71 9 14
## 138 13 112 11.5 71 9 15
## 139 46 237 6.9 78 9 16
## 140 18 224 13.8 67 9 17
## 141 13 27 10.3 76 9 18
## 142 24 238 10.3 68 9 19
## 143 16 201 8.0 82 9 20
## 144 13 238 12.6 64 9 21
## 145 23 14 9.2 71 9 22
## 146 36 139 10.3 81 9 23
## 147 7 49 10.3 69 9 24
## 148 14 20 16.6 63 9 25
## 149 30 193 6.9 70 9 26
## 150 NA 145 13.2 77 9 27
## 151 14 191 14.3 75 9 28
## 152 18 131 8.0 76 9 29
## 153 20 223 11.5 68 9 30
str(airquality)## 'data.frame': 153 obs. of 6 variables:
## $ Ozone : int 41 36 12 18 NA 28 23 19 8 NA ...
## $ Solar.R: int 190 118 149 313 NA NA 299 99 19 194 ...
## $ Wind : num 7.4 8 12.6 11.5 14.3 14.9 8.6 13.8 20.1 8.6 ...
## $ Temp : int 67 72 74 62 56 66 65 59 61 69 ...
## $ Month : int 5 5 5 5 5 5 5 5 5 5 ...
## $ Day : int 1 2 3 4 5 6 7 8 9 10 ...
Vẽ hai đường Ozone và Temp trong
tháng 5 ở dataset airquality.
month_5 <- subset(airquality, Month == 5)
par(mar = c(5, 4, 4, 4) + 0.5)
plot(Ozone ~ Day, na.omit(month_5[, c("Ozone", "Day")]),
type = "l", lwd = 2, las = 1, col = "blue",
xaxs = "i", yaxs = "i",
xaxt = "n", yaxt = "n",
ylim = c(0, round(max(airquality$Ozone, na.rm = TRUE), digits = -2)),
xlim = c(0, max(month_5$Day, na.rm = TRUE)),
xlab = "Ngày", ylab = ""
)
title(
main = "Chỉ số quan trắc môi trường tháng 05/2022",
col.main = "darkgreen", line = 2
)
mysubtitle <- "Đồ thị được code bởi Duc Nguyen | www.tuhocr.com"
mtext(line = 0.4, mysubtitle, col = "darkgreen")
y1 <- par("usr")
# Changing x axis
lastnum_seq_x <- function(start, end, by) {
by <- seq(from = start, to = end, by = by)
e <- c(by, end)
if (e[length(e)] - e[length(e) - 1] < 3) {
e_1 <- c(e[1:c(length(e) - 2)], e[length(e)])
e_2 <- append(e_1, 1, after = 1) # insert element
return(e_2)
}
e_3 <- c(by, end)
e_4 <- append(e_3, 1, after = 1)
return(e_4)
}
xtick <- lastnum_seq_x(0, max(month_5$Day, na.rm = TRUE), 5)
axis(side = 1, at = xtick, labels = FALSE)
text(
x = xtick, y1[3] - 2,
labels = xtick, srt = 0, pos = 1, xpd = TRUE
)
# Changing y axis
lastnum_seq_y <- function(start, end, by) {
by <- seq(from = start, to = end, by = by)
e <- c(by, end)
if (e[length(e)] - e[length(e) - 1] < 9) {
e_1 <- c(e[1:c(length(e) - 2)], e[length(e)])
e_2 <- append(e_1, 1, after = 1) # insert element
return(e_1)
}
e_3 <- c(by, end)
e_4 <- append(e_3, 1, after = 1)
return(e_3)
}
ytick <- lastnum_seq_y(0, round(max(airquality$Ozone, na.rm = TRUE), digits = -2), 20)
axis(side = 2, at = ytick, labels = FALSE)
text(y1[1] - 0.2, ytick,
labels = ytick, srt = 0, pos = 2, xpd = TRUE, col = "blue"
)
text(y1[1] - 3, mean(y1[3:4]),
labels = "Ozone (ppb)", srt = 90,
# pos = 2,
xpd = NA, col = "blue"
)
# text(mean(y1[1:2]), y1[3] - 13,
# labels = "Ngày", srt = 0,
# # pos = 1,
# xpd = NA, col = "black"
# )
## Vẽ line chart thứ 2
par(new = TRUE)
plot(Temp ~ Day, na.omit(month_5[, c("Temp", "Day")]),
type = "b", axes = FALSE, ylab = "", lty = 1, lwd = 2, col = "red",
xaxs = "i", yaxs = "i",
ylim = c(0, round(max(airquality$Temp, na.rm = TRUE), digits = -2)),
xlim = c(0, max(month_5$Day, na.rm = TRUE)),
xlab = ""
)
axis(side = 1, at = xtick, labels = FALSE) # trục x của plot 2 được ẩn
# Changing y_2 axis
ytick_2 <- lastnum_seq_y(0, round(max(airquality$Temp, na.rm = TRUE), digits = -2) , 20)
y2 <- par("usr")
axis(side = 4, at = ytick_2, labels = FALSE)
text(y2[2] + 0.2, ytick_2,
labels = ytick_2, srt = 0, pos = 4, xpd = TRUE, col = "red"
)
# mtext(side = 4, line = 2, "Temp")
text(y2[2] + 3, mean(y2[3:4]),
labels = "Nhiệt độ (°F)",
xpd = NA, srt = -90, col = "red"
)
legend("topleft",
legend = c("Nhiệt độ (°F)", "Ozone (ppb)"),
col = c("red", "blue"),
lty = c(2, 1), cex = 1,
pch = c(21, NA),
lwd = 2,
x.intersp = 1,
y.intersp = 2,
inset = 0.02,
box.lty = 0,
horiz = FALSE
)## Function vẽ đồ thị hai trục tung
ve_hai_truc_tung <- function(month_input,
main_title = "Chỉ số quan trắc môi trường",
sub_title = "Đồ thị được code bởi Duc Nguyen | www.tuhocr.com",
label_y1 = 3,
label_y2 = 3,
legend_show = "1",
add_one = "1"){
month_5 <- subset(airquality, Month == month_input)
par(mar = c(5, 4, 4, 4) + 0.5)
plot(Ozone ~ Day, na.omit(month_5[, c("Ozone", "Day")]),
type = "l", lwd = 2, las = 1, col = "blue",
xaxs = "i", yaxs = "i",
xaxt = "n", yaxt = "n",
ylim = c(0, round(max(airquality$Ozone, na.rm = TRUE), digits = -2)),
xlim = c(0, max(month_5$Day, na.rm = TRUE)),
xlab = "Ngày", ylab = ""
)
title(main = paste(main_title, "tháng", paste0("0", month_input, "/2022")),
col.main = "darkgreen", line = 2)
mysubtitle <- sub_title
mtext(line = 0.4, mysubtitle, col = "darkgreen")
y1 <- par("usr")
# Changing x axis
lastnum_seq_x <- function(start, end, by) {
by <- seq(from = start, to = end, by = by)
e <- c(by, end)
if (e[length(e)] - e[length(e) - 1] < 3) {
e_1 <- c(e[1:c(length(e) - 2)], e[length(e)])
if(add_one == "1"){
e_2 <- append(e_1, 1, after = 1) # insert element
return(e_2)
}
return(e_1)
}
e_3 <- c(by, end)
e_4 <- append(e_3, 1, after = 1)
return(e_4)
}
xtick <- lastnum_seq_x(0, max(month_5$Day, na.rm = TRUE), 5)
axis(side = 1, at = xtick, labels = FALSE)
text(
x = xtick, y1[3] - 2,
labels = xtick, srt = 0, pos = 1, xpd = TRUE
)
# Changing y axis
lastnum_seq_y <- function(start, end, by) {
by <- seq(from = start, to = end, by = by)
e <- c(by, end)
if (e[length(e)] - e[length(e) - 1] < 9) {
e_1 <- c(e[1:c(length(e) - 2)], e[length(e)])
e_2 <- append(e_1, 1, after = 1) # insert element
return(e_1)
}
e_3 <- c(by, end)
e_4 <- append(e_3, 1, after = 1)
return(e_3)
}
ytick <- lastnum_seq_y(0, round(max(airquality$Ozone, na.rm = TRUE), digits = -2), 20)
axis(side = 2, at = ytick, labels = FALSE)
text(y1[1] - 0.2, ytick,
labels = ytick, srt = 0, pos = 2, xpd = TRUE, col = "blue"
)
text(y1[1] - label_y1, mean(y1[3:4]),
labels = "Ozone (ppb)", srt = 90,
# pos = 2,
xpd = NA, col = "blue"
)
# text(mean(y1[1:2]), y1[3] - 13,
# labels = "Ngày", srt = 0, xpd = NA, col = "black")
## Vẽ line chart thứ 2
par(new = TRUE)
plot(Temp ~ Day, na.omit(month_5[, c("Temp", "Day")]),
type = "b", axes = FALSE, ylab = "", lty = 1, lwd = 2, col = "red",
xaxs = "i", yaxs = "i",
ylim = c(0, round(max(airquality$Temp, na.rm = TRUE), digits = -2)),
xlim = c(0, max(month_5$Day, na.rm = TRUE)),
xlab = ""
)
axis(side = 1, at = xtick, labels = FALSE) # trục x của plot 2 được ẩn
# Changing y_2 axis
ytick_2 <- lastnum_seq_y(0, round(max(airquality$Temp, na.rm = TRUE), digits = -2), 20)
y2 <- par("usr")
axis(side = 4, at = ytick_2, labels = FALSE)
text(y2[2] + 0.2, ytick_2,
labels = ytick_2, srt = 0, pos = 4, xpd = TRUE, col = "red"
)
# mtext(side = 4, line = 2, "Temp")
text(y2[2] + label_y2, mean(y2[3:4]),
labels = "Nhiệt độ (°F)",
xpd = NA, srt = -90, col = "red"
)
if(legend_show == 1){
legend("topleft",
legend = c("Nhiệt độ (°F)", "Ozone (ppb)"),
col = c("red", "blue"),
lty = c(2, 1), cex = 1,
pch = c(21, NA),
lwd = 2,
x.intersp = 1,
y.intersp = 2,
inset = 0.4,
box.lty = 0,
horiz = FALSE
)
}
}Đồ thị tháng 6
ve_hai_truc_tung(month_input = 6)Đồ thị tháng 7
ve_hai_truc_tung(month_input = 7)Đồ thị tháng 8
ve_hai_truc_tung(month_input = 8)Đồ thị tháng 9
ve_hai_truc_tung(month_input = 9)par(mfrow = c(2, 3))
ve_hai_truc_tung(month_input = 5, "", "", label_y1 = 8, label_y2 = 8, legend_show = 0, add_one = 0)
ve_hai_truc_tung(month_input = 6, "", "", label_y1 = 8, label_y2 = 8, legend_show = 0, add_one = 0)
ve_hai_truc_tung(month_input = 7, "", "", label_y1 = 8, label_y2 = 8, legend_show = 0, add_one = 0)
ve_hai_truc_tung(month_input = 8, "", "", label_y1 = 8, label_y2 = 8, legend_show = 0, add_one = 0)
ve_hai_truc_tung(month_input = 9, "", "", label_y1 = 8, label_y2 = 8, legend_show = 0, add_one = 0)
plot.new()
title(
main = "Kết quả quan trắc \nmôi trường năm 2022",
col.main = "darkgreen", line = -3, cex.main = 1.8
)
# mtext("Kết quả quan trắc môi trường năm 2022 | tuhocr.com",
# side = 3,
# line = -30,
# outer = TRUE)
# plot(1:10,ty="n")
library(png) ## dùng đề chèn file ảnh
library(grid) ## canh chỉnh vị trí ảnh
image_tuhocr <- readPNG("img/logor.png")
# rasterImage(image_tuhocr,5,5,7,7)
grid.raster(image_tuhocr, x = 0.88, y = 0.15, width = 0.15)
### BẢNG CHÚ THÍCH
legend("top",
legend = c("Nhiệt độ (°F)", "Ozone (ppb)"),
col = c("red", "blue"),
lty = c(2, 1), cex = 1.3,
pch = c(21, NA),
lwd = 2,
x.intersp = 1.2,
y.intersp = 1.2,
inset = 0.24,
box.lty = 1,
horiz = FALSE,
bg = "yellow",
title = as.expression(bquote(bold("www.tuhocr.com"))))Ta sẽ tính trung bình của 2 cột Ozone và
Temp theo từng tháng, sau đó vẽ đồ thị cột side-by-side để
thuận tiện đánh giá kết quả chỉ tiêu môi trường.
## Vẽ barchart hai trục y cần tinh chỉnh một chút dataset. Các bạn theo dõi dòng code nhé.
x <- tapply(airquality$Temp, airquality$Month, mean, na.rm = TRUE)
y <- tapply(airquality$Ozone, airquality$Month, mean, na.rm = TRUE)
sum_data <- data.frame("Ozone" = y, "Temp" = x)
sum_data <- as.matrix(sum_data)
sum_data ## Chuyển thành matrix để vẽ barchart## Ozone Temp
## 5 23.61538 65.54839
## 6 29.44444 79.10000
## 7 59.11538 83.90323
## 8 59.96154 83.96774
## 9 31.44828 76.90000
## tính SD để vẽ error bar
x_sd <- tapply(airquality$Temp, airquality$Month, sd, na.rm = TRUE)
y_sd <- tapply(airquality$Ozone, airquality$Month, sd, na.rm = TRUE)
sum_sd <- data.frame("Ozone" = y_sd, "Temp" = x_sd)
sum_sd <- as.matrix(sum_sd)
sum_sd ## Chuyển thành matrix để vẽ barchart## Ozone Temp
## 5 22.22445 6.854870
## 6 18.20790 6.598589
## 7 31.63584 4.315513
## 8 39.68121 6.585256
## 9 24.14182 8.355671
sum_data_1 <- t(sum_data)
sum_data_1[2, ] <- NA
sum_data_2 <- t(sum_data)
sum_data_2[1, ] <- NA
sum_sd_1 <- t(sum_sd)
sum_sd_1[2, ] <- NA
sum_sd_2 <- t(sum_sd)
sum_sd_2[1, ] <- NA
##
error.bar <- function(x, y, upper, lower = upper, length = 0.035, ...) {
arrows(x , y + upper, x, y - lower, angle = 90, code = 3, length = length, ...)
}
##
par(mar = c(5, 4, 4, 4) + 0.5)
ok_1 <- barplot(sum_data_1, col = "#89cff0",
# xaxs = "i",
yaxs = "i",
# xaxt="n",
yaxt="n",
ylim = c(0, round(max(sum_data_1[1, ]), digits = 0) + 70),
# xlim = c(0, max(month_5$Day, na.rm = TRUE)),
xlab = "Tháng", ylab = "", beside = TRUE)
error.bar(ok_1, sum_data_1, sum_sd_1)
y3 <- par('usr')
lastnum_seq_y <- function(start, end, by){
by <- seq(from = start, to = end, by = by)
e <- c(by, end)
if(e[length(e)] - e[length(e) - 1] < 9){
e_1 <- c(e[1:c(length(e) - 2)], e[length(e)])
e_2 <- append(e_1, 1, after = 1) # insert element
return(e_1)
}
e_3 <- c(by, end)
e_4 <- append(e_3, 1, after = 1)
return(e_3)
}
ytick <- lastnum_seq_y(0, round(max(sum_data_1[1, ]), digits = 0) + 70, 10)
axis(side = 2, at = ytick, labels = FALSE)
text(y3[1] - 0.2 , ytick,
labels = ytick, srt = 0, pos = 2, xpd = TRUE, col = "blue")
text(y3[1] - 1.5, mean(y3[3:4]),
labels = "Ozone (ppb)", srt = 90,
# pos = 2,
xpd = NA, col = "blue")
# text(mean(y3[1:2]), y3[3] - 8,
# labels = "Tháng", srt = 0,
# # pos = 1,
# xpd = NA, col = "black")
par(new = TRUE)
ok_2 <- barplot(sum_data_2, col = "#fe6f5e",
# xaxs = "i",
yaxs = "i",
xaxt = "n",
yaxt = "n",
# ylim = c(0, 500),
ylim = c(0, round(max(sum_data_2[2, ]), digits = -2) + 20),
# xlim = c(0, max(month_5$Day, na.rm = TRUE)),
xlab = "", ylab = "", beside = TRUE)
error.bar(ok_2, sum_data_2, sum_sd_2)
# ytick_2 <- lastnum_seq_y(0, 100, 10)
ytick_2 <- lastnum_seq_y(0, round(max(sum_data_2[2, ]), digits = -2) + 20, 10)
y4 <- par('usr')
axis(side = 4, at = ytick_2, labels = FALSE)
text(y4[2] + 0.2, ytick_2,
labels = ytick_2, srt = 0, pos = 4, xpd = TRUE, col = "red")
text(y4[2] + 1.5, mean(y2[3:4]), labels = "Nhiệt độ (°F)",
xpd = NA, srt = -90, col = "red")
legend("topright",
legend = c("Ozone (ppb)", "Nhiệt độ (°F)"),
fill = c("#89cff0", "#fe6f5e"),
# lty = c(1, 2), cex = 1,
# pch = c(NA, 21),
# lwd = 2,
x.intersp = 1,
y.intersp = 2,
inset = 0.001,
box.lty = 0,
horiz = TRUE)
box()
# abline(h = 120, lwd = 1)
# abline(h = 0, lwd = 1)
title(main = "Chỉ số quan trắc môi trường theo từng tháng trong năm 2022")
mysubtitle <- "Đồ thị được code bởi Duc Nguyen | www.tuhocr.com"
mtext(line = 0.25, mysubtitle)
# library(png) ## dùng đề chèn file ảnh
# library(grid) ## canh chỉnh vị trí ảnh
image_tuhocr_package <- readPNG("img/tuhocr.png")
# # rasterImage(image_tuhocr,5,5,7,7)
grid.raster(image_tuhocr_package, x = 0.23, y = 0.76, width = 0.2)Crawley, M. J. (2013). The R book (2nd edition). Wiley. (page 952)
http://www.sthda.com/english/wiki/add-legends-to-plots-in-r-software-the-easiest-way
https://stackoverflow.com/questions/49263821/generate-a-sequence-and-include-the-last-number-in-r
Trên đây là hướng dẫn vẽ đồ thị hai trục tung. Để học R bài bản từ A đến Z, thân mời Bạn tham gia khóa học “HDSD R để xử lý dữ liệu” để có nền tảng vững chắc về R nhằm tự tay làm các câu chuyện dữ liệu của riêng mình!
ĐĂNG KÝ NGAY:
https://www.tuhocr.com/register
Hướng dẫn cài đặt package tuhocr
https://tuhocr.github.io/