├── .github ├── .gitignore ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── workflows │ ├── pkgdown.yaml │ ├── test-coverage.yaml │ ├── R-CMD-check.yaml │ └── pr-commands.yaml ├── CONTRIBUTING.md └── CODE_OF_CONDUCT.md ├── vignettes ├── .gitignore ├── bits_raw.png ├── bits_reduced.png ├── bits_half_reduced.png ├── bit-figures.R ├── ggip.Rmd └── visualizing-ip-data.Rmd ├── src ├── .gitignore ├── curves.h ├── mapping.h ├── curves.cpp ├── mapping.cpp ├── address_to_pixel.cpp ├── RcppExports.cpp └── network_to_bbox.cpp ├── tests ├── testthat │ ├── .gitignore │ ├── test-coord-ip.R │ ├── test-theme-ip.R │ ├── test-vctrs-coords.R │ ├── _snaps │ │ ├── geom-hilbert-outline.md │ │ ├── stat-summary-address.md │ │ └── ip_to_cartesian.md │ ├── test-stat-summary-address.R │ ├── test-geom-hilbert-outline.R │ └── test-ip_to_cartesian.R └── testthat.R ├── .gitignore ├── LICENSE ├── man ├── figures │ ├── logo.png │ ├── README-ipv4-heatmap-1.png │ ├── logo.R │ └── logo.svg ├── theme_ip.Rd ├── ggip-package.Rd ├── ip_to_cartesian.Rd ├── coord_ip.Rd ├── geom_hilbert_outline.Rd └── stat_summary_address.Rd ├── pkgdown ├── favicon │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── apple-touch-icon.png │ ├── apple-touch-icon-60x60.png │ ├── apple-touch-icon-76x76.png │ ├── apple-touch-icon-120x120.png │ ├── apple-touch-icon-152x152.png │ └── apple-touch-icon-180x180.png └── _pkgdown.yml ├── R ├── ggip-package.R ├── RcppExports.R ├── utils.R ├── theme-ip.R ├── vctrs-coords.R ├── ip_to_cartesian.R ├── coord-ip.R ├── stat-summary-address.R └── geom-hilbert-outline.R ├── codecov.yml ├── .Rbuildignore ├── ggip.Rproj ├── cran-comments.md ├── LICENSE.md ├── NAMESPACE ├── DESCRIPTION ├── NEWS.md ├── README.md └── README.Rmd /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /vignettes/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.so 3 | *.dll 4 | -------------------------------------------------------------------------------- /tests/testthat/.gitignore: -------------------------------------------------------------------------------- 1 | Rplots.pdf 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | docs 3 | inst/doc 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2020 2 | COPYRIGHT HOLDER: David Hall 3 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library(testthat) 2 | library(ggip) 3 | 4 | test_check("ggip") 5 | -------------------------------------------------------------------------------- /man/figures/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchall/ggip/HEAD/man/figures/logo.png -------------------------------------------------------------------------------- /vignettes/bits_raw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchall/ggip/HEAD/vignettes/bits_raw.png -------------------------------------------------------------------------------- /vignettes/bits_reduced.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchall/ggip/HEAD/vignettes/bits_reduced.png -------------------------------------------------------------------------------- /pkgdown/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchall/ggip/HEAD/pkgdown/favicon/favicon.ico -------------------------------------------------------------------------------- /vignettes/bits_half_reduced.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchall/ggip/HEAD/vignettes/bits_half_reduced.png -------------------------------------------------------------------------------- /pkgdown/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchall/ggip/HEAD/pkgdown/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /pkgdown/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchall/ggip/HEAD/pkgdown/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchall/ggip/HEAD/pkgdown/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /man/figures/README-ipv4-heatmap-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchall/ggip/HEAD/man/figures/README-ipv4-heatmap-1.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchall/ggip/HEAD/pkgdown/favicon/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchall/ggip/HEAD/pkgdown/favicon/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchall/ggip/HEAD/pkgdown/favicon/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchall/ggip/HEAD/pkgdown/favicon/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchall/ggip/HEAD/pkgdown/favicon/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /tests/testthat/test-coord-ip.R: -------------------------------------------------------------------------------- 1 | test_that("ipaddress classes passed through ggplot unscaled", { 2 | expect_equal(scale_type(ip_address()), "identity") 3 | expect_equal(scale_type(ip_network()), "identity") 4 | expect_equal(scale_type(ip_interface()), "identity") 5 | }) 6 | -------------------------------------------------------------------------------- /src/curves.h: -------------------------------------------------------------------------------- 1 | #ifndef __GGIP_CURVES__ 2 | #define __GGIP_CURVES__ 3 | 4 | #include 5 | 6 | 7 | void hilbert_curve(uint32_t s, int order, uint32_t *x, uint32_t *y); 8 | 9 | void morton_curve(uint32_t s, int order, uint32_t *x, uint32_t *y); 10 | 11 | #endif 12 | -------------------------------------------------------------------------------- /R/ggip-package.R: -------------------------------------------------------------------------------- 1 | #' @keywords internal 2 | #' @import ipaddress 3 | #' @import rlang 4 | "_PACKAGE" 5 | 6 | ## usethis namespace: start 7 | #' @importFrom Rcpp sourceCpp 8 | #' @useDynLib ggip, .registration = TRUE 9 | #' @importFrom dplyr %>% 10 | ## usethis namespace: end 11 | NULL 12 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | 3 | coverage: 4 | status: 5 | project: 6 | default: 7 | target: auto 8 | threshold: 1% 9 | informational: true 10 | patch: 11 | default: 12 | target: auto 13 | threshold: 1% 14 | informational: true 15 | -------------------------------------------------------------------------------- /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^ggip\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^LICENSE\.md$ 4 | ^README\.Rmd$ 5 | ^cran-comments\.md$ 6 | ^\.github$ 7 | ^pkgdown/_pkgdown\.yml$ 8 | ^docs$ 9 | ^pkgdown$ 10 | ^codecov\.yml$ 11 | ^vignettes/bit-figures\.R$ 12 | ^man/figures/logo\.R$ 13 | ^man/figures/logo\.svg$ 14 | ^CRAN-RELEASE$ 15 | ^CRAN-SUBMISSION$ 16 | -------------------------------------------------------------------------------- /src/mapping.h: -------------------------------------------------------------------------------- 1 | #ifndef __GGIP_MAPPING__ 2 | #define __GGIP_MAPPING__ 3 | 4 | #include 5 | 6 | 7 | struct AddressMapping { 8 | unsigned int space_bits, canvas_bits, pixel_bits; 9 | }; 10 | 11 | AddressMapping setup_mapping(const ipaddress::IpNetwork &canvas_network, int pixel_prefix); 12 | 13 | uint32_t address_to_integer(const ipaddress::IpAddress &address, AddressMapping mapping); 14 | 15 | #endif 16 | -------------------------------------------------------------------------------- /tests/testthat/test-theme-ip.R: -------------------------------------------------------------------------------- 1 | test_that("themes drop elements", { 2 | expect_simplistic_theme <- function(t) { 3 | expect_s3_class(t$axis.text, "element_blank") 4 | expect_s3_class(t$axis.ticks, "element_blank") 5 | expect_s3_class(t$axis.title, "element_blank") 6 | expect_s3_class(t$panel.border, "element_blank") 7 | expect_s3_class(t$panel.grid, "element_blank") 8 | } 9 | 10 | expect_simplistic_theme(theme_ip_light()) 11 | expect_simplistic_theme(theme_ip_dark()) 12 | }) 13 | -------------------------------------------------------------------------------- /ggip.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: No 4 | SaveWorkspace: No 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | 15 | AutoAppendNewline: Yes 16 | StripTrailingWhitespace: Yes 17 | LineEndingConversion: Posix 18 | 19 | BuildType: Package 20 | PackageUseDevtools: Yes 21 | PackageInstallArgs: --no-multiarch --with-keep.source 22 | PackageRoxygenize: rd,collate,namespace 23 | -------------------------------------------------------------------------------- /tests/testthat/test-vctrs-coords.R: -------------------------------------------------------------------------------- 1 | test_that("coords classes passed through ggplot unscaled", { 2 | expect_equal(scale_type(ip_address_coords()), "identity") 3 | expect_equal(scale_type(ip_network_coords()), "identity") 4 | }) 5 | 6 | test_that("coords classes identified", { 7 | expect_true(is_ip_address_coords(ip_address_coords())) 8 | expect_false(is_ip_address_coords(ip_network_coords())) 9 | expect_true(is_ip_network_coords(ip_network_coords())) 10 | expect_false(is_ip_network_coords(ip_address_coords())) 11 | }) 12 | -------------------------------------------------------------------------------- /R/RcppExports.R: -------------------------------------------------------------------------------- 1 | # Generated by using Rcpp::compileAttributes() -> do not edit by hand 2 | # Generator token: 10BE3573-1514-4C36-9D1C-5A225CD40393 3 | 4 | wrap_address_to_cartesian <- function(address_r, canvas_network_r, pixel_prefix, curve) { 5 | .Call(`_ggip_wrap_address_to_cartesian`, address_r, canvas_network_r, pixel_prefix, curve) 6 | } 7 | 8 | wrap_network_to_cartesian <- function(network_r, canvas_network_r, pixel_prefix, curve) { 9 | .Call(`_ggip_wrap_network_to_cartesian`, network_r, canvas_network_r, pixel_prefix, curve) 10 | } 11 | 12 | -------------------------------------------------------------------------------- /cran-comments.md: -------------------------------------------------------------------------------- 1 | ## Test environments 2 | 3 | * macOS 13 (local): 4.2 4 | * macOS 12 (GitHub Actions): 4.2 5 | * Ubuntu 22.04 (GitHub Actions): 3.5, 3.6, 4.0, 4.1, 4.2, devel 6 | * Ubuntu 20.04 (RHub): 4.2 7 | * Fedora Linux (RHub): devel 8 | * Windows Server 2022 (GitHub Actions): 3.6, 4.1, 4.2 9 | * Windows Server 2022 (RHub): devel 10 | * Windows Server 2022 (win-builder): devel 11 | 12 | ## R CMD check results 13 | 14 | 0 errors | 0 warnings | 0 notes 15 | 16 | ## Reverse dependencies 17 | 18 | There are currently no reverse dependencies for this package. 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Bug description** 11 | 12 | Please briefly describe your problem and what output you expect. 13 | 14 | **To Reproduce** 15 | 16 | Please include a minimal reproducible example (AKA a reprex). If you've never heard of a [reprex](https://reprex.tidyverse.org/) before, start by reading . 17 | 18 | ```r 19 | # insert reprex here 20 | ``` 21 | 22 | **Additional context** 23 | 24 | Add any other context about the problem here (e.g. operating system). 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /pkgdown/_pkgdown.yml: -------------------------------------------------------------------------------- 1 | url: https://davidchall.github.io/ggip 2 | 3 | home: 4 | title: ggip 5 | description: R package for data visualization of IP addresses and networks 6 | 7 | template: 8 | bootstrap: 5 9 | 10 | reference: 11 | - title: "Fundamentals" 12 | desc: "These components must be included in _every_ ggip plot." 13 | contents: 14 | - coord_ip 15 | - theme_ip 16 | - title: "Layers" 17 | desc: "These IP-specific layers support common use cases." 18 | contents: 19 | - stat_summary_address 20 | - geom_hilbert_outline 21 | - title: "Internal functions" 22 | desc: "These functions are exposed to support use cases beyond ggplot2." 23 | contents: 24 | - ip_to_cartesian 25 | 26 | figures: 27 | fig.asp: 1 28 | fig.width: 5 29 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/geom-hilbert-outline.md: -------------------------------------------------------------------------------- 1 | # input validation 2 | 3 | Problem while converting geom to grob. 4 | i Error occurred in the 1st layer. 5 | Caused by error in `draw_panel()`: 6 | ! ggip plots require `coord_ip()`. 7 | 8 | --- 9 | 10 | Problem while converting geom to grob. 11 | i Error occurred in the 1st layer. 12 | Caused by error in `draw_panel()`: 13 | ! The ip aesthetic of `geom_hilbert_outline()` must be an object. 14 | x You supplied an object. 15 | 16 | --- 17 | 18 | Problem while converting geom to grob. 19 | i Error occurred in the 1st layer. 20 | Caused by error in `draw_panel()`: 21 | ! `geom_hilbert_outline()` only works with `coord_ip(curve = "hilbert")`. 22 | 23 | -------------------------------------------------------------------------------- /man/figures/logo.R: -------------------------------------------------------------------------------- 1 | library(hexSticker) 2 | library(ggip) 3 | library(sysfonts) 4 | 5 | 6 | ggip_logo <- function(outfile, text_color = "#f79d1e", text_font = "Ubuntu Mono", 7 | line_color = "#6ee2ff", bg_color = "#000000", border_color = "#f79d1e") { 8 | 9 | font_add_google(text_font) 10 | 11 | p <- ggplot() + 12 | geom_hilbert_outline(curve_order = 3, color = line_color) + 13 | coord_ip() + 14 | theme_transparent() 15 | 16 | sticker( 17 | # subplot 18 | subplot = p, s_x = 1, s_y = 1, s_width = 2.11, s_height = 2.11, 19 | # package name 20 | package = "ggip", p_family = text_font, p_color = text_color, p_size = 8.5, p_y = 1.18, 21 | # border 22 | h_color = border_color, h_fill = bg_color, 23 | # output file 24 | filename = outfile 25 | ) 26 | } 27 | 28 | 29 | ggip_logo("man/figures/logo.svg") 30 | ggip_logo("man/figures/logo.png") 31 | -------------------------------------------------------------------------------- /man/theme_ip.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/theme-ip.R 3 | \name{theme_ip} 4 | \alias{theme_ip} 5 | \alias{theme_ip_light} 6 | \alias{theme_ip_dark} 7 | \title{Themes for IP data} 8 | \usage{ 9 | theme_ip_light(base_size = 11, base_family = "") 10 | 11 | theme_ip_dark( 12 | background_color = "black", 13 | text_color = "white", 14 | base_size = 11, 15 | base_family = "" 16 | ) 17 | } 18 | \arguments{ 19 | \item{base_size}{base font size, given in pts.} 20 | 21 | \item{base_family}{base font family} 22 | 23 | \item{background_color}{Background color} 24 | 25 | \item{text_color}{Text color} 26 | } 27 | \description{ 28 | These set sensible defaults for plots generated by ggip. 29 | Use \code{\link[ggplot2:theme]{ggplot2::theme()}} if you want to tweak the results. 30 | } 31 | \examples{ 32 | p <- ggplot(data.frame(ip = ip_address("128.0.0.0"))) + 33 | geom_point(aes(x = ip$x, y = ip$y), color = "grey") + 34 | coord_ip() 35 | 36 | p + theme_ip_light() 37 | 38 | p + theme_ip_dark() 39 | } 40 | -------------------------------------------------------------------------------- /man/ggip-package.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/ggip-package.R 3 | \docType{package} 4 | \name{ggip-package} 5 | \alias{ggip} 6 | \alias{ggip-package} 7 | \title{ggip: Data Visualization for IP Addresses and Networks} 8 | \description{ 9 | \if{html}{\figure{logo.png}{options: style='float: right' alt='logo' width='120'}} 10 | 11 | A 'ggplot2' extension that enables visualization of IP (Internet Protocol) addresses and networks. The address space is mapped onto the Cartesian coordinate system using a space-filling curve. Offers full support for both IPv4 and IPv6 (Internet Protocol versions 4 and 6) address spaces. 12 | } 13 | \seealso{ 14 | Useful links: 15 | \itemize{ 16 | \item \url{https://davidchall.github.io/ggip/} 17 | \item \url{https://github.com/davidchall/ggip} 18 | \item Report bugs at \url{https://github.com/davidchall/ggip/issues} 19 | } 20 | 21 | } 22 | \author{ 23 | \strong{Maintainer}: David Hall \email{david.hall.physics@gmail.com} (\href{https://orcid.org/0000-0002-2193-0480}{ORCID}) 24 | 25 | } 26 | \keyword{internal} 27 | -------------------------------------------------------------------------------- /src/curves.cpp: -------------------------------------------------------------------------------- 1 | #include "curves.h" 2 | 3 | 4 | void hilbert_curve(uint32_t s, int order, uint32_t *x, uint32_t *y) { 5 | unsigned int state, row; 6 | state = *x = *y = 0; 7 | 8 | for (int i=2*order-2; i>=0; i-=2) { 9 | row = 4 * state | ((s >> i) & 3); 10 | 11 | *x = (*x << 1) | ((0x936C >> row) & 1); 12 | *y = (*y << 1) | ((0x39C6 >> row) & 1); 13 | 14 | state = (0x3E6B94C1 >> 2 * row) & 3; 15 | } 16 | 17 | // invert y-axis 18 | unsigned int y_max = (1 << order) - 1; 19 | *y = y_max - *y; 20 | } 21 | 22 | 23 | // https://fgiesen.wordpress.com/2009/12/13/decoding-morton-codes/ 24 | uint32_t morton_extract(uint32_t x) { 25 | x &= 0x55555555; 26 | x = (x ^ (x >> 1)) & 0x33333333; 27 | x = (x ^ (x >> 2)) & 0x0f0f0f0f; 28 | x = (x ^ (x >> 4)) & 0x00ff00ff; 29 | x = (x ^ (x >> 8)) & 0x0000ffff; 30 | return x; 31 | } 32 | 33 | void morton_curve(uint32_t s, int order, uint32_t *x, uint32_t *y) { 34 | *x = morton_extract(s); 35 | *y = morton_extract(s >> 1); 36 | 37 | // invert y-axis 38 | unsigned int y_max = (1 << order) - 1; 39 | *y = y_max - *y; 40 | } 41 | -------------------------------------------------------------------------------- /R/utils.R: -------------------------------------------------------------------------------- 1 | # these functions are copied from ggplot2 2 | 3 | # Use chartr() for safety since toupper() fails to convert i to I in Turkish locale 4 | lower_ascii <- "abcdefghijklmnopqrstuvwxyz" 5 | upper_ascii <- "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 6 | to_lower_ascii <- function(x) chartr(upper_ascii, lower_ascii, x) 7 | 8 | snakeize <- function(x) { 9 | x <- gsub("([A-Za-z])([A-Z])([a-z])", "\\1_\\2\\3", x) 10 | x <- gsub(".", "_", x, fixed = TRUE) 11 | x <- gsub("([a-z])([A-Z])", "\\1_\\2", x) 12 | to_lower_ascii(x) 13 | } 14 | 15 | snake_class <- function(x) { 16 | snakeize(class(x)[1]) 17 | } 18 | 19 | # Convenience function used by `stat_function()` and 20 | # `geom_function()` to convert empty input data into 21 | # non-empty input data without touching any non-empty 22 | # input data that may have been provided. 23 | ensure_nonempty_data <- function(data) { 24 | if (empty(data)) { 25 | data.frame(group = 1) 26 | } else { 27 | data 28 | } 29 | } 30 | 31 | empty <- function(df) { 32 | is.null(df) || nrow(df) == 0 || ncol(df) == 0 || is.waive(df) 33 | } 34 | 35 | is.waive <- function(x) inherits(x, "waiver") 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2020 David Hall 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | S3method("$",ip_address_coords) 4 | S3method("$",ip_network_coords) 5 | S3method(as.character,ip_address_coords) 6 | S3method(as.character,ip_network_coords) 7 | S3method(format,ip_address_coords) 8 | S3method(format,ip_network_coords) 9 | S3method(scale_type,ip_address) 10 | S3method(scale_type,ip_address_coords) 11 | S3method(scale_type,ip_network) 12 | S3method(scale_type,ip_network_coords) 13 | S3method(vec_proxy_equal,ip_address_coords) 14 | S3method(vec_proxy_equal,ip_network_coords) 15 | export(address_to_cartesian) 16 | export(coord_ip) 17 | export(geom_hilbert_outline) 18 | export(network_to_cartesian) 19 | export(stat_summary_address) 20 | export(theme_ip_dark) 21 | export(theme_ip_light) 22 | import(ipaddress) 23 | import(rlang) 24 | importFrom(Rcpp,sourceCpp) 25 | importFrom(dplyr,"%>%") 26 | importFrom(ggplot2,"%+replace%") 27 | importFrom(ggplot2,element_blank) 28 | importFrom(ggplot2,element_rect) 29 | importFrom(ggplot2,element_text) 30 | importFrom(ggplot2,scale_type) 31 | importFrom(ggplot2,theme) 32 | importFrom(vctrs,vec_proxy_equal) 33 | useDynLib(ggip, .registration = TRUE) 34 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: ggip 2 | Title: Data Visualization for IP Addresses and Networks 3 | Version: 0.3.2.9000 4 | Authors@R: 5 | person("David", "Hall", , "david.hall.physics@gmail.com", role = c("aut", "cre"), 6 | comment = c(ORCID = "0000-0002-2193-0480")) 7 | Description: A 'ggplot2' extension that enables visualization of IP 8 | (Internet Protocol) addresses and networks. The address space is 9 | mapped onto the Cartesian coordinate system using a space-filling 10 | curve. Offers full support for both IPv4 and IPv6 (Internet Protocol 11 | versions 4 and 6) address spaces. 12 | License: MIT + file LICENSE 13 | URL: https://davidchall.github.io/ggip/, 14 | https://github.com/davidchall/ggip 15 | BugReports: https://github.com/davidchall/ggip/issues 16 | Depends: 17 | ggplot2 (>= 3.4.0), 18 | ipaddress (>= 1.0.0) 19 | Imports: 20 | cli, 21 | dplyr, 22 | Rcpp, 23 | rlang (>= 1.0.0), 24 | tidyr, 25 | vctrs 26 | Suggests: 27 | knitr, 28 | rmarkdown, 29 | testthat 30 | LinkingTo: 31 | ipaddress, 32 | Rcpp 33 | VignetteBuilder: 34 | knitr 35 | Config/testthat/edition: 3 36 | Encoding: UTF-8 37 | Roxygen: list(markdown = TRUE) 38 | RoxygenNote: 7.2.3 39 | -------------------------------------------------------------------------------- /src/mapping.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "mapping.h" 3 | 4 | using namespace ipaddress; 5 | 6 | 7 | AddressMapping setup_mapping(const IpNetwork &canvas_network, int pixel_prefix) { 8 | AddressMapping mapping; 9 | 10 | mapping.space_bits = canvas_network.address().n_bits(); 11 | mapping.canvas_bits = mapping.space_bits - canvas_network.prefix_length(); 12 | mapping.pixel_bits = mapping.space_bits - pixel_prefix; 13 | 14 | return mapping; 15 | } 16 | 17 | uint32_t address_to_integer(const IpAddress &address, AddressMapping mapping) { 18 | 19 | // neglect leading bits 20 | IpAddress canvas_hostmask = prefix_to_hostmask( 21 | mapping.space_bits - mapping.canvas_bits, 22 | address.is_ipv6() 23 | ); 24 | IpAddress reduced_address = bitwise_and(address, canvas_hostmask); 25 | 26 | // neglect trailing bits 27 | reduced_address = bitwise_shift_right(reduced_address, mapping.pixel_bits); 28 | 29 | // interpret final 4 bytes as integer 30 | // NOTE: this limits plotting to canvas_bits - pixel_bits <= 32 31 | uint32_t reduced_integer; 32 | std::memcpy(&reduced_integer, reduced_address.end() - 4, 4); 33 | reduced_integer = network_to_host_long(reduced_integer); 34 | 35 | return reduced_integer; 36 | } 37 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # ggip (development version) 2 | 3 | # ggip 0.3.2 4 | 5 | Fix for CRAN checks. 6 | 7 | # ggip 0.3.1 8 | 9 | Compatible with ipaddress 1.0.0. 10 | 11 | 12 | # ggip 0.3.0 13 | 14 | * Compatible with ggplot2 3.4.0. This introduced a **breaking change**, where an aesthetic of `geom_hilbert_outline()` was renamed from `size` to `linewidth`. 15 | * Use {cli} to format error messages. 16 | 17 | 18 | # ggip 0.2.2 19 | 20 | Compatible with tidyselect 1.2.0. 21 | 22 | 23 | # ggip 0.2.1 24 | 25 | Fix for CRAN checks. 26 | 27 | 28 | # ggip 0.2.0 29 | 30 | * A new layer `geom_hilbert_outline()` can overlay the outline of the Hilbert curve. This can guide the eye to regions that are close in IP address space (#16). 31 | * Improved performance of `stat_summary_address()`, by using an alternative check for missing values (#15). 32 | * `stat_summary_address()` now removes data that were not mapped to the 2D grid (because the address was outside the visualized network). A warning is generated by default, which can be disabled using the `na.rm` argument. This new behavior is consistent with that of native ggplot2 layers (#15). 33 | 34 | 35 | # ggip 0.1.0 36 | 37 | First CRAN release. 38 | 39 | * `coord_ip()` provides a unified mapping from IP address space to the 2D grid. 40 | * `stat_summary_address()` summarizes IP addresses on a heatmap. 41 | * `theme_ip_light()` and `theme_ip_dark()` offer sensible default theming for ggip plots. 42 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/stat-summary-address.md: -------------------------------------------------------------------------------- 1 | # input validation 2 | 3 | Problem while computing stat. 4 | i Error occurred in the 1st layer. 5 | Caused by error in `compute_layer()`: 6 | ! ggip plots require `coord_ip()`. 7 | 8 | --- 9 | 10 | Problem while computing stat. 11 | i Error occurred in the 1st layer. 12 | Caused by error in `compute_layer()`: 13 | ! The ip aesthetic of `stat_summary_address()` must be an object. 14 | x You supplied an object. 15 | 16 | --- 17 | 18 | Problem while computing stat. 19 | i Error occurred in the 1st layer. 20 | Caused by error in `compute_layer()`: 21 | ! `stat_summary_address()` requires an ip aesthetic. 22 | 23 | --- 24 | 25 | Problem while computing stat. 26 | i Error occurred in the 1st layer. 27 | Caused by error in `compute_layer()`: 28 | ! `stat_summary_address()` requires an ip aesthetic. 29 | 30 | --- 31 | 32 | Problem while computing stat. 33 | i Error occurred in the 1st layer. 34 | Caused by error in `compute_layer()`: 35 | ! The ip aesthetic of `stat_summary_address()` must map to a `data` variable. 36 | 37 | --- 38 | 39 | Problem while computing stat. 40 | i Error occurred in the 1st layer. 41 | Caused by error in `compute_layer()`: 42 | ! `stat_summary_address()` requires a z aesthetic when using the `fun` argument. 43 | 44 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | release: 9 | types: [published] 10 | workflow_dispatch: 11 | 12 | name: pkgdown 13 | 14 | jobs: 15 | pkgdown: 16 | runs-on: ubuntu-latest 17 | # Only restrict concurrency for non-PR jobs 18 | concurrency: 19 | group: pkgdown-${{ github.event_name != 'pull_request' || github.run_id }} 20 | env: 21 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 22 | steps: 23 | - uses: actions/checkout@v3 24 | 25 | - uses: r-lib/actions/setup-pandoc@v2 26 | 27 | - uses: r-lib/actions/setup-r@v2 28 | with: 29 | use-public-rspm: true 30 | 31 | - uses: r-lib/actions/setup-r-dependencies@v2 32 | with: 33 | extra-packages: any::pkgdown, local::. 34 | needs: website 35 | 36 | - name: Build site 37 | run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE) 38 | shell: Rscript {0} 39 | 40 | - name: Deploy to GitHub pages 🚀 41 | if: github.event_name != 'pull_request' 42 | uses: JamesIves/github-pages-deploy-action@v4.4.1 43 | with: 44 | clean: false 45 | branch: gh-pages 46 | folder: docs 47 | -------------------------------------------------------------------------------- /.github/workflows/test-coverage.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | 9 | name: test-coverage 10 | 11 | jobs: 12 | test-coverage: 13 | runs-on: ubuntu-latest 14 | env: 15 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - uses: r-lib/actions/setup-r@v2 21 | with: 22 | use-public-rspm: true 23 | 24 | - uses: r-lib/actions/setup-r-dependencies@v2 25 | with: 26 | extra-packages: any::covr 27 | needs: coverage 28 | 29 | - name: Test coverage 30 | run: | 31 | covr::codecov( 32 | quiet = FALSE, 33 | clean = FALSE, 34 | install_path = file.path(Sys.getenv("RUNNER_TEMP"), "package") 35 | ) 36 | shell: Rscript {0} 37 | 38 | - name: Show testthat output 39 | if: always() 40 | run: | 41 | ## -------------------------------------------------------------------- 42 | find ${{ runner.temp }}/package -name 'testthat.Rout*' -exec cat '{}' \; || true 43 | shell: bash 44 | 45 | - name: Upload test results 46 | if: failure() 47 | uses: actions/upload-artifact@v3 48 | with: 49 | name: coverage-test-failures 50 | path: ${{ runner.temp }}/package 51 | -------------------------------------------------------------------------------- /vignettes/bit-figures.R: -------------------------------------------------------------------------------- 1 | library(tidyverse) 2 | library(ipaddress) 3 | library(DiagrammeR) 4 | library(DiagrammeRsvg) 5 | library(rsvg) 6 | 7 | 8 | bit_figure <- function(address, path, 9 | canvas_prefix = 0, pixel_prefix = max_prefix_length(address), 10 | canvas_color = "#fed766", pixel_color = "#2ab7ca") { 11 | 12 | bit_array <- strsplit(ip_to_binary(address), "")[[1]] 13 | 14 | table_rows <- tibble(bit = bit_array) %>% 15 | mutate( 16 | is_canvas = row_number() - 1 < canvas_prefix, 17 | is_pixel = row_number() - 1 >= pixel_prefix, 18 | table_row = case_when( 19 | is_canvas ~ str_glue('{bit}'), 20 | is_pixel ~ str_glue('{bit}'), 21 | TRUE ~ str_glue('{bit}') 22 | ) 23 | ) %>% 24 | pull(table_row) 25 | 26 | graph <- str_glue(' 27 | digraph address {{ 28 | address [ 29 | shape = none 30 | label = < 31 | {paste(table_rows, collapse = "\n")} 32 |
> 33 | ] 34 | }} 35 | ') 36 | 37 | grViz(graph) %>% 38 | export_svg %>% 39 | charToRaw %>% 40 | rsvg_png(path, height = 100) 41 | } 42 | 43 | 44 | bit_figure(ip_address("192.168.0.124"), "vignettes/bits_raw.png") 45 | 46 | bit_figure(ip_address("192.168.0.124"), "vignettes/bits_half_reduced.png", canvas_prefix = 8) 47 | 48 | bit_figure(ip_address("192.168.0.124"), "vignettes/bits_reduced.png", canvas_prefix = 8, pixel_prefix = 24) 49 | 50 | -------------------------------------------------------------------------------- /src/address_to_pixel.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "mapping.h" 4 | #include "curves.h" 5 | 6 | using namespace Rcpp; 7 | using namespace ipaddress; 8 | 9 | 10 | void address_to_pixel(const IpAddress &address, AddressMapping mapping, bool is_morton, uint32_t *x, uint32_t *y) { 11 | int curve_order = (mapping.canvas_bits - mapping.pixel_bits) / 2; 12 | uint32_t pixel_int = address_to_integer(address, mapping); 13 | if (is_morton) { 14 | morton_curve(pixel_int, curve_order, x, y); 15 | } else { 16 | hilbert_curve(pixel_int, curve_order, x, y); 17 | } 18 | } 19 | 20 | // [[Rcpp::export]] 21 | DataFrame wrap_address_to_cartesian(List address_r, List canvas_network_r, int pixel_prefix, String curve) { 22 | std::vector address = decode_addresses(address_r); 23 | std::vector canvas_networks = decode_networks(canvas_network_r); 24 | 25 | if (canvas_networks.size() != 1) { 26 | stop("'canvas_network' must be an ip_network scalar"); // # nocov 27 | } 28 | IpNetwork canvas_network = canvas_networks[0]; 29 | 30 | // initialize output vectors 31 | std::size_t vsize = address.size(); 32 | IntegerVector out_x(vsize); 33 | IntegerVector out_y(vsize); 34 | 35 | // setup mapping from IP space to plotting canvas 36 | AddressMapping mapping = setup_mapping(canvas_network, pixel_prefix); 37 | 38 | // setup curve 39 | bool is_morton = (curve == "morton"); 40 | 41 | for (std::size_t i=0; i scalar. 4 | 5 | --- 6 | 7 | `canvas_network` must be an scalar. 8 | 9 | --- 10 | 11 | `pixel_prefix` must be a positive integer scalar. 12 | 13 | --- 14 | 15 | `pixel_prefix` must be a positive integer scalar. 16 | 17 | --- 18 | 19 | `pixel_prefix` must be a positive integer scalar. 20 | 21 | --- 22 | 23 | `curve` must be one of "hilbert" or "morton", not "hilber". 24 | i Did you mean "hilbert"? 25 | 26 | --- 27 | 28 | `curve` must be one of "hilbert" or "morton", not "hilber". 29 | i Did you mean "hilbert"? 30 | 31 | --- 32 | 33 | Pixel prefix length must not be greater than 32. 34 | i Canvas uses IPv4 address space. 35 | x Pixel prefix length is 33. 36 | 37 | --- 38 | 39 | Pixel prefix length must not be greater than 128. 40 | i Canvas uses IPv6 address space. 41 | x Pixel prefix length is 129. 42 | 43 | --- 44 | 45 | Pixel prefix length must be greater than canvas. 46 | x Canvas has prefix length 16. 47 | x Pixel has prefix length 14. 48 | 49 | --- 50 | 51 | The difference between canvas and pixel prefix lengths must be even. 52 | x Canvas has prefix length 0. 53 | x Pixel has prefix length 31. 54 | 55 | --- 56 | 57 | The difference between canvas and pixel prefix lengths must not be greater than 24. 58 | x Canvas has prefix length 0. 59 | x Pixel has prefix length 32. 60 | i These values would produce a plot with 65,536 x 65,536 pixels. 61 | 62 | # Other input validation 63 | 64 | `address` must be an vector. 65 | 66 | --- 67 | 68 | `network` must be an vector. 69 | 70 | # Missing values 71 | 72 | `canvas_network` cannot be NA. 73 | 74 | --- 75 | 76 | `canvas_network` cannot be NA. 77 | 78 | --- 79 | 80 | `pixel_prefix` must be a positive integer scalar. 81 | 82 | --- 83 | 84 | `pixel_prefix` must be a positive integer scalar. 85 | 86 | -------------------------------------------------------------------------------- /R/theme-ip.R: -------------------------------------------------------------------------------- 1 | #' Themes for IP data 2 | #' 3 | #' These set sensible defaults for plots generated by ggip. 4 | #' Use [ggplot2::theme()] if you want to tweak the results. 5 | #' 6 | #' @inheritParams ggplot2::theme_grey 7 | #' @param background_color Background color 8 | #' @param text_color Text color 9 | #' 10 | #' @examples 11 | #' p <- ggplot(data.frame(ip = ip_address("128.0.0.0"))) + 12 | #' geom_point(aes(x = ip$x, y = ip$y), color = "grey") + 13 | #' coord_ip() 14 | #' 15 | #' p + theme_ip_light() 16 | #' 17 | #' p + theme_ip_dark() 18 | #' @name theme_ip 19 | NULL 20 | 21 | #' @rdname theme_ip 22 | #' @importFrom ggplot2 theme %+replace% element_blank element_rect element_text 23 | #' @export 24 | theme_ip_light <- function(base_size = 11, base_family = "") { 25 | ggplot2::theme_bw( 26 | base_size = base_size, 27 | base_family = base_family 28 | ) %+replace% 29 | theme( 30 | axis.text = element_blank(), 31 | axis.ticks = element_blank(), 32 | axis.title = element_blank(), 33 | panel.border = element_blank(), 34 | panel.grid = element_blank() 35 | ) 36 | } 37 | 38 | #' @rdname theme_ip 39 | #' @export 40 | theme_ip_dark <- function(background_color = "black", text_color = "white", 41 | base_size = 11, base_family = "") { 42 | theme_ip_light( 43 | base_size = base_size, 44 | base_family = base_family 45 | ) %+replace% 46 | theme( 47 | legend.background = element_rect(fill = background_color), 48 | legend.key = element_rect(fill = background_color), 49 | panel.background = element_rect(fill = background_color), 50 | plot.background = element_rect(fill = background_color) 51 | ) + 52 | theme( 53 | legend.text = element_text(color = text_color), 54 | legend.title = element_text(color = text_color), 55 | plot.caption = element_text(color = text_color), 56 | plot.subtitle = element_text(color = text_color), 57 | plot.tag = element_text(color = text_color), 58 | plot.title = element_text(color = text_color) 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /man/ip_to_cartesian.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/ip_to_cartesian.R 3 | \name{ip_to_cartesian} 4 | \alias{ip_to_cartesian} 5 | \alias{address_to_cartesian} 6 | \alias{network_to_cartesian} 7 | \title{Map IP data to Cartesian coordinates} 8 | \usage{ 9 | address_to_cartesian( 10 | address, 11 | canvas_network = ip_network("0.0.0.0/0"), 12 | pixel_prefix = 16, 13 | curve = c("hilbert", "morton") 14 | ) 15 | 16 | network_to_cartesian( 17 | network, 18 | canvas_network = ip_network("0.0.0.0/0"), 19 | pixel_prefix = 16, 20 | curve = c("hilbert", "morton") 21 | ) 22 | } 23 | \arguments{ 24 | \item{address}{An \code{\link[ipaddress:ip_address]{ip_address}} vector} 25 | 26 | \item{canvas_network}{An \code{\link[ipaddress:ip_network]{ip_network}} scalar that 27 | determines the region of IP space visualized by the entire 2D grid. The 28 | default shows the entire IPv4 address space.} 29 | 30 | \item{pixel_prefix}{An integer scalar that sets the prefix length of the 31 | network represented by a single pixel. The default value is 16. Increasing 32 | this effectively improves the resolution of the plot.} 33 | 34 | \item{curve}{A string to choose the space-filling curve. Choices are 35 | \code{"hilbert"} (default) and \code{"morton"}.} 36 | 37 | \item{network}{An \code{\link[ipaddress:ip_network]{ip_network}} vector} 38 | } 39 | \value{ 40 | A data.frame containing columns: 41 | \itemize{ 42 | \item \code{address_to_cartesian()}: \code{x} and \code{y} 43 | \item \code{network_to_cartesian()}: \code{xmin}, \code{ymin}, \code{xmax} and \code{ymax} 44 | } 45 | } 46 | \description{ 47 | These functions are used internally by \code{\link[=coord_ip]{coord_ip()}} to map 48 | \code{\link[ipaddress:ip_address]{ip_address}} and \code{\link[ipaddress:ip_network]{ip_network}} 49 | vectors to Cartesian coordinates. They are exposed externally to support use 50 | of these coordinates outside of ggplot2. 51 | } 52 | \examples{ 53 | address_to_cartesian(ip_address("192.168.0.1")) 54 | 55 | network_to_cartesian(ip_network("224.0.0.0/4")) 56 | } 57 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | # 4 | # NOTE: This workflow is overkill for most R packages and 5 | # check-standard.yaml is likely a better choice. 6 | # usethis::use_github_action("check-standard") will install it. 7 | on: 8 | push: 9 | branches: [main, master] 10 | pull_request: 11 | branches: [main, master] 12 | 13 | name: R-CMD-check 14 | 15 | jobs: 16 | R-CMD-check: 17 | runs-on: ${{ matrix.config.os }} 18 | 19 | name: ${{ matrix.config.os }} (${{ matrix.config.r }}) 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | config: 25 | - {os: macos-latest, r: 'release'} 26 | 27 | - {os: windows-latest, r: 'release'} 28 | # Use 3.6 to trigger usage of RTools35 29 | - {os: windows-latest, r: '3.6'} 30 | # use 4.1 to check with rtools40's older compiler 31 | - {os: windows-latest, r: '4.1'} 32 | 33 | - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} 34 | - {os: ubuntu-latest, r: 'release'} 35 | - {os: ubuntu-latest, r: 'oldrel-1'} 36 | - {os: ubuntu-latest, r: 'oldrel-2'} 37 | - {os: ubuntu-latest, r: 'oldrel-3'} 38 | - {os: ubuntu-latest, r: 'oldrel-4'} 39 | 40 | env: 41 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 42 | R_KEEP_PKG_SOURCE: yes 43 | 44 | steps: 45 | - uses: actions/checkout@v3 46 | 47 | - uses: r-lib/actions/setup-pandoc@v2 48 | 49 | - uses: r-lib/actions/setup-r@v2 50 | with: 51 | r-version: ${{ matrix.config.r }} 52 | http-user-agent: ${{ matrix.config.http-user-agent }} 53 | use-public-rspm: true 54 | 55 | - uses: r-lib/actions/setup-r-dependencies@v2 56 | with: 57 | extra-packages: any::rcmdcheck 58 | needs: check 59 | 60 | - uses: r-lib/actions/check-r-package@v2 61 | with: 62 | upload-snapshots: true 63 | -------------------------------------------------------------------------------- /src/RcppExports.cpp: -------------------------------------------------------------------------------- 1 | // Generated by using Rcpp::compileAttributes() -> do not edit by hand 2 | // Generator token: 10BE3573-1514-4C36-9D1C-5A225CD40393 3 | 4 | #include 5 | 6 | using namespace Rcpp; 7 | 8 | #ifdef RCPP_USE_GLOBAL_ROSTREAM 9 | Rcpp::Rostream& Rcpp::Rcout = Rcpp::Rcpp_cout_get(); 10 | Rcpp::Rostream& Rcpp::Rcerr = Rcpp::Rcpp_cerr_get(); 11 | #endif 12 | 13 | // wrap_address_to_cartesian 14 | DataFrame wrap_address_to_cartesian(List address_r, List canvas_network_r, int pixel_prefix, String curve); 15 | RcppExport SEXP _ggip_wrap_address_to_cartesian(SEXP address_rSEXP, SEXP canvas_network_rSEXP, SEXP pixel_prefixSEXP, SEXP curveSEXP) { 16 | BEGIN_RCPP 17 | Rcpp::RObject rcpp_result_gen; 18 | Rcpp::RNGScope rcpp_rngScope_gen; 19 | Rcpp::traits::input_parameter< List >::type address_r(address_rSEXP); 20 | Rcpp::traits::input_parameter< List >::type canvas_network_r(canvas_network_rSEXP); 21 | Rcpp::traits::input_parameter< int >::type pixel_prefix(pixel_prefixSEXP); 22 | Rcpp::traits::input_parameter< String >::type curve(curveSEXP); 23 | rcpp_result_gen = Rcpp::wrap(wrap_address_to_cartesian(address_r, canvas_network_r, pixel_prefix, curve)); 24 | return rcpp_result_gen; 25 | END_RCPP 26 | } 27 | // wrap_network_to_cartesian 28 | DataFrame wrap_network_to_cartesian(List network_r, List canvas_network_r, int pixel_prefix, String curve); 29 | RcppExport SEXP _ggip_wrap_network_to_cartesian(SEXP network_rSEXP, SEXP canvas_network_rSEXP, SEXP pixel_prefixSEXP, SEXP curveSEXP) { 30 | BEGIN_RCPP 31 | Rcpp::RObject rcpp_result_gen; 32 | Rcpp::RNGScope rcpp_rngScope_gen; 33 | Rcpp::traits::input_parameter< List >::type network_r(network_rSEXP); 34 | Rcpp::traits::input_parameter< List >::type canvas_network_r(canvas_network_rSEXP); 35 | Rcpp::traits::input_parameter< int >::type pixel_prefix(pixel_prefixSEXP); 36 | Rcpp::traits::input_parameter< String >::type curve(curveSEXP); 37 | rcpp_result_gen = Rcpp::wrap(wrap_network_to_cartesian(network_r, canvas_network_r, pixel_prefix, curve)); 38 | return rcpp_result_gen; 39 | END_RCPP 40 | } 41 | 42 | static const R_CallMethodDef CallEntries[] = { 43 | {"_ggip_wrap_address_to_cartesian", (DL_FUNC) &_ggip_wrap_address_to_cartesian, 4}, 44 | {"_ggip_wrap_network_to_cartesian", (DL_FUNC) &_ggip_wrap_network_to_cartesian, 4}, 45 | {NULL, NULL, 0} 46 | }; 47 | 48 | RcppExport void R_init_ggip(DllInfo *dll) { 49 | R_registerRoutines(dll, NULL, CallEntries, NULL, NULL); 50 | R_useDynamicSymbols(dll, FALSE); 51 | } 52 | -------------------------------------------------------------------------------- /.github/workflows/pr-commands.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | issue_comment: 5 | types: [created] 6 | 7 | name: Commands 8 | 9 | jobs: 10 | document: 11 | if: ${{ github.event.issue.pull_request && (github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER') && startsWith(github.event.comment.body, '/document') }} 12 | name: document 13 | runs-on: ubuntu-latest 14 | env: 15 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - uses: r-lib/actions/pr-fetch@v2 20 | with: 21 | repo-token: ${{ secrets.GITHUB_TOKEN }} 22 | 23 | - uses: r-lib/actions/setup-r@v2 24 | with: 25 | use-public-rspm: true 26 | 27 | - uses: r-lib/actions/setup-r-dependencies@v2 28 | with: 29 | extra-packages: any::roxygen2 30 | needs: pr-document 31 | 32 | - name: Document 33 | run: roxygen2::roxygenise() 34 | shell: Rscript {0} 35 | 36 | - name: commit 37 | run: | 38 | git config --local user.name "$GITHUB_ACTOR" 39 | git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" 40 | git add man/\* NAMESPACE 41 | git commit -m 'Document' 42 | 43 | - uses: r-lib/actions/pr-push@v2 44 | with: 45 | repo-token: ${{ secrets.GITHUB_TOKEN }} 46 | 47 | style: 48 | if: ${{ github.event.issue.pull_request && (github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER') && startsWith(github.event.comment.body, '/style') }} 49 | name: style 50 | runs-on: ubuntu-latest 51 | env: 52 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 53 | steps: 54 | - uses: actions/checkout@v3 55 | 56 | - uses: r-lib/actions/pr-fetch@v2 57 | with: 58 | repo-token: ${{ secrets.GITHUB_TOKEN }} 59 | 60 | - uses: r-lib/actions/setup-r@v2 61 | 62 | - name: Install dependencies 63 | run: install.packages("styler") 64 | shell: Rscript {0} 65 | 66 | - name: Style 67 | run: styler::style_pkg() 68 | shell: Rscript {0} 69 | 70 | - name: commit 71 | run: | 72 | git config --local user.name "$GITHUB_ACTOR" 73 | git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" 74 | git add \*.R 75 | git commit -m 'Style' 76 | 77 | - uses: r-lib/actions/pr-push@v2 78 | with: 79 | repo-token: ${{ secrets.GITHUB_TOKEN }} 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # ggip 5 | 6 | 7 | 8 | [![Lifecycle: 9 | experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://lifecycle.r-lib.org/articles/stages.html) 10 | [![CRAN 11 | status](https://www.r-pkg.org/badges/version/ggip)](https://CRAN.R-project.org/package=ggip) 12 | [![R build 13 | status](https://github.com/davidchall/ggip/workflows/R-CMD-check/badge.svg)](https://github.com/davidchall/ggip/actions) 14 | [![Coverage 15 | status](https://codecov.io/gh/davidchall/ggip/branch/master/graph/badge.svg)](https://app.codecov.io/gh/davidchall/ggip?branch=master) 16 | 17 | 18 | ggip is a [{ggplot2}](https://ggplot2.tidyverse.org) extension for 19 | visualizing IP addresses and networks stored in 20 | [{ipaddress}](https://davidchall.github.io/ipaddress/) vectors. 21 | 22 | Here are some of the key features: 23 | 24 | - IP data mapped to 2D plane by a **unified coordinate system** 25 | - Compatible with **existing ggplot2 layers** 26 | - Custom **IP-specific layers** for common use cases 27 | - Full support for both **IPv4 and IPv6** address spaces 28 | 29 | ## Installation 30 | 31 | You can install the released version of ggip from 32 | [CRAN](https://CRAN.R-project.org) with: 33 | 34 | ``` r 35 | install.packages("ggip") 36 | ``` 37 | 38 | Or you can install the development version from GitHub: 39 | 40 | ``` r 41 | # install.packages("remotes") 42 | remotes::install_github("davidchall/ggip") 43 | ``` 44 | 45 | ## Usage 46 | 47 | Plotting with {ggip} follows most of the conventions set by 48 | [{ggplot2}](https://ggplot2.tidyverse.org). A major difference is that 49 | `coord_ip()` is required to map IP data to the 2D grid (addresses to 50 | points and networks to rectangles). Learn more in `vignette("ggip")`. 51 | 52 | Here’s a quick showcase of what’s possible: 53 | 54 | ``` r 55 | library(tidyverse) 56 | library(ggfittext) 57 | library(ggip) 58 | 59 | ggplot(ip_data) + 60 | stat_summary_address(aes(ip = address)) + 61 | geom_hilbert_outline(color = "grey80") + 62 | geom_fit_text( 63 | aes( 64 | xmin = network$xmin, xmax = network$xmax, 65 | ymin = network$ymin, ymax = network$ymax, 66 | label = label 67 | ), 68 | data = iana_ipv4 %>% filter(allocation == "Reserved"), 69 | color = "#fdc086", grow = TRUE 70 | ) + 71 | scale_fill_viridis_c(name = NULL, trans = "log2", na.value = "black") + 72 | coord_ip(pixel_prefix = 20) + 73 | theme_ip_dark() 74 | #> Warning: Transformation introduced infinite values in discrete y-axis 75 | ``` 76 | 77 | 78 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | --- 4 | 5 | 6 | 7 | ```{r, include = FALSE} 8 | knitr::opts_chunk$set( 9 | collapse = TRUE, 10 | comment = "#>", 11 | fig.path = "man/figures/README-", 12 | out.width = "100%" 13 | ) 14 | ``` 15 | 16 | # ggip 17 | 18 | 19 | [![Lifecycle: experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://lifecycle.r-lib.org/articles/stages.html) 20 | [![CRAN status](https://www.r-pkg.org/badges/version/ggip)](https://CRAN.R-project.org/package=ggip) 21 | [![R build status](https://github.com/davidchall/ggip/workflows/R-CMD-check/badge.svg)](https://github.com/davidchall/ggip/actions) 22 | [![Coverage status](https://codecov.io/gh/davidchall/ggip/branch/master/graph/badge.svg)](https://app.codecov.io/gh/davidchall/ggip?branch=master) 23 | 24 | 25 | 26 | ggip is a [{ggplot2}](https://ggplot2.tidyverse.org) extension for visualizing IP addresses and networks stored in [{ipaddress}](https://davidchall.github.io/ipaddress/) vectors. 27 | 28 | Here are some of the key features: 29 | 30 | * IP data mapped to 2D plane by a **unified coordinate system** 31 | * Compatible with **existing ggplot2 layers** 32 | * Custom **IP-specific layers** for common use cases 33 | * Full support for both **IPv4 and IPv6** address spaces 34 | 35 | 36 | ## Installation 37 | 38 | You can install the released version of ggip from [CRAN](https://CRAN.R-project.org) with: 39 | 40 | ``` r 41 | install.packages("ggip") 42 | ``` 43 | 44 | Or you can install the development version from GitHub: 45 | 46 | ``` r 47 | # install.packages("remotes") 48 | remotes::install_github("davidchall/ggip") 49 | ``` 50 | 51 | 52 | ## Usage 53 | 54 | Plotting with {ggip} follows most of the conventions set by [{ggplot2}](https://ggplot2.tidyverse.org). 55 | A major difference is that `coord_ip()` is required to map IP data to the 2D grid (addresses to points and networks to rectangles). 56 | Learn more in `vignette("ggip")`. 57 | 58 | Here's a quick showcase of what's possible: 59 | 60 | ```{r ipv4-heatmap, fig.width=11.2, fig.height=10.3} 61 | library(tidyverse) 62 | library(ggfittext) 63 | library(ggip) 64 | 65 | ggplot(ip_data) + 66 | stat_summary_address(aes(ip = address)) + 67 | geom_hilbert_outline(color = "grey80") + 68 | geom_fit_text( 69 | aes( 70 | xmin = network$xmin, xmax = network$xmax, 71 | ymin = network$ymin, ymax = network$ymax, 72 | label = label 73 | ), 74 | data = iana_ipv4 %>% filter(allocation == "Reserved"), 75 | color = "#fdc086", grow = TRUE 76 | ) + 77 | scale_fill_viridis_c(name = NULL, trans = "log2", na.value = "black") + 78 | coord_ip(pixel_prefix = 20) + 79 | theme_ip_dark() 80 | ``` 81 | -------------------------------------------------------------------------------- /tests/testthat/test-stat-summary-address.R: -------------------------------------------------------------------------------- 1 | test_that("input validation", { 2 | network_data <- data.frame(network = ip_network("0.0.0.0/16")) 3 | address_data <- data.frame(address = ip_address("0.0.0.0")) 4 | 5 | expect_snapshot_error( 6 | ggplot_build(ggplot(address_data) + 7 | stat_summary_address(aes(ip = address))) 8 | ) 9 | expect_snapshot_error( 10 | ggplot_build(ggplot(network_data) + 11 | coord_ip() + 12 | stat_summary_address(aes(ip = network))) 13 | ) 14 | 15 | expect_snapshot_error( 16 | ggplot_build(ggplot(address_data) + 17 | coord_ip() + 18 | stat_summary_address()) 19 | ) 20 | expect_snapshot_error( 21 | ggplot_build(ggplot(address_data, aes(ip = address)) + 22 | coord_ip() + 23 | stat_summary_address(inherit.aes = FALSE)) 24 | ) 25 | expect_snapshot_error( 26 | ggplot_build(ggplot(address_data) + 27 | coord_ip() + 28 | stat_summary_address(aes(ip = ip_address("0.0.0.0")))) 29 | ) 30 | expect_snapshot_error( 31 | ggplot_build(ggplot(address_data) + 32 | coord_ip() + 33 | stat_summary_address(aes(ip = address), fun = sum)) 34 | ) 35 | }) 36 | 37 | test_that("alternative ways to specify data/aesthetics", { 38 | dat <- data.frame( 39 | ip = ip_address(c("0.0.0.0", "0.0.0.0", "255.255.255.255")) 40 | ) 41 | 42 | p1 <- ggplot() + 43 | coord_ip(pixel_prefix = 2) + 44 | stat_summary_address(aes(ip = ip), data = dat) 45 | 46 | p2 <- ggplot(dat) + 47 | coord_ip(pixel_prefix = 2) + 48 | stat_summary_address(aes(ip = ip)) 49 | 50 | p3 <- ggplot(dat, aes(ip = ip)) + 51 | coord_ip(pixel_prefix = 2) + 52 | stat_summary_address() 53 | 54 | expect_equal(layer_data(p1), layer_data(p2)) 55 | expect_equal(layer_data(p1), layer_data(p3)) 56 | 57 | expect_equal(layer_data(p1)$x, c(0, 0, 1, 1)) 58 | expect_equal(layer_data(p1)$y, c(0, 1, 0, 1)) 59 | expect_equal(layer_data(p1)$count, c(0, 2, 0, 1)) 60 | }) 61 | 62 | test_that("alternative ways to specify summary function", { 63 | dat <- data.frame( 64 | ip = ip_address(c("0.0.0.0", "0.0.0.0", "255.255.255.255")), 65 | z = c(1, 2, 3) 66 | ) 67 | 68 | p_base <- ggplot(dat, aes(ip = ip, z = z)) + 69 | coord_ip(pixel_prefix = 2) 70 | 71 | p1 <- p_base + stat_summary_address(fun = "mean") 72 | p2 <- p_base + stat_summary_address(fun = mean) 73 | p3 <- p_base + stat_summary_address(fun = ~ mean(.x)) 74 | 75 | expect_equal(layer_data(p1), layer_data(p2)) 76 | expect_equal(layer_data(p1), layer_data(p3)) 77 | }) 78 | 79 | test_that("addresses outside 2D grid raise warning", { 80 | dat <- data.frame(ip = ip_address(c("0.0.0.0", "255.255.255.255"))) 81 | 82 | p <- ggplot(dat, aes(ip = ip)) + 83 | coord_ip( 84 | canvas_network = ip_network("0.0.0.0/2"), 85 | pixel_prefix = 2 86 | ) 87 | 88 | expect_warning(layer_data(p + stat_summary_address())) 89 | expect_silent(layer_data(p + stat_summary_address(na.rm = TRUE))) 90 | }) 91 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to ggip 2 | 3 | This outlines how to propose a change to ggip. 4 | For more detailed info about contributing to this, and other tidyverse packages, please see the 5 | [**development contributing guide**](https://rstd.io/tidy-contrib). 6 | 7 | ## Fixing typos 8 | 9 | You can fix typos, spelling mistakes, or grammatical errors in the documentation directly using the GitHub web interface, as long as the changes are made in the _source_ file. 10 | This generally means you'll need to edit [roxygen2 comments](https://roxygen2.r-lib.org/articles/roxygen2.html) in an `.R`, not a `.Rd` file. 11 | You can find the `.R` file that generates the `.Rd` by reading the comment in the first line. 12 | 13 | ## Bigger changes 14 | 15 | If you want to make a bigger change, it's a good idea to first file an issue and make sure someone from the team agrees that it’s needed. 16 | If you’ve found a bug, please file an issue that illustrates the bug with a minimal 17 | [reprex](https://www.tidyverse.org/help/#reprex) (this will also help you write a unit test, if needed). 18 | 19 | ### Pull request process 20 | 21 | * Fork the package and clone onto your computer. If you haven't done this before, we recommend using `usethis::create_from_github("", fork = TRUE)`. 22 | 23 | * Install all development dependences with `devtools::install_dev_deps()`, and then make sure the package passes R CMD check by running `devtools::check()`. 24 | If R CMD check doesn't pass cleanly, it's a good idea to ask for help before continuing. 25 | * Create a Git branch for your pull request (PR). We recommend using `usethis::pr_init("brief-description-of-change")`. 26 | 27 | * Make your changes, commit to git, and then create a PR by running `usethis::pr_push()`, and following the prompts in your browser. 28 | The title of your PR should briefly describe the change. 29 | The body of your PR should contain `Fixes #issue-number`. 30 | 31 | * For user-facing changes, add a bullet to the top of `NEWS.md` (i.e. just below the first header). Follow the style described in . 32 | 33 | ### Code style 34 | 35 | * New code should follow the tidyverse [style guide](https://style.tidyverse.org). 36 | You can use the [styler](https://CRAN.R-project.org/package=styler) package to apply these styles, but please don't restyle code that has nothing to do with your PR. 37 | 38 | * We use [roxygen2](https://cran.r-project.org/package=roxygen2), with [Markdown syntax](https://cran.r-project.org/web/packages/roxygen2/vignettes/rd-formatting.html), for documentation. 39 | 40 | * We use [testthat](https://cran.r-project.org/package=testthat) for unit tests. 41 | Contributions with test cases included are easier to accept. 42 | 43 | ## Code of Conduct 44 | 45 | Please note that the ggip project is released with a 46 | [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By contributing to this 47 | project you agree to abide by its terms. 48 | -------------------------------------------------------------------------------- /tests/testthat/test-geom-hilbert-outline.R: -------------------------------------------------------------------------------- 1 | test_that("input validation", { 2 | address_data <- data.frame(address = ip_address("0.0.0.0")) 3 | network_data <- data.frame(network = ip_network("0.0.0.0/16")) 4 | 5 | expect_snapshot_error( 6 | print(ggplot(network_data) + geom_hilbert_outline(aes(ip = network))) 7 | ) 8 | expect_snapshot_error( 9 | print(ggplot(address_data) + coord_ip() + geom_hilbert_outline(aes(ip = address))) 10 | ) 11 | expect_snapshot_error( 12 | print(ggplot(network_data) + coord_ip(curve = "morton") + geom_hilbert_outline(aes(ip = network))) 13 | ) 14 | }) 15 | 16 | test_that("alternative ways to specify data/aesthetics", { 17 | dat <- data.frame( 18 | ip = ip_network(c("0.0.0.0/2", "128.0.0.0/4")) 19 | ) 20 | 21 | p1 <- ggplot() + 22 | coord_ip() + 23 | geom_hilbert_outline(aes(ip = ip), data = dat) 24 | 25 | p2 <- ggplot(dat) + 26 | coord_ip() + 27 | geom_hilbert_outline(aes(ip = ip)) 28 | 29 | p3 <- ggplot(dat, aes(ip = ip)) + 30 | coord_ip() + 31 | geom_hilbert_outline() 32 | 33 | g1 <- layer_grob(p1)[[1]] 34 | g2 <- layer_grob(p2)[[1]] 35 | g3 <- layer_grob(p3)[[1]] 36 | 37 | expect_s3_class(g1, "segments") 38 | expect_s3_class(g2, "segments") 39 | expect_s3_class(g3, "segments") 40 | 41 | expect_equal(g1$x0, g2$x0) 42 | expect_equal(g1$x0, g3$x0) 43 | expect_equal(g1$y0, g2$y0) 44 | expect_equal(g1$y0, g3$y0) 45 | expect_equal(g1$x1, g2$x1) 46 | expect_equal(g1$x1, g3$x1) 47 | expect_equal(g1$y1, g2$y1) 48 | expect_equal(g1$y1, g3$y1) 49 | }) 50 | 51 | test_that("works without data", { 52 | p <- ggplot() + coord_ip() + geom_hilbert_outline() 53 | g <- layer_grob(p)[[1]] 54 | 55 | expect_s3_class(g, "segments") 56 | }) 57 | 58 | test_that("validate drawn segments", { 59 | expect_segments <- function(curve_order, closed) { 60 | n_segments <- (2^curve_order + 1)^2 61 | n_segments <- ifelse(closed, n_segments, n_segments - 2) 62 | 63 | p <- ggplot() + 64 | coord_ip() + 65 | geom_hilbert_outline(curve_order = curve_order, closed = closed) 66 | 67 | g <- layer_grob(p)[[1]] 68 | 69 | expect_length(g$x0, n_segments) 70 | } 71 | 72 | expect_segments(1, FALSE) 73 | expect_segments(2, TRUE) 74 | expect_segments(3, FALSE) 75 | expect_segments(4, TRUE) 76 | }) 77 | 78 | test_that("networks outside 2D grid raise warning", { 79 | dat <- data.frame(ip = ip_network("128.0.0.0/4")) 80 | 81 | p <- ggplot(dat, aes(ip = ip)) + 82 | coord_ip(canvas_network = ip_network("0.0.0.0/2")) 83 | 84 | expect_warning(layer_grob(p + geom_hilbert_outline())) 85 | expect_silent(layer_grob(p + geom_hilbert_outline(na.rm = TRUE))) 86 | }) 87 | 88 | test_that("networks without outline are silently ignored", { 89 | dat <- data.frame(ip = ip_network("128.0.0.0/4")) 90 | 91 | p <- ggplot(dat, aes(ip = ip)) + 92 | coord_ip() + 93 | geom_hilbert_outline(curve_order = 2) 94 | 95 | expect_silent(layer_grob(p)) 96 | expect_s3_class(layer_grob(p)[[1]], "zeroGrob") 97 | }) 98 | -------------------------------------------------------------------------------- /man/coord_ip.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/coord-ip.R 3 | \name{coord_ip} 4 | \alias{coord_ip} 5 | \title{Coordinate system for IP data} 6 | \usage{ 7 | coord_ip( 8 | canvas_network = ip_network("0.0.0.0/0"), 9 | pixel_prefix = 16, 10 | curve = c("hilbert", "morton"), 11 | expand = FALSE 12 | ) 13 | } 14 | \arguments{ 15 | \item{canvas_network}{An \code{\link[ipaddress:ip_network]{ip_network}} scalar that 16 | determines the region of IP space visualized by the entire 2D grid. The 17 | default shows the entire IPv4 address space.} 18 | 19 | \item{pixel_prefix}{An integer scalar that sets the prefix length of the 20 | network represented by a single pixel. The default value is 16. Increasing 21 | this effectively improves the resolution of the plot.} 22 | 23 | \item{curve}{A string to choose the space-filling curve. Choices are 24 | \code{"hilbert"} (default) and \code{"morton"}.} 25 | 26 | \item{expand}{If \code{TRUE}, adds a small expanded margin around the data grid. 27 | The default is \code{FALSE}.} 28 | } 29 | \description{ 30 | A ggplot2 coordinate system that maps a range of IP address space onto a 31 | two-dimensional grid using a space-filling curve. 32 | 33 | \code{coord_ip()} forms the foundation of any ggip plot. It translates all 34 | \code{\link[ipaddress:ip_address]{ip_address}} and \code{\link[ipaddress:ip_network]{ip_network}} 35 | vectors to Cartesian coordinates, ready for use by ggplot2 layers (see 36 | Accessing Coordinates). This ensures all layers use a common mapping. 37 | } 38 | \section{Accessing Coordinates}{ 39 | 40 | \code{coord_ip()} stores the result of the mapping in a nested data frame column. 41 | This means each \code{\link[ipaddress:ip_address]{ip_address}} or 42 | \code{\link[ipaddress:ip_network]{ip_network}} column in the original data set is 43 | converted to a data frame column. When specifying ggplot2 aesthetics, you'll 44 | need to use \code{$} to access the nested data (see Examples). 45 | 46 | Each \code{\link[ipaddress:ip_address]{ip_address}} column will be replaced with a 47 | data frame containing the following columns:\tabular{lll}{ 48 | Column name \tab Data type \tab Description \cr 49 | \code{ip} \tab \code{ip_address} \tab Original IP data \cr 50 | \code{x} \tab \code{integer} \tab Pixel x \cr 51 | \code{y} \tab \code{integer} \tab Pixel y \cr 52 | } 53 | 54 | 55 | Each \code{\link[ipaddress:ip_network]{ip_network}} column will be replaced with a 56 | data frame containing the following columns:\tabular{lll}{ 57 | Column name \tab Data type \tab Description \cr 58 | \code{ip} \tab \code{ip_network} \tab Original IP data \cr 59 | \code{xmin} \tab \code{integer} \tab Bounding box xmin \cr 60 | \code{ymin} \tab \code{integer} \tab Bounding box ymin \cr 61 | \code{xmax} \tab \code{integer} \tab Bounding box xmax \cr 62 | \code{ymax} \tab \code{integer} \tab Bounding box ymax \cr 63 | } 64 | } 65 | 66 | \examples{ 67 | suppressPackageStartupMessages(library(dplyr)) 68 | 69 | tibble(address = ip_address(c("0.0.0.0", "128.0.0.0", "192.168.0.1"))) \%>\% 70 | ggplot(aes(x = address$x, y = address$y, label = address$ip)) + 71 | geom_point() + 72 | geom_label(nudge_x = c(10, 0, -10), nudge_y = -10) + 73 | coord_ip(expand = TRUE) + 74 | theme_ip_light() 75 | 76 | tibble(network = ip_network(c("0.0.0.0/8", "224.0.0.0/4"))) \%>\% 77 | mutate( 78 | start = network_address(network), 79 | end = broadcast_address(network) 80 | ) \%>\% 81 | ggplot() + 82 | geom_point(aes(x = start$x, y = start$y), color = "blue") + 83 | geom_point(aes(x = end$x, y = end$y), color = "red") + 84 | geom_rect( 85 | aes(xmin = network$xmin, xmax = network$xmax, ymin = network$ymin, ymax = network$ymax), 86 | alpha = 0.5, fill = "grey" 87 | ) + 88 | coord_ip(curve = "morton", expand = TRUE) + 89 | theme_ip_light() 90 | } 91 | \seealso{ 92 | \code{vignette("visualizing-ip-data")} describes the mapping in more detail. 93 | } 94 | -------------------------------------------------------------------------------- /man/geom_hilbert_outline.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/geom-hilbert-outline.R 3 | \name{geom_hilbert_outline} 4 | \alias{geom_hilbert_outline} 5 | \title{Hilbert curve outline} 6 | \usage{ 7 | geom_hilbert_outline( 8 | mapping = NULL, 9 | data = NULL, 10 | ..., 11 | na.rm = FALSE, 12 | show.legend = NA, 13 | inherit.aes = TRUE 14 | ) 15 | } 16 | \arguments{ 17 | \item{mapping}{Set of aesthetic mappings created by \code{\link[ggplot2:aes]{aes()}}. If specified and 18 | \code{inherit.aes = TRUE} (the default), it is combined with the default mapping 19 | at the top level of the plot. You must supply \code{mapping} if there is no plot 20 | mapping.} 21 | 22 | \item{data}{The data to be displayed in this layer. There are three 23 | options: 24 | 25 | If \code{NULL}, the default, the data is inherited from the plot 26 | data as specified in the call to \code{\link[ggplot2:ggplot]{ggplot()}}. 27 | 28 | A \code{data.frame}, or other object, will override the plot 29 | data. All objects will be fortified to produce a data frame. See 30 | \code{\link[ggplot2:fortify]{fortify()}} for which variables will be created. 31 | 32 | A \code{function} will be called with a single argument, 33 | the plot data. The return value must be a \code{data.frame}, and 34 | will be used as the layer data. A \code{function} can be created 35 | from a \code{formula} (e.g. \code{~ head(.x, 10)}).} 36 | 37 | \item{...}{Other arguments passed on to \code{\link[ggplot2:layer]{layer()}}. These are 38 | often aesthetics, used to set an aesthetic to a fixed value, like 39 | \code{colour = "red"} or \code{size = 3}. They may also be parameters 40 | to the paired geom/stat.} 41 | 42 | \item{na.rm}{If \code{FALSE}, the default, missing values are removed with 43 | a warning. If \code{TRUE}, missing values are silently removed.} 44 | 45 | \item{show.legend}{logical. Should this layer be included in the legends? 46 | \code{NA}, the default, includes if any aesthetics are mapped. 47 | \code{FALSE} never includes, and \code{TRUE} always includes. 48 | It can also be a named logical vector to finely select the aesthetics to 49 | display.} 50 | 51 | \item{inherit.aes}{If \code{FALSE}, overrides the default aesthetics, 52 | rather than combining with them. This is most useful for helper functions 53 | that define both data and aesthetics and shouldn't inherit behaviour from 54 | the default plot specification, e.g. \code{\link[ggplot2:borders]{borders()}}.} 55 | } 56 | \description{ 57 | Computes and draws the outline of the Hilbert curve used to map IP data to 58 | the Cartesian plane. By superimposing this outline on top of a ggip plot, 59 | it guides the eye to regions that are close in IP address space. 60 | } 61 | \section{Aesthetics}{ 62 | 63 | \code{geom_curve_outline()} understands the following aesthetics: 64 | \itemize{ 65 | \item \code{ip}: An \code{\link[ipaddress:ip_network]{ip_network}} column. By default, the 66 | entire Hilbert curve is shown. 67 | \item \code{curve_order}: How nested is the curve? (default: \code{3}). 68 | \item \code{closed}: Should the curve outline have closed ends? (default: \code{FALSE}). 69 | \item \code{alpha} 70 | \item \code{colour} 71 | \item \code{linetype} 72 | \item \code{linewidth} 73 | } 74 | } 75 | 76 | \section{Computed variables}{ 77 | 78 | 79 | \describe{ 80 | \item{x, y}{The start coordinates for the segment} 81 | \item{xend, yend}{The end coordinates for the segment} 82 | } 83 | } 84 | 85 | \examples{ 86 | p <- ggplot() + coord_ip() + theme_ip_light() 87 | 88 | # default shows curve across entire canvas 89 | p + geom_hilbert_outline() 90 | 91 | # only show subnetwork 92 | p + geom_hilbert_outline(ip = ip_network("128.0.0.0/2")) 93 | 94 | # increased nesting 95 | p + geom_hilbert_outline(curve_order = 4) 96 | 97 | # show multiple networks 98 | df <- data.frame( 99 | ip = ip_network(c("0.0.0.0/2", "128.0.0.0/4")), 100 | curve_order = c(4, 5), 101 | closed = c(FALSE, TRUE) 102 | ) 103 | p + geom_hilbert_outline( 104 | aes(ip = ip, curve_order = curve_order, closed = closed), 105 | data = df 106 | ) 107 | } 108 | -------------------------------------------------------------------------------- /src/network_to_bbox.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "mapping.h" 4 | #include "curves.h" 5 | 6 | using namespace Rcpp; 7 | using namespace ipaddress; 8 | 9 | 10 | struct BoundingBox { 11 | uint32_t xmin, xmax, ymin, ymax; 12 | }; 13 | 14 | BoundingBox network_to_bbox_hilbert(uint32_t first_pixel, unsigned int network_bits, AddressMapping mapping) { 15 | BoundingBox bbox; 16 | uint32_t x1, x2, y1, y2; 17 | unsigned int curve_order = (mapping.canvas_bits - mapping.pixel_bits) / 2; 18 | 19 | if (network_bits <= mapping.pixel_bits) { // no area 20 | hilbert_curve(first_pixel, curve_order, &x1, &y1); 21 | 22 | bbox.xmin = x1; 23 | bbox.ymin = y1; 24 | bbox.xmax = x1; 25 | bbox.ymax = y1; 26 | } else if (((network_bits - mapping.pixel_bits) & 1) == 0) { // square 27 | uint32_t diag = 0xAAAAAAAA; 28 | uint32_t last_pixel = first_pixel | (diag >> (32 - (network_bits - mapping.pixel_bits))); 29 | 30 | hilbert_curve(first_pixel, curve_order, &x1, &y1); 31 | hilbert_curve(last_pixel, curve_order, &x2, &y2); 32 | 33 | bbox.xmin = std::min(x1, x2); 34 | bbox.ymin = std::min(y1, y2); 35 | bbox.xmax = std::max(x1, x2); 36 | bbox.ymax = std::max(y1, y2); 37 | } else { // rectangle 38 | network_bits--; 39 | uint32_t mid_pixel = first_pixel | (1 << (network_bits - mapping.pixel_bits)); 40 | 41 | BoundingBox square1 = network_to_bbox_hilbert(first_pixel, network_bits, mapping); 42 | BoundingBox square2 = network_to_bbox_hilbert(mid_pixel, network_bits, mapping); 43 | 44 | bbox.xmin = std::min(square1.xmin, square2.xmin); 45 | bbox.ymin = std::min(square1.ymin, square2.ymin); 46 | bbox.xmax = std::max(square1.xmax, square2.xmax); 47 | bbox.ymax = std::max(square1.ymax, square2.ymax); 48 | } 49 | 50 | return bbox; 51 | } 52 | 53 | BoundingBox network_to_bbox(const IpNetwork &network, AddressMapping mapping, bool is_morton) { 54 | uint32_t first_pixel = address_to_integer(network.address(), mapping); 55 | 56 | if (is_morton) { 57 | BoundingBox bbox; 58 | uint32_t x1, x2, y1, y2; 59 | unsigned int curve_order = (mapping.canvas_bits - mapping.pixel_bits) / 2; 60 | 61 | uint32_t last_pixel = address_to_integer(broadcast_address(network), mapping); 62 | 63 | morton_curve(first_pixel, curve_order, &x1, &y1); 64 | morton_curve(last_pixel, curve_order, &x2, &y2); 65 | 66 | bbox.xmin = std::min(x1, x2); 67 | bbox.ymin = std::min(y1, y2); 68 | bbox.xmax = std::max(x1, x2); 69 | bbox.ymax = std::max(y1, y2); 70 | 71 | return bbox; 72 | } else { 73 | unsigned int network_bits = mapping.space_bits - network.prefix_length(); 74 | return network_to_bbox_hilbert(first_pixel, network_bits, mapping); 75 | } 76 | } 77 | 78 | // [[Rcpp::export]] 79 | DataFrame wrap_network_to_cartesian(List network_r, List canvas_network_r, int pixel_prefix, String curve) { 80 | std::vector network = decode_networks(network_r); 81 | std::vector canvas_networks = decode_networks(canvas_network_r); 82 | 83 | if (canvas_networks.size() != 1) { 84 | stop("'canvas_network' must be an ip_network scalar"); // # nocov 85 | } 86 | IpNetwork canvas_network = canvas_networks[0]; 87 | 88 | // initialize output vectors 89 | std::size_t vsize = network.size(); 90 | IntegerVector out_xmin(vsize); 91 | IntegerVector out_ymin(vsize); 92 | IntegerVector out_xmax(vsize); 93 | IntegerVector out_ymax(vsize); 94 | 95 | // setup mapping from IP space to plotting canvas 96 | AddressMapping mapping = setup_mapping(canvas_network, pixel_prefix); 97 | 98 | // setup curve 99 | bool is_morton = (curve == "morton"); 100 | 101 | for (std::size_t i=0; i max_prefix_length(canvas_network)) { 73 | space <- ifelse(is_ipv6(canvas_network), "IPv6", "IPv4") 74 | cli::cli_abort(c( 75 | "Pixel prefix length must not be greater than {max_prefix_length(canvas_network)}.", 76 | "i" = "Canvas uses {space} address space.", 77 | "x" = "Pixel prefix length is {pixel_prefix}." 78 | )) 79 | } 80 | 81 | n_bits <- pixel_prefix - prefix_length(canvas_network) 82 | if (n_bits < 0) { 83 | cli::cli_abort(c( 84 | "Pixel prefix length must be greater than canvas.", 85 | "x" = "Canvas has prefix length {prefix_length(canvas_network)}.", 86 | "x" = "Pixel has prefix length {pixel_prefix}." 87 | )) 88 | } 89 | 90 | if (n_bits %% 2 != 0) { 91 | cli::cli_abort(c( 92 | "The difference between canvas and pixel prefix lengths must be even.", 93 | "x" = "Canvas has prefix length {prefix_length(canvas_network)}.", 94 | "x" = "Pixel has prefix length {pixel_prefix}." 95 | )) 96 | } 97 | 98 | # enforce a sensible maximum resolution (16.7 million pixels) 99 | if (n_bits > 24) { 100 | n_pixels <- format(2^(n_bits / 2), big.mark = ",") 101 | cli::cli_abort(c( 102 | "The difference between canvas and pixel prefix lengths must not be greater than 24.", 103 | "x" = "Canvas has prefix length {prefix_length(canvas_network)}.", 104 | "x" = "Pixel has prefix length {pixel_prefix}.", 105 | "i" = "These values would produce a plot with {n_pixels} x {n_pixels} pixels." 106 | )) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /man/stat_summary_address.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/stat-summary-address.R 3 | \name{stat_summary_address} 4 | \alias{stat_summary_address} 5 | \title{Summarize IP addresses on a heatmap} 6 | \usage{ 7 | stat_summary_address( 8 | mapping = NULL, 9 | data = NULL, 10 | ..., 11 | fun = NULL, 12 | fun.args = list(), 13 | na.rm = FALSE, 14 | show.legend = NA, 15 | inherit.aes = TRUE 16 | ) 17 | } 18 | \arguments{ 19 | \item{mapping}{Set of aesthetic mappings created by \code{\link[ggplot2:aes]{aes()}}. If specified and 20 | \code{inherit.aes = TRUE} (the default), it is combined with the default mapping 21 | at the top level of the plot. You must supply \code{mapping} if there is no plot 22 | mapping.} 23 | 24 | \item{data}{The data to be displayed in this layer. There are three 25 | options: 26 | 27 | If \code{NULL}, the default, the data is inherited from the plot 28 | data as specified in the call to \code{\link[ggplot2:ggplot]{ggplot()}}. 29 | 30 | A \code{data.frame}, or other object, will override the plot 31 | data. All objects will be fortified to produce a data frame. See 32 | \code{\link[ggplot2:fortify]{fortify()}} for which variables will be created. 33 | 34 | A \code{function} will be called with a single argument, 35 | the plot data. The return value must be a \code{data.frame}, and 36 | will be used as the layer data. A \code{function} can be created 37 | from a \code{formula} (e.g. \code{~ head(.x, 10)}).} 38 | 39 | \item{...}{Other arguments passed on to \code{\link[ggplot2:layer]{layer()}}. These are 40 | often aesthetics, used to set an aesthetic to a fixed value, like 41 | \code{colour = "red"} or \code{size = 3}. They may also be parameters 42 | to the paired geom/stat.} 43 | 44 | \item{fun}{Summary function (see section below for details). If \code{NULL} (the 45 | default), the observations are simply counted.} 46 | 47 | \item{fun.args}{A list of extra arguments to pass to \code{fun}.} 48 | 49 | \item{na.rm}{If \code{FALSE}, the default, missing values are removed with 50 | a warning. If \code{TRUE}, missing values are silently removed.} 51 | 52 | \item{show.legend}{logical. Should this layer be included in the legends? 53 | \code{NA}, the default, includes if any aesthetics are mapped. 54 | \code{FALSE} never includes, and \code{TRUE} always includes. 55 | It can also be a named logical vector to finely select the aesthetics to 56 | display.} 57 | 58 | \item{inherit.aes}{If \code{FALSE}, overrides the default aesthetics, 59 | rather than combining with them. This is most useful for helper functions 60 | that define both data and aesthetics and shouldn't inherit behaviour from 61 | the default plot specification, e.g. \code{\link[ggplot2:borders]{borders()}}.} 62 | } 63 | \description{ 64 | Addresses are grouped into networks determined by the \code{pixel_prefix} argument 65 | of \code{coord_ip()}. Then the \code{z} values are summarized with summary function \code{fun}. 66 | } 67 | \section{Aesthetics}{ 68 | 69 | \code{stat_summary_address()} understands the following aesthetics (required 70 | aesthetics are in bold): 71 | \itemize{ 72 | \item \strong{\code{ip}}: An \code{\link[ipaddress:ip_address]{ip_address}} column 73 | \item \code{z}: Value passed to the summary function (required if \code{fun} is used) 74 | \item \code{fill}: Default is \code{after_stat(value)} 75 | \item \code{alpha} 76 | } 77 | } 78 | 79 | \section{Computed variables}{ 80 | 81 | The following variables are available to \code{\link[ggplot2:aes_eval]{after_stat()}}: 82 | \itemize{ 83 | \item \code{value}: Value of summary statistic 84 | \item \code{count}: Number of observations 85 | } 86 | } 87 | 88 | \section{Summary function}{ 89 | 90 | The \code{data} might contain multiple rows per pixel of the heatmap, so a summary 91 | function reduces this information to a single value to display. 92 | This function receives the \code{data} column specified by the \code{z} aesthetic 93 | and also receives arguments specified by \code{fun.args}. 94 | 95 | The \code{fun} argument can be specified in multiple ways: 96 | \describe{ 97 | \item{\code{NULL}}{If no summary function is provided, the number of observations 98 | is computed. In this case, you don't need to specify the \code{z} aesthetic, 99 | and the computed variables \code{value} and \code{count} will be equal.} 100 | \item{string}{The name of an existing function (e.g. \code{fun = "mean"}).} 101 | \item{function}{Either provide an existing function (e.g. \code{fun = mean}) or 102 | define a new function (e.g. \code{fun = function(x) sum(x^2)}).} 103 | \item{formula}{A function can also be created from a formula. This uses \code{.x} 104 | as the summarized variable (e.g. \code{fun = ~ sum(.x^2)}).} 105 | } 106 | } 107 | 108 | \examples{ 109 | dat <- data.frame( 110 | ip = sample_ipv4(10000), 111 | weight = runif(10000) 112 | ) 113 | 114 | p <- ggplot(dat, aes(ip = ip)) + 115 | coord_ip() + 116 | theme_ip_light() 117 | 118 | # simple count of observations 119 | p + 120 | stat_summary_address() + 121 | scale_fill_viridis_c(trans = "log2", na.value = "black", guide = "none") 122 | 123 | # compute mean weight 124 | p + 125 | stat_summary_address(aes(z = weight), fun = ~ mean(.x)) + 126 | scale_fill_viridis_c(na.value = "black", guide = "none") 127 | } 128 | -------------------------------------------------------------------------------- /vignettes/ggip.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Introduction to ggip" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Introduction to ggip} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r, include=FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>", 14 | fig.align = "center", 15 | fig.asp = 1, 16 | fig.width = 5 17 | ) 18 | ``` 19 | 20 | The ggip package provides data visualization of IP addresses and networks. 21 | It achieves this by mapping one-dimensional IP data onto a two-dimensional grid. 22 | 23 | This introductory vignette gives a quickstart guide on how to use ggip functions to generate plots. 24 | 25 | ```{r setup, message=FALSE} 26 | library(ggplot2) 27 | library(dplyr) 28 | library(ipaddress) 29 | library(ggip) 30 | ``` 31 | 32 | 33 | ## Configuring the coordinate system 34 | 35 | The central component of any ggip plot is the coordinate system, as specified by `coord_ip()`. 36 | It determines exactly how IP data is mapped to the 2D grid, and ensures this mapping is applied consistently across all plotted layers. 37 | 38 | The `coord_ip()` function must be called once per plot, and takes arguments: 39 | 40 | Argument | Description 41 | :------------ | :----------------------------------------------------------- 42 | `canvas_network` | The region of IP space visualized by the entire 2D grid. By default, the entire IPv4 space is visualized. This argument allows you to zoom into a region of IP space or to visualize IPv6 space. 43 | `pixel_prefix` | The network prefix length corresponding to a pixel in the 2D grid. Increasing this argument effectively improves the resolution of the plot. 44 | `curve` | The path taken across the 2D grid while mapping IP data. 45 | 46 | More details about this mapping are found in `vignette("visualizing-ip-data")`. 47 | 48 | 49 | ## Internal data transformation 50 | 51 | Behind the scenes, ggip searches the plotted data sets for any columns that are `ip_address()` or `ip_network()` vectors. 52 | For each matching column, it replaces the vector with a data frame containing both the original IP data and the mapped Cartesian coordinates. 53 | This means the plotted data set now contains a nested data frame column. 54 | 55 | As an example, consider a data set featuring two columns. 56 | The `label` column is a character vector and the `address` column is an `ip_address()` vector. 57 | 58 | ```{r before_transform, echo=FALSE} 59 | tibble( 60 | label = c("A", "B", "C"), 61 | address = ip_address(c("0.0.0.0", "192.168.0.1", "255.255.255.255")) 62 | ) 63 | ``` 64 | 65 | This data set is transformed such that the `address` column is now a data frame. 66 | It contains an `ip` column with the original `ip_address()` vector, and `x` and `y` columns with the Cartesian coordinates on the 2D grid. 67 | 68 | ```{r after_transform, echo=FALSE} 69 | tibble( 70 | label = c("A", "B", "C"), 71 | address = tibble( 72 | ip = ip_address(c("0.0.0.0", "192.168.0.1", "255.255.255.255")), 73 | x = as.integer(c(0, 214, 255)), 74 | y = as.integer(c(255, 142, 255)) 75 | ) 76 | ) 77 | ``` 78 | 79 | These transformed data frame columns are available when specifying aesthetics. 80 | The nested columns can be accessed using the usual `$` syntax (see examples below). 81 | 82 | 83 | ## Using layers from ggplot2 and other packages 84 | 85 | Layers from ggplot2 and other external packages don't know about the internal data transformation used by ggip. 86 | For this reason, these layers expect their positional aesthetics (e.g. `x` and `y`) to be specified explicitly. 87 | Fortunately, we can extract the Cartesian coordinates from our data frame columns using the `$` syntax. 88 | 89 | As an example, we plot an `ip_address()` vector as points accompanied by labels. 90 | Note that we've specified the `x`, `y` and `label` aesthetics at the top level of the plot, and then the `geom_point()` and `geom_label()` layers have picked them up later. 91 | 92 | ```{r} 93 | tibble(address = ip_address(c("0.0.0.0", "128.0.0.0", "192.168.0.1"))) %>% 94 | ggplot(aes(x = address$x, y = address$y, label = address$ip)) + 95 | geom_point() + 96 | geom_label(nudge_x = c(10, 0, -10), nudge_y = -10) + 97 | coord_ip(expand = TRUE) + 98 | theme_ip_light() 99 | ``` 100 | 101 | Similarly, we plot `ip_network()` vectors using layers corresponding to rectangles. 102 | 103 | ```{r, fig.asp=0.8, fig.width=6.25} 104 | iana_ipv4 %>% 105 | ggplot(aes(xmin = network$xmin, ymin = network$ymin, xmax = network$xmax, ymax = network$ymax)) + 106 | geom_rect(aes(fill = allocation)) + 107 | scale_fill_brewer(palette = "Accent", name = NULL) + 108 | coord_ip() + 109 | theme_ip_dark() 110 | ``` 111 | 112 | **Note:** There are small gaps between the rectangles because networks are mapped onto a 2D grid (i.e. discrete), whereas ggplot2 visualizes the continuous 2D plane. 113 | This can be resolved by adding/subtracting 0.5 to the positional aesthetics. 114 | However, this gap is often helpful to distinguish networks. 115 | 116 | 117 | ## Using ggip layers 118 | 119 | Layers from ggip **do** know about the internal data transformation, so they take an `ip` aesthetic corresponding to the data frame column. 120 | They can then automatically extract the relevant positional information. 121 | This is easier because the name of the data frame column is also the name of the original `ip_address()` or `ip_network()` column in the input data set. 122 | 123 | As an example, we plot a heatmap of an `ip_address()` vector. 124 | 125 | ```{r} 126 | tibble(address = sample_ipv4(10000)) %>% 127 | ggplot(aes(ip = address)) + 128 | stat_summary_address() + 129 | scale_fill_viridis_c(guide = "none") + 130 | coord_ip() + 131 | theme_ip_dark() 132 | ``` 133 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards 42 | of acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies 54 | when an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail 56 | address, posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at [INSERT CONTACT 63 | METHOD]. All complaints will be reviewed and investigated promptly and fairly. 64 | 65 | All community leaders are obligated to respect the privacy and security of the 66 | reporter of any incident. 67 | 68 | ## Enforcement Guidelines 69 | 70 | Community leaders will follow these Community Impact Guidelines in determining 71 | the consequences for any action they deem in violation of this Code of Conduct: 72 | 73 | ### 1. Correction 74 | 75 | **Community Impact**: Use of inappropriate language or other behavior deemed 76 | unprofessional or unwelcome in the community. 77 | 78 | **Consequence**: A private, written warning from community leaders, providing 79 | clarity around the nature of the violation and an explanation of why the 80 | behavior was inappropriate. A public apology may be requested. 81 | 82 | ### 2. Warning 83 | 84 | **Community Impact**: A violation through a single incident or series of 85 | actions. 86 | 87 | **Consequence**: A warning with consequences for continued behavior. No 88 | interaction with the people involved, including unsolicited interaction with 89 | those enforcing the Code of Conduct, for a specified period of time. This 90 | includes avoiding interactions in community spaces as well as external channels 91 | like social media. Violating these terms may lead to a temporary or permanent 92 | ban. 93 | 94 | ### 3. Temporary Ban 95 | 96 | **Community Impact**: A serious violation of community standards, including 97 | sustained inappropriate behavior. 98 | 99 | **Consequence**: A temporary ban from any sort of interaction or public 100 | communication with the community for a specified period of time. No public or 101 | private interaction with the people involved, including unsolicited interaction 102 | with those enforcing the Code of Conduct, is allowed during this period. 103 | Violating these terms may lead to a permanent ban. 104 | 105 | ### 4. Permanent Ban 106 | 107 | **Community Impact**: Demonstrating a pattern of violation of community 108 | standards, including sustained inappropriate behavior, harassment of an 109 | individual, or aggression toward or disparagement of classes of individuals. 110 | 111 | **Consequence**: A permanent ban from any sort of public interaction within the 112 | community. 113 | 114 | ## Attribution 115 | 116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 117 | version 2.0, 118 | available at https://www.contributor-covenant.org/version/2/0/ 119 | code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at https:// 128 | www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /R/coord-ip.R: -------------------------------------------------------------------------------- 1 | #' Coordinate system for IP data 2 | #' 3 | #' @description 4 | #' A ggplot2 coordinate system that maps a range of IP address space onto a 5 | #' two-dimensional grid using a space-filling curve. 6 | #' 7 | #' `coord_ip()` forms the foundation of any ggip plot. It translates all 8 | #' [`ip_address`][`ipaddress::ip_address`] and [`ip_network`][`ipaddress::ip_network`] 9 | #' vectors to Cartesian coordinates, ready for use by ggplot2 layers (see 10 | #' Accessing Coordinates). This ensures all layers use a common mapping. 11 | #' 12 | #' @section Accessing Coordinates: 13 | #' `coord_ip()` stores the result of the mapping in a nested data frame column. 14 | #' This means each [`ip_address`][`ipaddress::ip_address`] or 15 | #' [`ip_network`][`ipaddress::ip_network`] column in the original data set is 16 | #' converted to a data frame column. When specifying ggplot2 aesthetics, you'll 17 | #' need to use `$` to access the nested data (see Examples). 18 | #' 19 | #' Each [`ip_address`][`ipaddress::ip_address`] column will be replaced with a 20 | #' data frame containing the following columns: 21 | #' 22 | #' | Column name | Data type | Description | 23 | #' |:------------|:-------------|:-----------------| 24 | #' | `ip` | `ip_address` | Original IP data | 25 | #' | `x` | `integer` | Pixel x | 26 | #' | `y` | `integer` | Pixel y | 27 | #' 28 | #' Each [`ip_network`][`ipaddress::ip_network`] column will be replaced with a 29 | #' data frame containing the following columns: 30 | #' 31 | #' | Column name | Data type | Description | 32 | #' |:------------|:-------------|:------------------| 33 | #' | `ip` | `ip_network` | Original IP data | 34 | #' | `xmin` | `integer` | Bounding box xmin | 35 | #' | `ymin` | `integer` | Bounding box ymin | 36 | #' | `xmax` | `integer` | Bounding box xmax | 37 | #' | `ymax` | `integer` | Bounding box ymax | 38 | #' 39 | #' @inheritParams ip_to_cartesian 40 | #' @param expand If `TRUE`, adds a small expanded margin around the data grid. 41 | #' The default is `FALSE`. 42 | #' 43 | #' @examples 44 | #' suppressPackageStartupMessages(library(dplyr)) 45 | #' 46 | #' tibble(address = ip_address(c("0.0.0.0", "128.0.0.0", "192.168.0.1"))) %>% 47 | #' ggplot(aes(x = address$x, y = address$y, label = address$ip)) + 48 | #' geom_point() + 49 | #' geom_label(nudge_x = c(10, 0, -10), nudge_y = -10) + 50 | #' coord_ip(expand = TRUE) + 51 | #' theme_ip_light() 52 | #' 53 | #' tibble(network = ip_network(c("0.0.0.0/8", "224.0.0.0/4"))) %>% 54 | #' mutate( 55 | #' start = network_address(network), 56 | #' end = broadcast_address(network) 57 | #' ) %>% 58 | #' ggplot() + 59 | #' geom_point(aes(x = start$x, y = start$y), color = "blue") + 60 | #' geom_point(aes(x = end$x, y = end$y), color = "red") + 61 | #' geom_rect( 62 | #' aes(xmin = network$xmin, xmax = network$xmax, ymin = network$ymin, ymax = network$ymax), 63 | #' alpha = 0.5, fill = "grey" 64 | #' ) + 65 | #' coord_ip(curve = "morton", expand = TRUE) + 66 | #' theme_ip_light() 67 | #' @seealso 68 | #' `vignette("visualizing-ip-data")` describes the mapping in more detail. 69 | #' @export 70 | coord_ip <- function(canvas_network = ip_network("0.0.0.0/0"), 71 | pixel_prefix = 16, 72 | curve = c("hilbert", "morton"), 73 | expand = FALSE) { 74 | curve <- arg_match(curve) 75 | curve_order <- as.integer((pixel_prefix - prefix_length(canvas_network)) / 2) 76 | lim <- as.integer(c(0, 2^curve_order - 1)) 77 | 78 | ggplot2::ggproto(NULL, CoordIp, 79 | canvas_network = canvas_network, 80 | pixel_prefix = pixel_prefix, 81 | curve = curve, 82 | curve_order = curve_order, 83 | limits = list(x = lim, y = lim), 84 | expand = expand, 85 | ratio = 1 86 | ) 87 | } 88 | 89 | 90 | CoordIp <- ggplot2::ggproto("CoordIp", ggplot2::CoordFixed, 91 | 92 | # CoordIp needs to keep track of some parameters 93 | # internally as the plot is built. These are stored here. 94 | params = list(), 95 | 96 | setup_params = function(self, data) { 97 | list( 98 | canvas_network = self$canvas_network, 99 | pixel_prefix = self$pixel_prefix, 100 | curve = self$curve, 101 | curve_order = self$curve_order 102 | ) 103 | }, 104 | 105 | setup_data = function(data, params) { 106 | lapply(data, function(layer_data) { 107 | for (col in colnames(layer_data)) { 108 | 109 | # ip_address --> ip_address_coords 110 | if (is_ip_address(layer_data[[col]])) { 111 | coords <- address_to_cartesian( 112 | layer_data[[col]], params$canvas_network, params$pixel_prefix, params$curve 113 | ) 114 | layer_data[[col]] <- ip_address_coords( 115 | ip = layer_data[[col]], 116 | x = coords$x, y = coords$y 117 | ) 118 | } 119 | 120 | # ip_network --> ip_network_coords 121 | else if (is_ip_network(layer_data[[col]])) { 122 | coords <- network_to_cartesian( 123 | layer_data[[col]], params$canvas_network, params$pixel_prefix, params$curve 124 | ) 125 | layer_data[[col]] <- ip_network_coords( 126 | ip = layer_data[[col]], 127 | xmin = coords$xmin, ymin = coords$ymin, 128 | xmax = coords$xmax, ymax = coords$ymax 129 | ) 130 | } 131 | } 132 | 133 | layer_data 134 | }) 135 | } 136 | ) 137 | 138 | is_CoordIp <- function(x) inherits(x, "CoordIp") 139 | 140 | 141 | # ggplot2 scales ----------------------------------------------------------- 142 | # these prevent ggplot2 warnings 143 | 144 | #' @importFrom ggplot2 scale_type 145 | #' @export 146 | scale_type.ip_address <- function(x) "identity" 147 | 148 | #' @importFrom ggplot2 scale_type 149 | #' @export 150 | scale_type.ip_network <- function(x) "identity" 151 | -------------------------------------------------------------------------------- /R/stat-summary-address.R: -------------------------------------------------------------------------------- 1 | #' Summarize IP addresses on a heatmap 2 | #' 3 | #' Addresses are grouped into networks determined by the `pixel_prefix` argument 4 | #' of `coord_ip()`. Then the `z` values are summarized with summary function `fun`. 5 | #' 6 | #' @inheritParams ggplot2::layer 7 | #' @inheritParams ggplot2::geom_point 8 | #' @param fun Summary function (see section below for details). If `NULL` (the 9 | #' default), the observations are simply counted. 10 | #' @param fun.args A list of extra arguments to pass to `fun`. 11 | #' 12 | #' @section Aesthetics: 13 | #' `stat_summary_address()` understands the following aesthetics (required 14 | #' aesthetics are in bold): 15 | #' - **`ip`**: An [`ip_address`][`ipaddress::ip_address`] column 16 | #' - `z`: Value passed to the summary function (required if `fun` is used) 17 | #' - `fill`: Default is `after_stat(value)` 18 | #' - `alpha` 19 | #' 20 | #' @section Computed variables: 21 | #' The following variables are available to [`after_stat()`][ggplot2::after_stat()]: 22 | #' - `value`: Value of summary statistic 23 | #' - `count`: Number of observations 24 | #' 25 | #' @section Summary function: 26 | #' The `data` might contain multiple rows per pixel of the heatmap, so a summary 27 | #' function reduces this information to a single value to display. 28 | #' This function receives the `data` column specified by the `z` aesthetic 29 | #' and also receives arguments specified by `fun.args`. 30 | #' 31 | #' The `fun` argument can be specified in multiple ways: 32 | #' \describe{ 33 | #' \item{`NULL`}{If no summary function is provided, the number of observations 34 | #' is computed. In this case, you don't need to specify the `z` aesthetic, 35 | #' and the computed variables `value` and `count` will be equal.} 36 | #' \item{string}{The name of an existing function (e.g. `fun = "mean"`).} 37 | #' \item{function}{Either provide an existing function (e.g. `fun = mean`) or 38 | #' define a new function (e.g. `fun = function(x) sum(x^2)`).} 39 | #' \item{formula}{A function can also be created from a formula. This uses `.x` 40 | #' as the summarized variable (e.g. `fun = ~ sum(.x^2)`).} 41 | #' } 42 | #' 43 | #' @examples 44 | #' dat <- data.frame( 45 | #' ip = sample_ipv4(10000), 46 | #' weight = runif(10000) 47 | #' ) 48 | #' 49 | #' p <- ggplot(dat, aes(ip = ip)) + 50 | #' coord_ip() + 51 | #' theme_ip_light() 52 | #' 53 | #' # simple count of observations 54 | #' p + 55 | #' stat_summary_address() + 56 | #' scale_fill_viridis_c(trans = "log2", na.value = "black", guide = "none") 57 | #' 58 | #' # compute mean weight 59 | #' p + 60 | #' stat_summary_address(aes(z = weight), fun = ~ mean(.x)) + 61 | #' scale_fill_viridis_c(na.value = "black", guide = "none") 62 | #' @export 63 | stat_summary_address <- function(mapping = NULL, data = NULL, ..., 64 | fun = NULL, fun.args = list(), 65 | na.rm = FALSE, show.legend = NA, 66 | inherit.aes = TRUE) { 67 | ggplot2::layer( 68 | stat = StatSummaryAddress, data = data, mapping = mapping, geom = "raster", 69 | position = "identity", show.legend = show.legend, inherit.aes = inherit.aes, 70 | params = list( 71 | na.rm = na.rm, 72 | fun = fun, 73 | fun.args = fun.args, 74 | ... 75 | ) 76 | ) 77 | } 78 | 79 | StatSummaryAddress <- ggplot2::ggproto("StatSummaryAddress", ggplot2::Stat, 80 | 81 | default_aes = ggplot2::aes( 82 | ip = NULL, 83 | z = NULL, 84 | fill = ggplot2::after_stat(value) 85 | ), 86 | 87 | # The `ip` aesthetic is required, but putting it in required_aes causes a very 88 | # slow check for missing values. It's much faster to simply check `x` and `y` 89 | # for missing values. These are always NA when `ip` is NA. 90 | required_aes = c("x", "y"), 91 | 92 | dropped_aes = c("ip", "z"), 93 | 94 | extra_params = c("na.rm", "fun", "fun.args"), 95 | 96 | compute_layer = function(self, data, params, layout) { 97 | if (!is_CoordIp(layout$coord)) { 98 | cli::cli_abort("{.pkg ggip} plots require {.fn coord_ip}.") 99 | } 100 | 101 | # validate ip aesthetic 102 | if (is.null(data$ip)) { 103 | cli::cli_abort("{.fn {snake_class(self)}} requires an {.field ip} aesthetic.") 104 | } else if (is_ip_address_coords(data$ip)) { 105 | data$x <- data$ip$x 106 | data$y <- data$ip$y 107 | data$ip <- data$ip$ip 108 | } else if (is_ip_address(data$ip)) { 109 | cli::cli_abort("The {.field ip} aesthetic of {.fn {snake_class(self)}} must map to a {.arg data} variable.") 110 | } else { 111 | cli::cli_abort(c( 112 | "The {.field ip} aesthetic of {.fn {snake_class(self)}} must be {.type {ip_address()}}.", 113 | "x" = "You supplied {.type {data$ip}}." 114 | )) 115 | } 116 | 117 | if (!is.null(params$fun) && !("z" %in% colnames(data))) { 118 | cli::cli_abort("{.fn {snake_class(self)}} requires a {.field z} aesthetic when using the {.arg fun} argument.") 119 | } 120 | 121 | # add coord to the params, so it reaches compute_group() 122 | params$coord <- layout$coord 123 | ggproto_parent(Stat, self)$compute_layer(data, params, layout) 124 | }, 125 | 126 | compute_group = function(data, scales, coord, 127 | fun = NULL, fun.args = list(), ...) { 128 | summarize_addresses(data, scales, coord, fun, fun.args) 129 | } 130 | ) 131 | 132 | summarize_addresses <- function(data, scales, coord, fun, fun.args) { 133 | summarize_grid <- function(x, index, fun) { 134 | grps <- split(x, index) 135 | names(grps) <- NULL 136 | unlist(lapply(grps, fun)) 137 | } 138 | 139 | # support formula interface 140 | if (is_formula(fun)) { 141 | fun <- as_function(fun) 142 | } 143 | 144 | summarize_count <- is.null(fun) 145 | 146 | # summarize grid found in data 147 | index <- list(x = data$x, y = data$y) 148 | labels <- lapply(index, function(x) sort(unique(x))) 149 | out <- expand.grid(labels, KEEP.OUT.ATTRS = FALSE) 150 | 151 | out$count <- summarize_grid(data$x, index, length) 152 | out$value <- if (summarize_count) { 153 | out$count 154 | } else { 155 | f <- function(x) do.call(fun, c(list(quote(x)), fun.args)) 156 | summarize_grid(data$z, index, f) 157 | } 158 | 159 | # fill remaining grid so raster works 160 | range <- coord$limits$x[1]:coord$limits$x[2] 161 | fill_na <- list(count = 0) 162 | if (summarize_count) { 163 | fill_na$value <- 0 164 | } 165 | tidyr::complete(out, tidyr::expand(out, x = range, y = range), fill = fill_na) 166 | } 167 | -------------------------------------------------------------------------------- /vignettes/visualizing-ip-data.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Visualizing IP data" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Visualizing IP data} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r, include = FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>", 14 | echo = FALSE, 15 | message = FALSE 16 | ) 17 | ``` 18 | 19 | ```{r setup, warning=FALSE} 20 | library(ggip) 21 | ``` 22 | 23 | *This vignette assumes an understanding of IP addresses and networks.* 24 | *Please consult `vignette("ipaddress-classes", "ipaddress")` for a very basic introduction.* 25 | 26 | Data visualization of the IP address space is challenging because there are so many unique addresses (approximately 4.3 billion for IPv4 and $3.8 \times 10^{38}$ for IPv6). 27 | Owing to the hierarchical nature of address space, we must plot the addresses on a discrete scale (not a continuous scale). 28 | It's simply not possible to display (or interpret) such a large number of discrete levels simultaneously. 29 | 30 | There are a few actions we can take to improve the situation: 31 | 32 | 1. Visualize a reduced number of discrete levels by: 33 | a. Showing only a subnetwork of the full address space (i.e. filtering leading bits) 34 | b. Limiting the resolution by summarizing data within networks (i.e. neglecting trailing bits) 35 | 2. Transform the one-dimensional address space onto the two-dimensional plane 36 | 37 | These are handled by the `canvas_network`, `pixel_prefix` and `curve` arguments of `coord_ip()`, respectively. 38 | This vignette describes these actions in more detail. 39 | 40 | 41 | ## Reducing visualized information 42 | 43 | As an example, consider the 32-bit representation of the IPv4 address `192.168.0.124`. 44 | If we wanted to visualize this single address within the full context of the IPv4 address space, we'd need to simultaneously display $2^{32}$ discrete levels (roughly 4.3 billion). 45 | 46 | ```{r, out.width="100%"} 47 | knitr::include_graphics("bits_raw.png") 48 | ``` 49 | 50 | To reduce the visualized information, we could only show a subnetwork of the full address space. 51 | In our example, we could only display the `192.0.0.0/8` network using `coord_ip(canvas_network = ip_network("192.0.0.0/8"))`. 52 | This would effectively filter addresses where the leading 8 bits match the specified network, thereby reducing the number of discrete levels to $2^{24}$ (roughly 16.8 million). 53 | 54 | ```{r, out.width="100%"} 55 | knitr::include_graphics("bits_half_reduced.png") 56 | ``` 57 | 58 | Alternatively, we could make each discrete level represent a network of addresses. 59 | To do this, we'd need to use a summary function to reduce the network data to a single value. 60 | In our example, we could make each discrete level represent a network with a prefix length of 24 using `coord_ip(pixel_prefix = 24)`. 61 | This would effectively neglect the trailing 8 bits of the 32-bit address, thereby further reducing the number of discrete levels to $2^{16}$ (65,536). 62 | 63 | ```{r, out.width="100%"} 64 | knitr::include_graphics("bits_reduced.png") 65 | ``` 66 | 67 | These two techniques become even more important in the IPv6 address space, which uses 128-bit addresses. 68 | 69 | **Note:** To prevent accidentally plotting an unreasonably large number of discrete levels, ggip limits the number of plotted bits to 24. 70 | This means the `coord_ip()` arguments must satisfy: 71 | 72 | ``` 73 | pixel_prefix - prefix_length(canvas_network) <= 24 74 | ``` 75 | 76 | ## Space-filling curves 77 | 78 | Inspired by [an xkcd comic](https://xkcd.com/195/) originally published in December 2006, we use a space-filling curve to map IP data (one-dimensional) to Cartesian coordinates (two-dimensional). 79 | This means our discrete levels become represented by pixels. 80 | Two curves are commonly chosen for this task: the Hilbert curve and the Morton curve (also known as the Z curve). 81 | Compared to other space-filling curves, these are advantageous because they preserve locality (i.e. subnetworks remain close together). 82 | 83 | The curve order represents how nested the curve is and therefore determines how many data points can be visualized. 84 | Conversely, choosing the number of plotted bits (see above) determines the order of the curve. 85 | Since space-filling curves are fractal, increasing the curve order effectively improves the image resolution (plotted networks remain in the same overall location). 86 | 87 | ```{r plot_func} 88 | ordinal_suffix <- function(x) { 89 | suffix <- c("st", "nd", "rd", rep("th", 17)) 90 | suffix[((x-1) %% 10 + 1) + 10*(((x %% 100) %/% 10) == 1)] 91 | } 92 | 93 | plot_curve <- function(curve, curve_order) { 94 | pixel_prefix <- 32L 95 | canvas_prefix <- as.integer(pixel_prefix - (2 * curve_order)) 96 | canvas_network <- ip_network(ip_address("0.0.0.0"), canvas_prefix) 97 | n_pixels <- 2^curve_order 98 | 99 | ggplot(data.frame(address = seq(canvas_network))) + 100 | geom_path(aes(address$x, address$y)) + 101 | coord_ip( 102 | canvas_network = canvas_network, 103 | pixel_prefix = pixel_prefix, 104 | curve = curve, 105 | expand = TRUE 106 | ) + 107 | theme_ip_light() + 108 | labs(title = paste0( 109 | curve_order, ordinal_suffix(curve_order), 110 | " order (", n_pixels, "x", n_pixels, " grid)" 111 | )) 112 | } 113 | ``` 114 | 115 | ### Hilbert curve 116 | 117 | IP data is most commonly displayed on a Hilbert curve because it has optimal locality preservation. 118 | 119 | This curve starts in the top-left corner and ends in the top-right corner. 120 | It is chosen using `coord_ip(curve = "hilbert")`. 121 | 122 | ```{r hilbert, fig.show="hold", out.width="30%"} 123 | plot_curve("hilbert", 2) 124 | plot_curve("hilbert", 3) 125 | plot_curve("hilbert", 4) 126 | ``` 127 | 128 | 129 | ### Morton curve 130 | 131 | The Morton curve technically offers slightly poorer locality preservation than the Hilbert curve. 132 | However, the discontinuous jumps in the curve actually correspond to crossing IP network boundaries. 133 | In this sense, the Morton curve is a more natural representation of the IP network structure. 134 | For example, the start and end addresses of a network are always located diagonally across from each other. 135 | 136 | This curve starts in the top-left corner and ends in the bottom-right corner. 137 | It is chosen using `coord_ip(curve = "morton")`. 138 | 139 | ```{r morton, fig.show="hold", out.width="30%"} 140 | plot_curve("morton", 2) 141 | plot_curve("morton", 3) 142 | plot_curve("morton", 4) 143 | ``` 144 | 145 | 146 | ## Putting it all together 147 | 148 | Finally, let's consider a specific example. 149 | 150 | ```{r, echo=TRUE, eval=FALSE} 151 | coord_ip( 152 | canvas_network = ip_network("0.0.0.0/0"), 153 | pixel_prefix = 4, 154 | curve = "hilbert" 155 | ) 156 | ``` 157 | 158 | This coordinate system will use a 2nd order Hilbert curve to visualize the entire IPv4 address space, where each vertex represents a `/4` network. 159 | 160 | ```{r, fig.align="center", fig.asp=1, fig.width=5} 161 | curve_order <- 2 162 | pixel_prefix <- 2 * curve_order 163 | vertices <- subnets(ip_network("0.0.0.0/0"), new_prefix = pixel_prefix)[[1]] 164 | 165 | data <- data.frame(ip = network_address(vertices), label = as.character(vertices)) 166 | nudge <- c(1, 0, 0, 1, 1, 1, 0, 0, 0, 0, -1, -1, -1, 0, 0, -1) 167 | 168 | ggplot(data, aes(ip$x, ip$y)) + 169 | geom_path() + 170 | geom_label(aes(label = label), nudge_x = 0.2 * nudge) + 171 | coord_ip(pixel_prefix = pixel_prefix, expand = TRUE) + 172 | theme_ip_light() + 173 | labs(title = paste0("Hilbert curve: ", curve_order, ordinal_suffix(curve_order), " order")) 174 | ``` 175 | 176 | -------------------------------------------------------------------------------- /R/geom-hilbert-outline.R: -------------------------------------------------------------------------------- 1 | #' Hilbert curve outline 2 | #' 3 | #' Computes and draws the outline of the Hilbert curve used to map IP data to 4 | #' the Cartesian plane. By superimposing this outline on top of a ggip plot, 5 | #' it guides the eye to regions that are close in IP address space. 6 | #' 7 | #' @inheritParams ggplot2::layer 8 | #' @inheritParams ggplot2::geom_point 9 | #' 10 | #' @section Aesthetics: 11 | #' `geom_curve_outline()` understands the following aesthetics: 12 | #' - `ip`: An [`ip_network`][`ipaddress::ip_network`] column. By default, the 13 | #' entire Hilbert curve is shown. 14 | #' - `curve_order`: How nested is the curve? (default: `3`). 15 | #' - `closed`: Should the curve outline have closed ends? (default: `FALSE`). 16 | #' - `alpha` 17 | #' - `colour` 18 | #' - `linetype` 19 | #' - `linewidth` 20 | #' 21 | #' @section Computed variables: 22 | #' 23 | #' \describe{ 24 | #' \item{x, y}{The start coordinates for the segment} 25 | #' \item{xend, yend}{The end coordinates for the segment} 26 | #' } 27 | #' 28 | #' @examples 29 | #' p <- ggplot() + coord_ip() + theme_ip_light() 30 | #' 31 | #' # default shows curve across entire canvas 32 | #' p + geom_hilbert_outline() 33 | #' 34 | #' # only show subnetwork 35 | #' p + geom_hilbert_outline(ip = ip_network("128.0.0.0/2")) 36 | #' 37 | #' # increased nesting 38 | #' p + geom_hilbert_outline(curve_order = 4) 39 | #' 40 | #' # show multiple networks 41 | #' df <- data.frame( 42 | #' ip = ip_network(c("0.0.0.0/2", "128.0.0.0/4")), 43 | #' curve_order = c(4, 5), 44 | #' closed = c(FALSE, TRUE) 45 | #' ) 46 | #' p + geom_hilbert_outline( 47 | #' aes(ip = ip, curve_order = curve_order, closed = closed), 48 | #' data = df 49 | #' ) 50 | #' @export 51 | geom_hilbert_outline <- function(mapping = NULL, data = NULL, ..., 52 | na.rm = FALSE, show.legend = NA, 53 | inherit.aes = TRUE) { 54 | # can use layer without any data 55 | if (is.null(data)) { 56 | data <- ensure_nonempty_data 57 | } 58 | 59 | ggplot2::layer( 60 | geom = GeomHilbertOutline, data = data, mapping = mapping, stat = "identity", 61 | position = "identity", show.legend = show.legend, inherit.aes = inherit.aes, 62 | params = list( 63 | na.rm = na.rm, 64 | ... 65 | ) 66 | ) 67 | } 68 | 69 | GeomHilbertOutline <- ggplot2::ggproto("GeomHilbertOutline", ggplot2::Geom, 70 | default_aes = ggplot2::aes( 71 | ip = NULL, 72 | curve_order = 3, 73 | closed = FALSE, 74 | colour = "black", 75 | linewidth = 0.5, 76 | linetype = 1, 77 | alpha = NA 78 | ), 79 | 80 | draw_panel = function(self, data, panel_params, coord, na.rm = FALSE) { 81 | 82 | if (!is_CoordIp(coord)) { 83 | cli::cli_abort("{.pkg ggip} plots require {.fn coord_ip}.") 84 | } 85 | if (coord$curve != "hilbert") { 86 | cli::cli_abort('{.fn {snake_class(self)}} only works with {.code coord_ip(curve = "hilbert")}.') 87 | } 88 | 89 | # validate ip aesthetic 90 | if (is.null(data$ip)) { 91 | data$ip <- coord$canvas_network 92 | } else if (is_ip_network_coords(data$ip)) { 93 | data$ip <- data$ip$ip 94 | } 95 | if (!is_ip_network(data$ip)) { 96 | cli::cli_abort(c( 97 | "The {.field ip} aesthetic of {.fn {snake_class(self)}} must be {.type {ip_network()}}.", 98 | "x" = "You supplied {.type {data$ip}}." 99 | )) 100 | } 101 | 102 | segments <- data %>% 103 | dplyr::distinct() %>% 104 | networks_to_squares(coord) %>% 105 | squares_to_sides() %>% 106 | sides_to_segments(coord) %>% 107 | dplyr::distinct() 108 | 109 | ggplot2::GeomSegment$draw_panel(segments, panel_params, coord, 110 | lineend = "round", na.rm = na.rm) 111 | }, 112 | 113 | draw_key = ggplot2::draw_key_path 114 | ) 115 | 116 | #' Translate IP networks to paths of squares, to visualize their Hilbert curves. 117 | #' 118 | #' @param data A data.frame with 3 columns: `ip`, `curve_order`, `closed`. 119 | #' There is 1 row per network. 120 | #' @param coord The `CoordIp` coordinate system. 121 | #' @return A data.frame with 4 additional columns: `xmin`, `ymin`, `xmax`, `ymax`. 122 | #' There is 1 row per square of the path. 123 | #' 124 | #' @noRd 125 | networks_to_squares <- function(data, coord) { 126 | data %>% 127 | dplyr::mutate(prefix_curve = (2 * .data$curve_order) + prefix_length(coord$canvas_network)) %>% 128 | dplyr::filter(prefix_length(.data$ip) < .data$prefix_curve) %>% 129 | dplyr::mutate(network_curve = subnets(.data$ip, new_prefix = .data$prefix_curve)) %>% 130 | tidyr::unchop("network_curve") %>% 131 | dplyr::mutate(coords = network_to_cartesian( 132 | .data$network_curve, 133 | canvas_network = coord$canvas_network, 134 | pixel_prefix = coord$pixel_prefix, 135 | curve = coord$curve 136 | )) %>% 137 | dplyr::select(-"prefix_curve", -"network_curve") %>% 138 | tidyr::unnest("coords") 139 | } 140 | 141 | path_direction <- function(x_from, y_from, x_to, y_to) { 142 | factor(dplyr::case_when( 143 | x_to > x_from ~ "right", 144 | x_to < x_from ~ "left", 145 | y_to > y_from ~ "up", 146 | y_to < y_from ~ "down", 147 | TRUE ~ NA_character_ 148 | ), levels = c("right", "left", "up", "down")) 149 | } 150 | 151 | translate_endpoints <- function(side, opposite, closed) { 152 | dplyr::case_when( 153 | closed ~ side, 154 | !is.na(side) ~ side, 155 | opposite == "right" ~ factor("left"), 156 | opposite == "left" ~ factor("right"), 157 | opposite == "up" ~ factor("down"), 158 | opposite == "down" ~ factor("up") 159 | ) 160 | } 161 | 162 | snap_to_grid <- function(x, add_offset, limits) { 163 | dplyr::case_when( 164 | x %in% limits ~ as.numeric(x), 165 | add_offset ~ x + 0.5, 166 | TRUE ~ x - 0.5 167 | ) 168 | } 169 | 170 | #' Translate paths of squares to square sides (N sides per square). 171 | #' This accounts for path direction and closed/open endcaps. 172 | #' 173 | #' @param data A data.frame with 5 columns: `xmin`, `ymin`, `xmax`, `ymax`, `closed`. 174 | #' There is 1 row per square of the path. 175 | #' @return A data.frame with 5 columns: `xmin`, `ymin`, `xmax`, `ymax`, `side`. 176 | #' There is 1 row per side of the path outline. 177 | #' 178 | #' @noRd 179 | squares_to_sides <- function(data) { 180 | data %>% 181 | dplyr::mutate( 182 | xmid = (.data$xmin + .data$xmax) / 2, 183 | ymid = (.data$ymin + .data$ymax) / 2 184 | ) %>% 185 | dplyr::group_by(.data$ip) %>% 186 | dplyr::mutate( 187 | from = path_direction(.data$xmid, .data$ymid, dplyr::lag(.data$xmid), dplyr::lag(.data$ymid)), 188 | to = path_direction(.data$xmid, .data$ymid, dplyr::lead(.data$xmid), dplyr::lead(.data$ymid)) 189 | ) %>% 190 | dplyr::ungroup() %>% 191 | dplyr::mutate( 192 | from = translate_endpoints(.data$from, .data$to, .data$closed), 193 | to = translate_endpoints(.data$to, .data$from, .data$closed) 194 | ) %>% 195 | dplyr::select(-"xmid", -"ymid") %>% 196 | tidyr::expand_grid(side = factor(c("right", "left", "up", "down"))) %>% 197 | dplyr::filter( 198 | .data$side != dplyr::coalesce(.data$from, "endcap"), 199 | .data$side != dplyr::coalesce(.data$to, "endcap") 200 | ) %>% 201 | dplyr::select(-"from", -"to") 202 | } 203 | 204 | #' Translate square sides into line segments (1 segment per side). 205 | #' 206 | #' @param data A data.frame with 5 columns: `xmin`, `ymin`, `xmax`, `ymax`, `side`. 207 | #' There is 1 row per side of the path outline. 208 | #' @param coord The `CoordIp` coordinate system. 209 | #' @return A data.frame with 4 columns: `x`, `y`, `xend`, `yend`. 210 | #' There is 1 row per side of the path outline. 211 | #' 212 | #' @noRd 213 | sides_to_segments <- function(data, coord) { 214 | data %>% 215 | dplyr::mutate( 216 | x = dplyr::if_else(.data$side == "right", .data$xmax, .data$xmin), 217 | y = dplyr::if_else(.data$side == "up", .data$ymax, .data$ymin), 218 | xend = dplyr::if_else(.data$side == "left", .data$xmin, .data$xmax), 219 | yend = dplyr::if_else(.data$side == "down", .data$ymin, .data$ymax), 220 | 221 | x = snap_to_grid(.data$x, .data$side == "right", coord$limits$x), 222 | y = snap_to_grid(.data$y, .data$side == "up", coord$limits$y), 223 | xend = snap_to_grid(.data$xend, .data$side != "left", coord$limits$x), 224 | yend = snap_to_grid(.data$yend, .data$side != "down", coord$limits$y) 225 | ) %>% 226 | dplyr::select(-"xmin", -"ymin", -"xmax", -"ymax", -"side") 227 | } 228 | -------------------------------------------------------------------------------- /tests/testthat/test-ip_to_cartesian.R: -------------------------------------------------------------------------------- 1 | suppressPackageStartupMessages(library(dplyr)) 2 | 3 | expect_curve_endpoints <- function(canvas_network, curve) { 4 | max_plotted_bits <- 24 5 | min_pixel_prefix <- prefix_length(canvas_network) 6 | max_pixel_prefix <- pmin(min_pixel_prefix + max_plotted_bits, max_prefix_length(canvas_network)) 7 | 8 | curve_orders <- tibble( 9 | canvas_network, 10 | pixel_prefix = seq.int(min_pixel_prefix, max_pixel_prefix, 2), 11 | curve_order = (pixel_prefix - prefix_length(canvas_network)) / 2 12 | ) 13 | 14 | result <- curve_orders %>% 15 | rowwise() %>% 16 | mutate( 17 | start = address_to_cartesian(network_address(canvas_network), canvas_network, pixel_prefix, curve), 18 | end = address_to_cartesian(broadcast_address(canvas_network), canvas_network, pixel_prefix, curve) 19 | ) %>% 20 | ungroup() 21 | 22 | expected <- curve_orders %>% 23 | mutate( 24 | start = data.frame(x = 0, y = 2^curve_order - 1), 25 | end = data.frame( 26 | x = 2^curve_order - 1, 27 | y = if (curve == "hilbert") 2^curve_order - 1 else 0 28 | ) 29 | ) 30 | 31 | expect_equal(result, expected) 32 | } 33 | 34 | test_that("Start and end points", { 35 | expect_curve_endpoints(ip_network("0.0.0.0/0"), "hilbert") 36 | expect_curve_endpoints(ip_network("224.0.0.0/4"), "hilbert") 37 | expect_curve_endpoints(ip_network("::/0"), "hilbert") 38 | expect_curve_endpoints(ip_network("2001:db8::/32"), "hilbert") 39 | 40 | expect_curve_endpoints(ip_network("0.0.0.0/0"), "morton") 41 | expect_curve_endpoints(ip_network("224.0.0.0/4"), "morton") 42 | expect_curve_endpoints(ip_network("::/0"), "morton") 43 | expect_curve_endpoints(ip_network("2001:db8::/32"), "morton") 44 | }) 45 | 46 | test_that("Addresses mapped to points", { 47 | expect_equal( 48 | address_to_cartesian(ip_address("224.0.0.0"), pixel_prefix = 16, curve = "hilbert"), 49 | data.frame(x = 191, y = 192) 50 | ) 51 | expect_equal( 52 | address_to_cartesian(ip_address("224.0.0.0"), pixel_prefix = 16, curve = "morton"), 53 | data.frame(x = 128, y = 63) 54 | ) 55 | expect_equal( 56 | address_to_cartesian(ip_address("fc00::"), canvas_network = ip_network("::/0"), pixel_prefix = 16, curve = "hilbert"), 57 | data.frame(x = 255, y = 224) 58 | ) 59 | expect_equal( 60 | address_to_cartesian(ip_address("fc00::"), canvas_network = ip_network("::/0"), pixel_prefix = 16, curve = "morton"), 61 | data.frame(x = 224, y = 31) 62 | ) 63 | }) 64 | 65 | test_that("Networks mapped to bounding boxes", { 66 | # smaller than pixel 67 | expect_equal( 68 | network_to_cartesian(ip_network("224.0.0.0/17"), pixel_prefix = 16, curve = "hilbert"), 69 | data.frame(xmin = 191, ymin = 192, xmax = 191, ymax = 192) 70 | ) 71 | expect_equal( 72 | network_to_cartesian(ip_network("224.0.0.0/17"), pixel_prefix = 16, curve = "morton"), 73 | data.frame(xmin = 128, ymin = 63, xmax = 128, ymax = 63) 74 | ) 75 | expect_equal( 76 | network_to_cartesian(ip_network("fc00::/17"), canvas_network = ip_network("::/0"), pixel_prefix = 16, curve = "hilbert"), 77 | data.frame(xmin = 255, ymin = 224, xmax = 255, ymax = 224) 78 | ) 79 | expect_equal( 80 | network_to_cartesian(ip_network("fc00::/17"), canvas_network = ip_network("::/0"), pixel_prefix = 16, curve = "morton"), 81 | data.frame(xmin = 224, ymin = 31, xmax = 224, ymax = 31) 82 | ) 83 | 84 | # square 85 | expect_equal( 86 | network_to_cartesian(ip_network("224.0.0.0/4"), pixel_prefix = 16, curve = "hilbert"), 87 | data.frame(xmin = 128, ymin = 192, xmax = 191, ymax = 255) 88 | ) 89 | expect_equal( 90 | network_to_cartesian(ip_network("224.0.0.0/4"), pixel_prefix = 16, curve = "morton"), 91 | data.frame(xmin = 128, ymin = 0, xmax = 191, ymax = 63) 92 | ) 93 | expect_equal( 94 | network_to_cartesian(ip_network("fc00::/8"), canvas_network = ip_network("::/0"), pixel_prefix = 16, curve = "hilbert"), 95 | data.frame(xmin = 240, ymin = 224, xmax = 255, ymax = 239) 96 | ) 97 | expect_equal( 98 | network_to_cartesian(ip_network("fc00::/8"), canvas_network = ip_network("::/0"), pixel_prefix = 16, curve = "morton"), 99 | data.frame(xmin = 224, ymin = 16, xmax = 239, ymax = 31) 100 | ) 101 | 102 | # rectangle 103 | expect_equal( 104 | network_to_cartesian(ip_network("224.0.0.0/3"), pixel_prefix = 16, curve = "hilbert"), 105 | data.frame(xmin = 128, ymin = 192, xmax = 255, ymax = 255) 106 | ) 107 | expect_equal( 108 | network_to_cartesian(ip_network("224.0.0.0/3"), pixel_prefix = 16, curve = "morton"), 109 | data.frame(xmin = 128, ymin = 0, xmax = 255, ymax = 63) 110 | ) 111 | expect_equal( 112 | network_to_cartesian(ip_network("fc00::/7"), canvas_network = ip_network("::/0"), pixel_prefix = 16, curve = "hilbert"), 113 | data.frame(xmin = 224, ymin = 224, xmax = 255, ymax = 239) 114 | ) 115 | expect_equal( 116 | network_to_cartesian(ip_network("fc00::/7"), canvas_network = ip_network("::/0"), pixel_prefix = 16, curve = "morton"), 117 | data.frame(xmin = 224, ymin = 16, xmax = 255, ymax = 31) 118 | ) 119 | }) 120 | 121 | test_that("Outside canvas mapped to NA", { 122 | expect_equal( 123 | address_to_cartesian(ip_address("0.0.0.0"), ip_network("224.0.0.0/4")), 124 | data.frame(x = NA_integer_, y = NA_integer_) 125 | ) 126 | expect_equal( 127 | address_to_cartesian(ip_address("::"), ip_network("2001:db8::/32"), pixel_prefix = 48), 128 | data.frame(x = NA_integer_, y = NA_integer_) 129 | ) 130 | expect_equal( 131 | address_to_cartesian(ip_address("::"), ip_network("224.0.0.0/4")), 132 | data.frame(x = NA_integer_, y = NA_integer_) 133 | ) 134 | expect_equal( 135 | network_to_cartesian(ip_network("0.0.0.0/32"), ip_network("224.0.0.0/4")), 136 | data.frame(xmin = NA_integer_, ymin = NA_integer_, xmax = NA_integer_, ymax = NA_integer_) 137 | ) 138 | expect_equal( 139 | network_to_cartesian(ip_network("::/32"), ip_network("2001:db8::/32"), pixel_prefix = 48), 140 | data.frame(xmin = NA_integer_, ymin = NA_integer_, xmax = NA_integer_, ymax = NA_integer_) 141 | ) 142 | expect_equal( 143 | network_to_cartesian(ip_network("224.0.0.0/3"), ip_network("224.0.0.0/4")), 144 | data.frame(xmin = NA_integer_, ymin = NA_integer_, xmax = NA_integer_, ymax = NA_integer_) 145 | ) 146 | expect_equal( 147 | network_to_cartesian(ip_network("::/32"), ip_network("224.0.0.0/4")), 148 | data.frame(xmin = NA_integer_, ymin = NA_integer_, xmax = NA_integer_, ymax = NA_integer_) 149 | ) 150 | }) 151 | 152 | test_that("Input validation of mapping parameters", { 153 | expect_snapshot_error(validate_mapping_params(ip_address("0.0.0.0"), 16)) 154 | expect_snapshot_error(validate_mapping_params(ip_network(rep("0.0.0.0/0", 2)), 16)) 155 | 156 | expect_snapshot_error(validate_mapping_params(ip_network("0.0.0.0/0"), 2.5)) 157 | expect_snapshot_error(validate_mapping_params(ip_network("0.0.0.0/0"), c(1, 2))) 158 | expect_snapshot_error(validate_mapping_params(ip_network("0.0.0.0/0"), -1)) 159 | 160 | expect_snapshot_error(address_to_cartesian(ip_address("0.0.0.0"), curve = "hilber")) 161 | expect_snapshot_error(network_to_cartesian(ip_network("0.0.0.0/0"), curve = "hilber")) 162 | 163 | expect_snapshot_error(validate_mapping_params(ip_network("0.0.0.0/16"), 33)) 164 | expect_snapshot_error(validate_mapping_params(ip_network("::/120"), 129)) 165 | expect_snapshot_error(validate_mapping_params(ip_network("0.0.0.0/16"), 14)) 166 | expect_snapshot_error(validate_mapping_params(ip_network("0.0.0.0/0"), 31)) 167 | expect_snapshot_error(validate_mapping_params(ip_network("0.0.0.0/0"), 32)) 168 | }) 169 | 170 | test_that("Other input validation", { 171 | expect_snapshot_error(address_to_cartesian(ip_network())) 172 | expect_snapshot_error(network_to_cartesian(ip_address())) 173 | }) 174 | 175 | test_that("Missing values", { 176 | expect_equal( 177 | address_to_cartesian(ip_address(NA)), 178 | data.frame(x = NA_integer_, y = NA_integer_) 179 | ) 180 | expect_equal( 181 | network_to_cartesian(ip_network(NA)), 182 | data.frame(xmin = NA_integer_, ymin = NA_integer_, xmax = NA_integer_, ymax = NA_integer_) 183 | ) 184 | expect_snapshot_error( 185 | address_to_cartesian(ip_address("0.0.0.0"), canvas_network = ip_network(NA)) 186 | ) 187 | expect_snapshot_error( 188 | network_to_cartesian(ip_network("0.0.0.0/32"), canvas_network = ip_network(NA)) 189 | ) 190 | expect_snapshot_error( 191 | address_to_cartesian(ip_address("0.0.0.0"), pixel_prefix = NA) 192 | ) 193 | expect_snapshot_error( 194 | network_to_cartesian(ip_network("0.0.0.0/32"), pixel_prefix = NA) 195 | ) 196 | }) 197 | -------------------------------------------------------------------------------- /man/figures/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 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 | --------------------------------------------------------------------------------