├── .cargo └── config ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE.md ├── README.md ├── TODO.md ├── docs └── example-simple_screenshot.png ├── examples └── simple │ ├── Cargo.toml │ └── src │ └── main.rs └── src ├── graph.rs ├── graph_with_controls.rs ├── lib.rs ├── null_data_source.rs ├── observable_value.rs ├── signal.rs ├── store.rs └── test_data_generator.rs /.cargo/config: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags = ["-C", "target-cpu=native"] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "aho-corasick" 5 | version = "0.7.13" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "043164d8ba5c4c3035fec9bbee8647c0261d788f3474306f93bb65901cae0e86" 8 | dependencies = [ 9 | "memchr", 10 | ] 11 | 12 | [[package]] 13 | name = "anyhow" 14 | version = "1.0.32" 15 | source = "registry+https://github.com/rust-lang/crates.io-index" 16 | checksum = "6b602bfe940d21c130f3895acd65221e8a61270debe89d628b9cb4e3ccb8569b" 17 | 18 | [[package]] 19 | name = "atk" 20 | version = "0.9.0" 21 | source = "registry+https://github.com/rust-lang/crates.io-index" 22 | checksum = "812b4911e210bd51b24596244523c856ca749e6223c50a7fbbba3f89ee37c426" 23 | dependencies = [ 24 | "atk-sys", 25 | "bitflags", 26 | "glib", 27 | "glib-sys", 28 | "gobject-sys", 29 | "libc", 30 | ] 31 | 32 | [[package]] 33 | name = "atk-sys" 34 | version = "0.10.0" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "f530e4af131d94cc4fa15c5c9d0348f0ef28bac64ba660b6b2a1cf2605dedfce" 37 | dependencies = [ 38 | "glib-sys", 39 | "gobject-sys", 40 | "libc", 41 | "system-deps", 42 | ] 43 | 44 | [[package]] 45 | name = "atty" 46 | version = "0.2.14" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 49 | dependencies = [ 50 | "hermit-abi", 51 | "libc", 52 | "winapi", 53 | ] 54 | 55 | [[package]] 56 | name = "bitflags" 57 | version = "1.2.1" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 60 | 61 | [[package]] 62 | name = "cairo-rs" 63 | version = "0.9.1" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "c5c0f2e047e8ca53d0ff249c54ae047931d7a6ebe05d00af73e0ffeb6e34bdb8" 66 | dependencies = [ 67 | "bitflags", 68 | "cairo-sys-rs", 69 | "glib", 70 | "glib-sys", 71 | "gobject-sys", 72 | "libc", 73 | "thiserror", 74 | ] 75 | 76 | [[package]] 77 | name = "cairo-sys-rs" 78 | version = "0.10.0" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "2ed2639b9ad5f1d6efa76de95558e11339e7318426d84ac4890b86c03e828ca7" 81 | dependencies = [ 82 | "glib-sys", 83 | "libc", 84 | "system-deps", 85 | ] 86 | 87 | [[package]] 88 | name = "cc" 89 | version = "1.0.58" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "f9a06fb2e53271d7c279ec1efea6ab691c35a2ae67ec0d91d7acec0caf13b518" 92 | 93 | [[package]] 94 | name = "cfg-if" 95 | version = "0.1.10" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 98 | 99 | [[package]] 100 | name = "darling" 101 | version = "0.10.2" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" 104 | dependencies = [ 105 | "darling_core", 106 | "darling_macro", 107 | ] 108 | 109 | [[package]] 110 | name = "darling_core" 111 | version = "0.10.2" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" 114 | dependencies = [ 115 | "fnv", 116 | "ident_case", 117 | "proc-macro2", 118 | "quote", 119 | "strsim", 120 | "syn", 121 | ] 122 | 123 | [[package]] 124 | name = "darling_macro" 125 | version = "0.10.2" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" 128 | dependencies = [ 129 | "darling_core", 130 | "quote", 131 | "syn", 132 | ] 133 | 134 | [[package]] 135 | name = "derive_builder" 136 | version = "0.9.0" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "a2658621297f2cf68762a6f7dc0bb7e1ff2cfd6583daef8ee0fed6f7ec468ec0" 139 | dependencies = [ 140 | "darling", 141 | "derive_builder_core", 142 | "proc-macro2", 143 | "quote", 144 | "syn", 145 | ] 146 | 147 | [[package]] 148 | name = "derive_builder_core" 149 | version = "0.9.0" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "2791ea3e372c8495c0bc2033991d76b512cd799d07491fbd6890124db9458bef" 152 | dependencies = [ 153 | "darling", 154 | "proc-macro2", 155 | "quote", 156 | "syn", 157 | ] 158 | 159 | [[package]] 160 | name = "either" 161 | version = "1.6.0" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "cd56b59865bce947ac5958779cfa508f6c3b9497cc762b7e24a12d11ccde2c4f" 164 | 165 | [[package]] 166 | name = "env_logger" 167 | version = "0.7.1" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" 170 | dependencies = [ 171 | "atty", 172 | "humantime", 173 | "log", 174 | "regex", 175 | "termcolor", 176 | ] 177 | 178 | [[package]] 179 | name = "example-simple" 180 | version = "0.1.0" 181 | dependencies = [ 182 | "env_logger", 183 | "gdk", 184 | "gio", 185 | "glib", 186 | "gtk", 187 | "log", 188 | "rt-graph", 189 | ] 190 | 191 | [[package]] 192 | name = "fnv" 193 | version = "1.0.7" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 196 | 197 | [[package]] 198 | name = "futures" 199 | version = "0.3.5" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "1e05b85ec287aac0dc34db7d4a569323df697f9c55b99b15d6b4ef8cde49f613" 202 | dependencies = [ 203 | "futures-channel", 204 | "futures-core", 205 | "futures-executor", 206 | "futures-io", 207 | "futures-sink", 208 | "futures-task", 209 | "futures-util", 210 | ] 211 | 212 | [[package]] 213 | name = "futures-channel" 214 | version = "0.3.5" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "f366ad74c28cca6ba456d95e6422883cfb4b252a83bed929c83abfdbbf2967d5" 217 | dependencies = [ 218 | "futures-core", 219 | "futures-sink", 220 | ] 221 | 222 | [[package]] 223 | name = "futures-core" 224 | version = "0.3.5" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "59f5fff90fd5d971f936ad674802482ba441b6f09ba5e15fd8b39145582ca399" 227 | 228 | [[package]] 229 | name = "futures-executor" 230 | version = "0.3.5" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "10d6bb888be1153d3abeb9006b11b02cf5e9b209fda28693c31ae1e4e012e314" 233 | dependencies = [ 234 | "futures-core", 235 | "futures-task", 236 | "futures-util", 237 | ] 238 | 239 | [[package]] 240 | name = "futures-io" 241 | version = "0.3.5" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "de27142b013a8e869c14957e6d2edeef89e97c289e69d042ee3a49acd8b51789" 244 | 245 | [[package]] 246 | name = "futures-macro" 247 | version = "0.3.5" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "d0b5a30a4328ab5473878237c447333c093297bded83a4983d10f4deea240d39" 250 | dependencies = [ 251 | "proc-macro-hack", 252 | "proc-macro2", 253 | "quote", 254 | "syn", 255 | ] 256 | 257 | [[package]] 258 | name = "futures-sink" 259 | version = "0.3.5" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "3f2032893cb734c7a05d85ce0cc8b8c4075278e93b24b66f9de99d6eb0fa8acc" 262 | 263 | [[package]] 264 | name = "futures-task" 265 | version = "0.3.5" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "bdb66b5f09e22019b1ab0830f7785bcea8e7a42148683f99214f73f8ec21a626" 268 | dependencies = [ 269 | "once_cell", 270 | ] 271 | 272 | [[package]] 273 | name = "futures-util" 274 | version = "0.3.5" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "8764574ff08b701a084482c3c7031349104b07ac897393010494beaa18ce32c6" 277 | dependencies = [ 278 | "futures-channel", 279 | "futures-core", 280 | "futures-io", 281 | "futures-macro", 282 | "futures-sink", 283 | "futures-task", 284 | "memchr", 285 | "pin-project", 286 | "pin-utils", 287 | "proc-macro-hack", 288 | "proc-macro-nested", 289 | "slab", 290 | ] 291 | 292 | [[package]] 293 | name = "gdk" 294 | version = "0.13.2" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "db00839b2a68a7a10af3fa28dfb3febaba3a20c3a9ac2425a33b7df1f84a6b7d" 297 | dependencies = [ 298 | "bitflags", 299 | "cairo-rs", 300 | "cairo-sys-rs", 301 | "gdk-pixbuf", 302 | "gdk-sys", 303 | "gio", 304 | "gio-sys", 305 | "glib", 306 | "glib-sys", 307 | "gobject-sys", 308 | "libc", 309 | "pango", 310 | ] 311 | 312 | [[package]] 313 | name = "gdk-pixbuf" 314 | version = "0.9.0" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "8f6dae3cb99dd49b758b88f0132f8d401108e63ae8edd45f432d42cdff99998a" 317 | dependencies = [ 318 | "gdk-pixbuf-sys", 319 | "gio", 320 | "gio-sys", 321 | "glib", 322 | "glib-sys", 323 | "gobject-sys", 324 | "libc", 325 | ] 326 | 327 | [[package]] 328 | name = "gdk-pixbuf-sys" 329 | version = "0.10.0" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "3bfe468a7f43e97b8d193a762b6c5cf67a7d36cacbc0b9291dbcae24bfea1e8f" 332 | dependencies = [ 333 | "gio-sys", 334 | "glib-sys", 335 | "gobject-sys", 336 | "libc", 337 | "system-deps", 338 | ] 339 | 340 | [[package]] 341 | name = "gdk-sys" 342 | version = "0.10.0" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "0a9653cfc500fd268015b1ac055ddbc3df7a5c9ea3f4ccef147b3957bd140d69" 345 | dependencies = [ 346 | "cairo-sys-rs", 347 | "gdk-pixbuf-sys", 348 | "gio-sys", 349 | "glib-sys", 350 | "gobject-sys", 351 | "libc", 352 | "pango-sys", 353 | "pkg-config", 354 | "system-deps", 355 | ] 356 | 357 | [[package]] 358 | name = "getrandom" 359 | version = "0.1.14" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" 362 | dependencies = [ 363 | "cfg-if", 364 | "libc", 365 | "wasi", 366 | ] 367 | 368 | [[package]] 369 | name = "gio" 370 | version = "0.9.1" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "1fb60242bfff700772dae5d9e3a1f7aa2e4ebccf18b89662a16acb2822568561" 373 | dependencies = [ 374 | "bitflags", 375 | "futures", 376 | "futures-channel", 377 | "futures-core", 378 | "futures-io", 379 | "futures-util", 380 | "gio-sys", 381 | "glib", 382 | "glib-sys", 383 | "gobject-sys", 384 | "libc", 385 | "once_cell", 386 | "thiserror", 387 | ] 388 | 389 | [[package]] 390 | name = "gio-sys" 391 | version = "0.10.0" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "35993626299fbcaa73c0a19be8fdd01c950f9f3d3ac9cb4fb5532b924ab1a5d7" 394 | dependencies = [ 395 | "glib-sys", 396 | "gobject-sys", 397 | "libc", 398 | "system-deps", 399 | ] 400 | 401 | [[package]] 402 | name = "glib" 403 | version = "0.10.1" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "a5e0533f48640d86e8e2f3cee778a9f97588d4a0bec8be065ee51ea52346d6c1" 406 | dependencies = [ 407 | "bitflags", 408 | "futures-channel", 409 | "futures-core", 410 | "futures-executor", 411 | "futures-task", 412 | "futures-util", 413 | "glib-macros", 414 | "glib-sys", 415 | "gobject-sys", 416 | "libc", 417 | "once_cell", 418 | ] 419 | 420 | [[package]] 421 | name = "glib-macros" 422 | version = "0.10.1" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "41486a26d1366a8032b160b59065a59fb528530a46a49f627e7048fb8c064039" 425 | dependencies = [ 426 | "anyhow", 427 | "heck", 428 | "itertools", 429 | "proc-macro-crate", 430 | "proc-macro-error", 431 | "proc-macro2", 432 | "quote", 433 | "syn", 434 | ] 435 | 436 | [[package]] 437 | name = "glib-sys" 438 | version = "0.10.0" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "b6cda4af5c2f4507b7a3535b798dca2135293f4bc3a17f399ce244ef15841c4c" 441 | dependencies = [ 442 | "libc", 443 | "system-deps", 444 | ] 445 | 446 | [[package]] 447 | name = "gobject-sys" 448 | version = "0.10.0" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "952133b60c318a62bf82ee75b93acc7e84028a093e06b9e27981c2b6fe68218c" 451 | dependencies = [ 452 | "glib-sys", 453 | "libc", 454 | "system-deps", 455 | ] 456 | 457 | [[package]] 458 | name = "gtk" 459 | version = "0.9.2" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | checksum = "2f022f2054072b3af07666341984562c8e626a79daa8be27b955d12d06a5ad6a" 462 | dependencies = [ 463 | "atk", 464 | "bitflags", 465 | "cairo-rs", 466 | "cairo-sys-rs", 467 | "cc", 468 | "gdk", 469 | "gdk-pixbuf", 470 | "gdk-pixbuf-sys", 471 | "gdk-sys", 472 | "gio", 473 | "gio-sys", 474 | "glib", 475 | "glib-sys", 476 | "gobject-sys", 477 | "gtk-sys", 478 | "libc", 479 | "once_cell", 480 | "pango", 481 | "pango-sys", 482 | "pkg-config", 483 | ] 484 | 485 | [[package]] 486 | name = "gtk-sys" 487 | version = "0.10.0" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | checksum = "89acda6f084863307d948ba64a4b1ef674e8527dddab147ee4cdcc194c880457" 490 | dependencies = [ 491 | "atk-sys", 492 | "cairo-sys-rs", 493 | "gdk-pixbuf-sys", 494 | "gdk-sys", 495 | "gio-sys", 496 | "glib-sys", 497 | "gobject-sys", 498 | "libc", 499 | "pango-sys", 500 | "system-deps", 501 | ] 502 | 503 | [[package]] 504 | name = "heck" 505 | version = "0.3.1" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" 508 | dependencies = [ 509 | "unicode-segmentation", 510 | ] 511 | 512 | [[package]] 513 | name = "hermit-abi" 514 | version = "0.1.15" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "3deed196b6e7f9e44a2ae8d94225d80302d81208b1bb673fd21fe634645c85a9" 517 | dependencies = [ 518 | "libc", 519 | ] 520 | 521 | [[package]] 522 | name = "humantime" 523 | version = "1.3.0" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" 526 | dependencies = [ 527 | "quick-error", 528 | ] 529 | 530 | [[package]] 531 | name = "ident_case" 532 | version = "1.0.1" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 535 | 536 | [[package]] 537 | name = "itertools" 538 | version = "0.9.0" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" 541 | dependencies = [ 542 | "either", 543 | ] 544 | 545 | [[package]] 546 | name = "lazy_static" 547 | version = "1.4.0" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 550 | 551 | [[package]] 552 | name = "libc" 553 | version = "0.2.74" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "a2f02823cf78b754822df5f7f268fb59822e7296276d3e069d8e8cb26a14bd10" 556 | 557 | [[package]] 558 | name = "log" 559 | version = "0.4.11" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" 562 | dependencies = [ 563 | "cfg-if", 564 | ] 565 | 566 | [[package]] 567 | name = "memchr" 568 | version = "2.3.3" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" 571 | 572 | [[package]] 573 | name = "once_cell" 574 | version = "1.4.0" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "0b631f7e854af39a1739f401cf34a8a013dfe09eac4fa4dba91e9768bd28168d" 577 | 578 | [[package]] 579 | name = "pango" 580 | version = "0.9.1" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "9937068580bebd8ced19975938573803273ccbcbd598c58d4906efd4ac87c438" 583 | dependencies = [ 584 | "bitflags", 585 | "glib", 586 | "glib-sys", 587 | "gobject-sys", 588 | "libc", 589 | "once_cell", 590 | "pango-sys", 591 | ] 592 | 593 | [[package]] 594 | name = "pango-sys" 595 | version = "0.10.0" 596 | source = "registry+https://github.com/rust-lang/crates.io-index" 597 | checksum = "24d2650c8b62d116c020abd0cea26a4ed96526afda89b1c4ea567131fdefc890" 598 | dependencies = [ 599 | "glib-sys", 600 | "gobject-sys", 601 | "libc", 602 | "system-deps", 603 | ] 604 | 605 | [[package]] 606 | name = "pin-project" 607 | version = "0.4.23" 608 | source = "registry+https://github.com/rust-lang/crates.io-index" 609 | checksum = "ca4433fff2ae79342e497d9f8ee990d174071408f28f726d6d83af93e58e48aa" 610 | dependencies = [ 611 | "pin-project-internal", 612 | ] 613 | 614 | [[package]] 615 | name = "pin-project-internal" 616 | version = "0.4.23" 617 | source = "registry+https://github.com/rust-lang/crates.io-index" 618 | checksum = "2c0e815c3ee9a031fdf5af21c10aa17c573c9c6a566328d99e3936c34e36461f" 619 | dependencies = [ 620 | "proc-macro2", 621 | "quote", 622 | "syn", 623 | ] 624 | 625 | [[package]] 626 | name = "pin-utils" 627 | version = "0.1.0" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 630 | 631 | [[package]] 632 | name = "pkg-config" 633 | version = "0.3.18" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "d36492546b6af1463394d46f0c834346f31548646f6ba10849802c9c9a27ac33" 636 | 637 | [[package]] 638 | name = "ppv-lite86" 639 | version = "0.2.8" 640 | source = "registry+https://github.com/rust-lang/crates.io-index" 641 | checksum = "237a5ed80e274dbc66f86bd59c1e25edc039660be53194b5fe0a482e0f2612ea" 642 | 643 | [[package]] 644 | name = "proc-macro-crate" 645 | version = "0.1.5" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" 648 | dependencies = [ 649 | "toml", 650 | ] 651 | 652 | [[package]] 653 | name = "proc-macro-error" 654 | version = "1.0.4" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 657 | dependencies = [ 658 | "proc-macro-error-attr", 659 | "proc-macro2", 660 | "quote", 661 | "syn", 662 | "version_check", 663 | ] 664 | 665 | [[package]] 666 | name = "proc-macro-error-attr" 667 | version = "1.0.4" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 670 | dependencies = [ 671 | "proc-macro2", 672 | "quote", 673 | "version_check", 674 | ] 675 | 676 | [[package]] 677 | name = "proc-macro-hack" 678 | version = "0.5.18" 679 | source = "registry+https://github.com/rust-lang/crates.io-index" 680 | checksum = "99c605b9a0adc77b7211c6b1f722dcb613d68d66859a44f3d485a6da332b0598" 681 | 682 | [[package]] 683 | name = "proc-macro-nested" 684 | version = "0.1.6" 685 | source = "registry+https://github.com/rust-lang/crates.io-index" 686 | checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a" 687 | 688 | [[package]] 689 | name = "proc-macro2" 690 | version = "1.0.19" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "04f5f085b5d71e2188cb8271e5da0161ad52c3f227a661a3c135fdf28e258b12" 693 | dependencies = [ 694 | "unicode-xid", 695 | ] 696 | 697 | [[package]] 698 | name = "quick-error" 699 | version = "1.2.3" 700 | source = "registry+https://github.com/rust-lang/crates.io-index" 701 | checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" 702 | 703 | [[package]] 704 | name = "quote" 705 | version = "1.0.7" 706 | source = "registry+https://github.com/rust-lang/crates.io-index" 707 | checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" 708 | dependencies = [ 709 | "proc-macro2", 710 | ] 711 | 712 | [[package]] 713 | name = "rand" 714 | version = "0.7.3" 715 | source = "registry+https://github.com/rust-lang/crates.io-index" 716 | checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" 717 | dependencies = [ 718 | "getrandom", 719 | "libc", 720 | "rand_chacha", 721 | "rand_core", 722 | "rand_hc", 723 | ] 724 | 725 | [[package]] 726 | name = "rand_chacha" 727 | version = "0.2.2" 728 | source = "registry+https://github.com/rust-lang/crates.io-index" 729 | checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" 730 | dependencies = [ 731 | "ppv-lite86", 732 | "rand_core", 733 | ] 734 | 735 | [[package]] 736 | name = "rand_core" 737 | version = "0.5.1" 738 | source = "registry+https://github.com/rust-lang/crates.io-index" 739 | checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" 740 | dependencies = [ 741 | "getrandom", 742 | ] 743 | 744 | [[package]] 745 | name = "rand_hc" 746 | version = "0.2.0" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" 749 | dependencies = [ 750 | "rand_core", 751 | ] 752 | 753 | [[package]] 754 | name = "regex" 755 | version = "1.3.9" 756 | source = "registry+https://github.com/rust-lang/crates.io-index" 757 | checksum = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6" 758 | dependencies = [ 759 | "aho-corasick", 760 | "memchr", 761 | "regex-syntax", 762 | "thread_local", 763 | ] 764 | 765 | [[package]] 766 | name = "regex-syntax" 767 | version = "0.6.18" 768 | source = "registry+https://github.com/rust-lang/crates.io-index" 769 | checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" 770 | 771 | [[package]] 772 | name = "rt-graph" 773 | version = "0.3.4" 774 | dependencies = [ 775 | "cairo-rs", 776 | "derive_builder", 777 | "env_logger", 778 | "gdk", 779 | "gio", 780 | "glib", 781 | "gtk", 782 | "log", 783 | "once_cell", 784 | "rand", 785 | ] 786 | 787 | [[package]] 788 | name = "serde" 789 | version = "1.0.114" 790 | source = "registry+https://github.com/rust-lang/crates.io-index" 791 | checksum = "5317f7588f0a5078ee60ef675ef96735a1442132dc645eb1d12c018620ed8cd3" 792 | 793 | [[package]] 794 | name = "slab" 795 | version = "0.4.2" 796 | source = "registry+https://github.com/rust-lang/crates.io-index" 797 | checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" 798 | 799 | [[package]] 800 | name = "strsim" 801 | version = "0.9.3" 802 | source = "registry+https://github.com/rust-lang/crates.io-index" 803 | checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" 804 | 805 | [[package]] 806 | name = "strum" 807 | version = "0.18.0" 808 | source = "registry+https://github.com/rust-lang/crates.io-index" 809 | checksum = "57bd81eb48f4c437cadc685403cad539345bf703d78e63707418431cecd4522b" 810 | 811 | [[package]] 812 | name = "strum_macros" 813 | version = "0.18.0" 814 | source = "registry+https://github.com/rust-lang/crates.io-index" 815 | checksum = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c" 816 | dependencies = [ 817 | "heck", 818 | "proc-macro2", 819 | "quote", 820 | "syn", 821 | ] 822 | 823 | [[package]] 824 | name = "syn" 825 | version = "1.0.38" 826 | source = "registry+https://github.com/rust-lang/crates.io-index" 827 | checksum = "e69abc24912995b3038597a7a593be5053eb0fb44f3cc5beec0deb421790c1f4" 828 | dependencies = [ 829 | "proc-macro2", 830 | "quote", 831 | "unicode-xid", 832 | ] 833 | 834 | [[package]] 835 | name = "system-deps" 836 | version = "1.3.2" 837 | source = "registry+https://github.com/rust-lang/crates.io-index" 838 | checksum = "0f3ecc17269a19353b3558b313bba738b25d82993e30d62a18406a24aba4649b" 839 | dependencies = [ 840 | "heck", 841 | "pkg-config", 842 | "strum", 843 | "strum_macros", 844 | "thiserror", 845 | "toml", 846 | "version-compare", 847 | ] 848 | 849 | [[package]] 850 | name = "termcolor" 851 | version = "1.1.0" 852 | source = "registry+https://github.com/rust-lang/crates.io-index" 853 | checksum = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" 854 | dependencies = [ 855 | "winapi-util", 856 | ] 857 | 858 | [[package]] 859 | name = "thiserror" 860 | version = "1.0.20" 861 | source = "registry+https://github.com/rust-lang/crates.io-index" 862 | checksum = "7dfdd070ccd8ccb78f4ad66bf1982dc37f620ef696c6b5028fe2ed83dd3d0d08" 863 | dependencies = [ 864 | "thiserror-impl", 865 | ] 866 | 867 | [[package]] 868 | name = "thiserror-impl" 869 | version = "1.0.20" 870 | source = "registry+https://github.com/rust-lang/crates.io-index" 871 | checksum = "bd80fc12f73063ac132ac92aceea36734f04a1d93c1240c6944e23a3b8841793" 872 | dependencies = [ 873 | "proc-macro2", 874 | "quote", 875 | "syn", 876 | ] 877 | 878 | [[package]] 879 | name = "thread_local" 880 | version = "1.0.1" 881 | source = "registry+https://github.com/rust-lang/crates.io-index" 882 | checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" 883 | dependencies = [ 884 | "lazy_static", 885 | ] 886 | 887 | [[package]] 888 | name = "toml" 889 | version = "0.5.6" 890 | source = "registry+https://github.com/rust-lang/crates.io-index" 891 | checksum = "ffc92d160b1eef40665be3a05630d003936a3bc7da7421277846c2613e92c71a" 892 | dependencies = [ 893 | "serde", 894 | ] 895 | 896 | [[package]] 897 | name = "unicode-segmentation" 898 | version = "1.6.0" 899 | source = "registry+https://github.com/rust-lang/crates.io-index" 900 | checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" 901 | 902 | [[package]] 903 | name = "unicode-xid" 904 | version = "0.2.1" 905 | source = "registry+https://github.com/rust-lang/crates.io-index" 906 | checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" 907 | 908 | [[package]] 909 | name = "version-compare" 910 | version = "0.0.10" 911 | source = "registry+https://github.com/rust-lang/crates.io-index" 912 | checksum = "d63556a25bae6ea31b52e640d7c41d1ab27faba4ccb600013837a3d0b3994ca1" 913 | 914 | [[package]] 915 | name = "version_check" 916 | version = "0.9.2" 917 | source = "registry+https://github.com/rust-lang/crates.io-index" 918 | checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" 919 | 920 | [[package]] 921 | name = "wasi" 922 | version = "0.9.0+wasi-snapshot-preview1" 923 | source = "registry+https://github.com/rust-lang/crates.io-index" 924 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 925 | 926 | [[package]] 927 | name = "winapi" 928 | version = "0.3.9" 929 | source = "registry+https://github.com/rust-lang/crates.io-index" 930 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 931 | dependencies = [ 932 | "winapi-i686-pc-windows-gnu", 933 | "winapi-x86_64-pc-windows-gnu", 934 | ] 935 | 936 | [[package]] 937 | name = "winapi-i686-pc-windows-gnu" 938 | version = "0.4.0" 939 | source = "registry+https://github.com/rust-lang/crates.io-index" 940 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 941 | 942 | [[package]] 943 | name = "winapi-util" 944 | version = "0.1.5" 945 | source = "registry+https://github.com/rust-lang/crates.io-index" 946 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 947 | dependencies = [ 948 | "winapi", 949 | ] 950 | 951 | [[package]] 952 | name = "winapi-x86_64-pc-windows-gnu" 953 | version = "0.4.0" 954 | source = "registry+https://github.com/rust-lang/crates.io-index" 955 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 956 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rt-graph" 3 | description = "A real-time graphing experiment written in Rust." 4 | version = "0.3.4" 5 | authors = ["Alex Helfet "] 6 | edition = "2018" 7 | repository = "https://github.com/fluffysquirrels/rt-graph-rs/" 8 | license = "MIT" 9 | categories = ["graphics", "gui", "visualization"] 10 | keywords = ["graphics", "graph", "plotting", "visualization"] 11 | readme = "README.md" 12 | 13 | [dependencies] 14 | cairo-rs = "0.9.1" 15 | derive_builder = "0.9.0" 16 | env_logger = "0.7.1" 17 | gdk = "0.13.2" 18 | gio = "0.9.1" 19 | glib = "0.10.0" 20 | gtk = "0.9.0" 21 | log = "0.4.11" 22 | once_cell = "1.4.0" 23 | rand = "0.7" 24 | 25 | [workspace] 26 | members = [ 27 | "examples/simple", 28 | ] -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT license 2 | 3 | Copyright 2020 Alex Helfet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rt-graph 2 | 3 | A real-time graphing experiment written in Rust. 4 | 5 | ![Screenshot](docs/example-simple_screenshot.png) 6 | 7 | Many other graphing tools do not efficiently update the display when 8 | new data is added, for example redrawing the whole screen when only a 9 | few pixels of new data are added. 10 | 11 | This crate tries to do the minimum incremental work required to update 12 | the graph when new data is added: draw the few pixels of new data, and 13 | scroll the graph with efficient large copies, which can and should be 14 | accelerated by GPU hardware. 15 | 16 | As a result of this design rt-graph easily copes with 30k new points 17 | per second, at 60 FPS, using just 3% CPU (tested on a Lenovo T460 18 | laptop from 2016 with 2.4 GHz Intel Core i5-6300U, running Ubuntu 19 | 18.04.5). 20 | 21 | Source repository: 22 | (issues and pull requests are welcome!) 23 | 24 | Documentation: 25 | 26 | Crate: 27 | 28 | ## Prerequisites 29 | 30 | First install GTK 3 dependencies. 31 | 32 | On OS X with brew try: `brew install gtk+3` 33 | 34 | On Ubuntu try: `sudo apt-get install libgtk-3-dev` 35 | 36 | ## Run an example 37 | 38 | Clone the source repository from 39 | then try to run an 40 | example with some simulated data: 41 | 42 | ``` 43 | cargo run --package "example-simple" --release 44 | ``` 45 | 46 | You can scroll back and forth with the scrollbar under the graph, or 47 | go back to following the latest data by clicking the "Follow" button, 48 | and zoom in and out with the buttons. Click on the graph to show an 49 | information bar underneath it with the raw data where you clicked. 50 | 51 | ## Build your own application 52 | 53 | To use your own data implement the `DataSource` trait and pass an 54 | instance of your struct to the `ConfigBuilder::data_source()` method 55 | while building a `Graph` or `GraphWithControls`. 56 | 57 | `rt-graph` uses GTK (via the [gtk-rs](https://gtk-rs.org/) Rust 58 | bindings) for its UI and is designed to be embedded into any 59 | `gtk::Container` in your application. 60 | 61 | ## Helpful links 62 | 63 | GTK 3 documentation: 64 | 65 | gtk-rs (Rust GTK bindings) documentation: 66 | 67 | ## Changelog 68 | 69 | ### 0.3.4 70 | 71 | * Swap B and R color channels, they were backwards. 72 | 73 | ### 0.3.3 74 | 75 | * Add `.show` and `.hide` methods to `Graph`. 76 | * A hidden `Graph` ticks less frequently (1Hz), reducing CPU usage dramatically. 77 | * `Graph::tick()` does less when no data is ingested from a DataSource. 78 | 79 | ### 0.3.2 80 | 81 | * Add `.show` and `.hide` methods to `GraphWithControls`. 82 | 83 | ### 0.3.1 84 | 85 | * Fix panic when clicking on a graph with no data points. 86 | * Add NullDataSource. 87 | 88 | ### 0.3.0 89 | 90 | * Flip y axis so increasing values are higher on the screen. 91 | * Make `Store` private. 92 | * Add per-item documentation for all public items. 93 | * Replace naked `u16` and `u32` usage with type aliases `Value` and `Time`. 94 | 95 | ### 0.2.0 96 | 97 | * Refactor out Graph, GraphWithControls, so consumers can write their own controls. 98 | 99 | ### 0.1.1 100 | 101 | * Add more content to the README, including a screenshot. 102 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | ## Bugs 4 | 5 | ## Features 6 | * Scroll values read-out when there are several 7 | * Example with a non-blocking / fast DataSource 8 | * Example with a blocking DataSource that runs in another thread and ships data over a channel. 9 | * Example with multiple graphs. 10 | * Example with multiple graphs in sync. 11 | * Axes, legend 12 | * When you click on the graph it would be nice to have feedback as to 13 | where you clicked in the graph. 14 | * Daniel has 5 graphs, wants them all in sync 15 | * Leave it up to controls at a higher level how to navigate, each graph just has show methods. 16 | * Or one graph is just the n=1 case, support GraphSet concept with n `DataSource`s 17 | * Pause button 18 | * Mouse wheel press to pan 19 | * Mouse wheel to zoom x 20 | * Alt left mouse to zoom box 21 | * Export a GLib / GObject interface for consumption by other languages than Rust. 22 | * Scale and offset data (auto-fit to y?) 23 | * Probably use f32 for point data 24 | * Maybe hovering over the graph should show the current point value in a tooltip or sub-window 25 | * Resizing the window should resize the graph. 26 | * Maybe keep the section of the graph that's still valid when scrolling. 27 | * Lower CPU usage when hidden (e.g. minimised). Don't bother drawing. 28 | * Profile 29 | * Web port / rewrite? 30 | 31 | ## Notes 32 | 33 | ``` 34 | /// Scale value linearly from [0,1] to [min,max] 35 | fn map_to_range(value: f32, min: f32, max: f32) -> f32 { 36 | value * (max - min) + min 37 | } 38 | 39 | /// Scale value linearly from [min, max] to [0,1] 40 | fn normalize(value: f32, min: f32, max: f32) -> f32 { 41 | let delta = max - min; 42 | assert!(delta != 0.); 43 | 44 | (value.clamp(min, max) - min) / delta 45 | } 46 | ``` 47 | -------------------------------------------------------------------------------- /docs/example-simple_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluffysquirrels/rt-graph-rs/d3166c044b936917286734bbe713050889e8dcf8/docs/example-simple_screenshot.png -------------------------------------------------------------------------------- /examples/simple/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-simple" 3 | version = "0.1.0" 4 | authors = [] 5 | edition = "2018" 6 | publish = false 7 | 8 | [dependencies] 9 | env_logger = "0.7" 10 | gdk = "0.13.2" 11 | gio = "0.9.1" 12 | glib = "0.10.0" 13 | gtk = "0.9.0" 14 | log = "0.4.11" 15 | rt-graph = { path = "../.." } -------------------------------------------------------------------------------- /examples/simple/src/main.rs: -------------------------------------------------------------------------------- 1 | use gio::prelude::*; 2 | use gtk::prelude::*; 3 | use rt_graph::{ConfigBuilder, GraphWithControls, TestDataGenerator}; 4 | use std::{ 5 | env::args, 6 | }; 7 | 8 | fn main() { 9 | env_logger::init(); 10 | let application = 11 | gtk::Application::new(Some("com.github.fluffysquirrels.rt-graph.gtk-example"), 12 | gio::ApplicationFlags::default()) 13 | .expect("Application::new failed"); 14 | 15 | application.connect_activate(|app| { 16 | build_ui(app); 17 | }); 18 | 19 | application.run(&args().collect::>()); 20 | } 21 | 22 | fn build_ui(application: >k::Application) { 23 | let window = gtk::ApplicationWindowBuilder::new() 24 | .application(application) 25 | .title("rt-graph") 26 | .border_width(8) 27 | .window_position(gtk::WindowPosition::Center) 28 | .build(); 29 | 30 | // Show the (gtk) window so we can get a gdk::window below. 31 | window.show(); 32 | let gdk_window = window.get_window().unwrap(); 33 | 34 | let config = ConfigBuilder::default() 35 | .data_source(TestDataGenerator::new()) 36 | .build() 37 | .unwrap(); 38 | let mut _g = GraphWithControls::build_ui(config, &window, &gdk_window); 39 | window.show_all(); 40 | } 41 | -------------------------------------------------------------------------------- /src/graph.rs: -------------------------------------------------------------------------------- 1 | use crate::{Color, DataSource, observable_value, Point, Result, Store, Time, Value}; 2 | use gdk::prelude::*; 3 | use glib::source::Continue; 4 | use gtk::prelude::*; 5 | use std::{ 6 | cell::{Cell, RefCell, RefMut}, 7 | rc::Rc, 8 | time::Instant, 9 | }; 10 | 11 | const BYTES_PER_PIXEL: usize = 4; 12 | const BACKGROUND_COLOR: (f64, f64, f64) = (0.4, 0.4, 0.4); 13 | const DRAWN_AREA_BACKGROUND_COLOR: (f64, f64, f64) = (0.0, 0.0, 0.0); 14 | 15 | struct State { 16 | backing_surface: RefCell, 17 | temp_surface: RefCell, 18 | 19 | store: RefCell, 20 | 21 | drawing_area: gtk::DrawingArea, 22 | 23 | view_write: RefCell>, 24 | view_read: RefCell>, 25 | 26 | fps_count: Cell, 27 | fps_timer: Cell, 28 | 29 | config: Config, 30 | 31 | tick_id: RefCell, 32 | } 33 | 34 | enum TickId { 35 | IngestOnly(glib::source::SourceId), 36 | EveryFrame(gtk::TickCallbackId), 37 | None, 38 | } 39 | 40 | /// Describes what is currently showing on the graph. 41 | #[derive(Clone, Debug)] 42 | pub struct View { 43 | /// Zoom level, in units of t per x pixel 44 | pub zoom_x: f64, 45 | 46 | /// The most recently drawn time value. 47 | pub last_drawn_t: Time, 48 | 49 | /// The most recently drawn x pixel. 50 | pub last_drawn_x: u32, 51 | 52 | /// The longest ago time value that is still stored. Note 53 | /// that the oldest data is discarded to keep memory usage bounded. 54 | pub min_t: Time, 55 | 56 | /// The most recent time value. 57 | pub max_t: Time, 58 | 59 | /// The display mode. 60 | pub mode: ViewMode, 61 | } 62 | 63 | /// Describes the display mode of the graph 64 | #[derive(Clone, Debug, Eq, PartialEq)] 65 | pub enum ViewMode { 66 | /// Graph is following the latest data 67 | Following, 68 | 69 | /// Graph is scrolled to a particular point in time 70 | Scrolled, 71 | } 72 | 73 | impl View { 74 | fn default_from_config(c: &Config) -> View { 75 | View { 76 | zoom_x: c.base_zoom_x, 77 | last_drawn_t: 0, 78 | last_drawn_x: 0, 79 | min_t: 0, 80 | max_t: 0, 81 | mode: ViewMode::Following, 82 | } 83 | } 84 | } 85 | 86 | /// The configuration required by a `Graph` or `GraphWithControls` 87 | /// 88 | /// Create an instance using a `ConfigBuilder`: 89 | /// 90 | /// ``` 91 | /// use rt_graph::{Config, ConfigBuilder, TestDataGenerator}; 92 | /// 93 | /// let config: Config = 94 | /// ConfigBuilder::default() 95 | /// // Chain ConfigBuilder methods here 96 | /// .data_source(TestDataGenerator::new()) 97 | /// .build() 98 | /// .unwrap(); 99 | /// ``` 100 | #[derive(Builder, Debug)] 101 | #[builder(pattern = "owned")] 102 | pub struct Config { 103 | /// Maximum zoom out, in units of t per x pixel 104 | #[builder(default = "1000.0")] 105 | base_zoom_x: f64, 106 | 107 | /// Maximum zoom in, in units of t per x pixel 108 | #[builder(default = "1.0")] 109 | max_zoom_x: f64, 110 | 111 | /// Graph width in pixels 112 | #[builder(default = "800")] 113 | graph_width: u32, 114 | 115 | /// Graph height in pixels 116 | #[builder(default = "200")] 117 | graph_height: u32, 118 | 119 | #[builder(private, setter(name = "data_source_internal"))] 120 | data_source: RefCell>, 121 | 122 | /// How many windows width of data to store at maximum zoom out. 123 | #[builder(default = "100")] 124 | windows_to_store: u32, 125 | 126 | /// The style of point to draw 127 | #[builder(default = "PointStyle::Point")] 128 | point_style: PointStyle, 129 | } 130 | 131 | /// The style of point to draw 132 | #[derive(Clone, Copy, Debug)] 133 | pub enum PointStyle { 134 | /// A point, a single pixel. 135 | Point, 136 | 137 | /// A cross of 5 pixels in the shape of an 'x'. 138 | Cross, 139 | } 140 | 141 | impl ConfigBuilder { 142 | /// The data source for the graph. 143 | pub fn data_source(self, ds: T) -> Self { 144 | self.data_source_internal(RefCell::new(Box::new(ds))) 145 | } 146 | } 147 | 148 | /// A GTK widget that draws a graph. 149 | /// 150 | /// `Graph` contains no controls to navigate it, you are expected to 151 | /// provide your own controls using the methods and signals it provides. 152 | /// Or you can use `GraphWithControls` that comes with a set of controls. 153 | pub struct Graph { 154 | s: Rc, 155 | } 156 | 157 | impl Graph { 158 | /// Build and show a `Graph` widget in the target `gtk::Container`. 159 | pub fn build_ui(config: Config, container: &C, gdk_window: &gdk::Window) -> Graph 160 | where C: IsA + IsA 161 | { 162 | 163 | let drawing_area = gtk::DrawingAreaBuilder::new() 164 | .height_request(config.graph_height as i32) 165 | .width_request(config.graph_width as i32) 166 | .build(); 167 | container.add(&drawing_area); 168 | 169 | // Initialise State 170 | 171 | let backing_surface = create_backing_surface(gdk_window, 172 | config.graph_width, config.graph_height); 173 | let temp_surface = create_backing_surface(gdk_window, 174 | config.graph_width, config.graph_height); 175 | let store = Store::new(config.data_source.borrow().get_num_values().unwrap() as u8); 176 | let view = View::default_from_config(&config); 177 | let (view_read, view_write) = 178 | observable_value::ObservableValue::new(view.clone()).split(); 179 | let s = Rc::new(State { 180 | backing_surface: RefCell::new(backing_surface), 181 | temp_surface: RefCell::new(temp_surface), 182 | 183 | store: RefCell::new(store), 184 | 185 | drawing_area: drawing_area.clone(), 186 | 187 | view_read: RefCell::new(view_read), 188 | view_write: RefCell::new(view_write), 189 | 190 | fps_count: Cell::new(0), 191 | fps_timer: Cell::new(Instant::now()), 192 | 193 | config, 194 | 195 | tick_id: RefCell::new(TickId::None), 196 | }); 197 | let graph = Graph { 198 | s: s.clone(), 199 | }; 200 | 201 | // Set signal handlers that require State 202 | let sc = s.clone(); 203 | drawing_area.connect_draw(move |ctrl, ctx| { 204 | graph_draw(ctrl, ctx, &*sc) 205 | }); 206 | 207 | graph.set_frame_tick(); 208 | 209 | // Show everything recursively 210 | drawing_area.show_all(); 211 | 212 | graph 213 | } 214 | 215 | fn set_frame_tick(&self) { 216 | // Take the old value. 217 | let old_tick_id = self.s.tick_id.replace(TickId::None); 218 | match old_tick_id { 219 | TickId::IngestOnly(id) => { 220 | glib::source::source_remove(id); 221 | }, 222 | TickId::EveryFrame(id) => { 223 | // Already as desired, put old value back. 224 | self.s.tick_id.replace(TickId::EveryFrame(id)); 225 | return; 226 | }, 227 | TickId::None => (), 228 | } 229 | 230 | let sc = self.s.clone(); 231 | let frame_tick_id = self.s.drawing_area.add_tick_callback(move |_ctrl, _clock| { 232 | tick(&*sc); 233 | Continue(true) 234 | }); 235 | *self.s.tick_id.borrow_mut() = TickId::EveryFrame(frame_tick_id); 236 | } 237 | 238 | fn set_ingest_tick(&self) { 239 | // Take the old value. 240 | let old_tick_id = self.s.tick_id.replace(TickId::None); 241 | match old_tick_id { 242 | TickId::EveryFrame(id) => { 243 | id.remove(); 244 | }, 245 | TickId::IngestOnly(id) => { 246 | // Already as desired, put old value back. 247 | self.s.tick_id.replace(TickId::IngestOnly(id)); 248 | return; 249 | } 250 | TickId::None => (), 251 | } 252 | 253 | let sc = self.s.clone(); 254 | let ingest_tick_id = 255 | glib::source::timeout_add_seconds_local( 256 | 1 /* seconds */, 257 | move || { 258 | tick(&*sc); 259 | Continue(true) 260 | }); 261 | *self.s.tick_id.borrow_mut() = TickId::IngestOnly(ingest_tick_id); 262 | } 263 | 264 | /// Show the graph. 265 | pub fn show(&self) { 266 | self.set_frame_tick(); 267 | self.s.drawing_area.show(); 268 | } 269 | 270 | /// Hide the graph. 271 | pub fn hide(&self) { 272 | self.set_ingest_tick(); 273 | self.s.drawing_area.hide(); 274 | } 275 | 276 | /// Return the width of the graph 277 | pub fn width(&self) -> u32 { 278 | self.s.config.graph_width 279 | } 280 | 281 | /// Return the height of the graph 282 | pub fn height(&self) -> u32 { 283 | self.s.config.graph_height 284 | } 285 | 286 | /// Return the initial and maximally zoomed out zoom level, in 287 | /// units of time per x pixel. 288 | pub fn base_zoom_x(&self) -> f64 { 289 | self.s.config.base_zoom_x 290 | } 291 | 292 | /// Return the maximally zoomed in zoom level, in 293 | /// units of time per x pixel. 294 | pub fn max_zoom_x(&self) -> f64 { 295 | self.s.config.max_zoom_x 296 | } 297 | 298 | /// Return a description of the current view 299 | pub fn view(&self) -> View { 300 | self.s.view_read.borrow().get() 301 | } 302 | 303 | /// Return the most recent time value. 304 | pub fn last_t(&self) -> Time { 305 | self.s.store.borrow().last_t() 306 | } 307 | 308 | /// Return the longest ago time value that is still stored. Note 309 | /// that the oldest data is discarded to keep memory usage bounded. 310 | pub fn first_t(&self) -> Time { 311 | self.s.store.borrow().first_t() 312 | } 313 | 314 | fn _clone(&self) -> Graph { 315 | Graph { 316 | s: self.s.clone() 317 | } 318 | } 319 | 320 | /// Change the zoom level on the graph. 321 | /// 322 | /// Any value you pass in will be clamped between `base_zoom_x` and `max_zoom_x`. 323 | pub fn set_zoom_x(&self, new_zoom_x: f64) { 324 | debug!("set_zoom_x new_zoom_x={}", new_zoom_x); 325 | let new_zoom_x = new_zoom_x.min(self.s.config.base_zoom_x) 326 | .max(self.s.config.max_zoom_x); 327 | { 328 | // Scope the mutable borrow of view. 329 | let new_view = View { 330 | zoom_x: new_zoom_x, 331 | .. self.s.view_read.borrow().get() 332 | }; 333 | self.s.view_write.borrow_mut().set(&new_view); 334 | } 335 | 336 | redraw_graph(&*self.s); 337 | } 338 | 339 | /// Sets the graph to follow the latest data. 340 | pub fn set_follow(&self) { 341 | debug!("set_follow"); 342 | { 343 | // Scope the mutable borrow of view. 344 | let new_view = View { 345 | mode: ViewMode::Following, 346 | last_drawn_t: self.s.store.borrow().last_t(), 347 | .. self.s.view_read.borrow().get() 348 | }; 349 | self.s.view_write.borrow_mut().set(&new_view); 350 | } 351 | redraw_graph(&*self.s); 352 | } 353 | 354 | /// Scrolls the graph to view a certain time value. 355 | pub fn scroll(&self, new_val: f64) { 356 | debug!("scroll new_val={}", new_val); 357 | { 358 | // Scope the borrow_mut on view 359 | let mut view = self.s.view_read.borrow().get(); 360 | view.mode = ViewMode::Scrolled; 361 | let new_t = (new_val as u32 + 362 | ((view.zoom_x * self.s.config.graph_width as f64) as u32)) 363 | .min(self.s.store.borrow().last_t()); 364 | // Snap new_t to a whole pixel. 365 | let new_t = (((new_t as f64) / view.zoom_x).floor() * view.zoom_x) as u32; 366 | view.last_drawn_t = new_t; 367 | view.last_drawn_x = 0; 368 | self.s.view_write.borrow_mut().set(&view); 369 | debug!("scroll_change, v={:?} view={:?}", new_val, view); 370 | } 371 | // TODO: Maybe keep the section of the graph that's still valid when scrolling. 372 | redraw_graph(&self.s); 373 | } 374 | 375 | /// Return an observable that lets you track the current `View`, 376 | /// which describes what is currently showing on the graph. 377 | pub fn view_observable(&mut self) -> RefMut> { 378 | self.s.view_read.borrow_mut() 379 | } 380 | 381 | /// Returns the `DrawingArea` gtk widget the graph is drawn on, so 382 | /// you can connect to its signals. 383 | pub fn drawing_area(&self) -> gtk::DrawingArea { 384 | self.s.drawing_area.clone() 385 | } 386 | 387 | /// Maps a position on `drawing_area` to the data point that is 388 | /// currently drawn there. Useful for handling clicks on the graph. 389 | /// 390 | /// Returns None if no appropriate point can be found, for example 391 | /// if the data point for a scroll position has already been 392 | /// discarded. 393 | pub fn drawing_area_pos_to_point(&self, x: f64, _y: f64) -> Option { 394 | let view = self.s.view_read.borrow().get(); 395 | let t = (view.last_drawn_t as i64 + 396 | ((x - (view.last_drawn_x as f64)) * view.zoom_x) as i64) 397 | .max(0).min(view.last_drawn_t as i64) 398 | as u32; 399 | let pt = self.s.store.borrow().query_point(t).unwrap()?; 400 | 401 | // If we are getting a point >= 10 pixels away, return None instead. 402 | // This can happen when old data has been discarded but is still on screen. 403 | let pt: Option = if (pt.t - t) >= (view.zoom_x * 10.0) as u32 { 404 | None 405 | } else { 406 | Some(pt) 407 | }; 408 | 409 | pt 410 | } 411 | } 412 | 413 | /// Handle the graph's draw signal. 414 | fn graph_draw(_ctrl: >k::DrawingArea, ctx: &cairo::Context, s: &State) -> Inhibit { 415 | trace!("graph_draw"); 416 | 417 | // Copy from the backing_surface, which was updated elsewhere 418 | ctx.rectangle(0.0, 0.0, s.config.graph_width as f64, s.config.graph_height as f64); 419 | ctx.set_source_surface(&s.backing_surface.borrow(), 420 | 0.0 /* offset x */, 0.0 /* offset y */); 421 | ctx.fill(); 422 | 423 | // Calculate FPS, log it once a second. 424 | s.fps_count.set(s.fps_count.get() + 1); 425 | let now = Instant::now(); 426 | if (now - s.fps_timer.get()).as_secs() >= 1 { 427 | debug!("fps: {}", s.fps_count.get()); 428 | s.fps_count.set(0); 429 | s.fps_timer.set(now); 430 | } 431 | 432 | Inhibit(false) 433 | } 434 | 435 | /// Redraw the whole graph to the backing store 436 | fn redraw_graph(s: &State) { 437 | trace!("redraw_graph"); 438 | let backing_surface = s.backing_surface.borrow(); 439 | { 440 | // Clear backing_surface 441 | let c = cairo::Context::new(&*backing_surface); 442 | c.set_source_rgb(BACKGROUND_COLOR.0, 443 | BACKGROUND_COLOR.1, 444 | BACKGROUND_COLOR.2); 445 | c.rectangle(0.0, 0.0, s.config.graph_width as f64, s.config.graph_height as f64); 446 | c.fill(); 447 | } 448 | 449 | let mut view = s.view_read.borrow().get(); 450 | let cols = s.config.data_source.borrow().get_colors().unwrap(); 451 | let t1: u32 = view.last_drawn_t; 452 | let t0: u32 = (t1 as i64 - (s.config.graph_width as f64 * view.zoom_x) as i64).max(0) as u32; 453 | let patch_dims = ((((t1-t0) as f64 / view.zoom_x).floor() as u32) 454 | .min(s.config.graph_width) as usize, 455 | s.config.graph_height as usize); 456 | if patch_dims.0 > 0 { 457 | let x = match view.mode { 458 | ViewMode::Following => (s.config.graph_width as usize) - patch_dims.0, 459 | ViewMode::Scrolled => 0, 460 | }; 461 | render_patch(&*backing_surface, 462 | &s.store.borrow(), 463 | &cols, 464 | patch_dims.0 /* w */, patch_dims.1 /* h */, 465 | x /* x */, 0 /* y */, 466 | t0, t1, 467 | 0 /* v0 */, std::u16::MAX /* v1 */, 468 | s.config.point_style); 469 | view.last_drawn_x = (x + patch_dims.0) as u32; 470 | view.last_drawn_t = t1; 471 | s.view_write.borrow_mut().set(&view); 472 | } 473 | s.drawing_area.queue_draw(); 474 | } 475 | 476 | fn tick(s: &State) { 477 | trace!("tick"); 478 | // Ingest new data 479 | let new_data = s.config.data_source.borrow_mut().get_data().unwrap(); 480 | 481 | 482 | if new_data.len() > 0 { 483 | s.store.borrow_mut().ingest(&*new_data).unwrap(); 484 | let t_latest = s.store.borrow().last_t(); 485 | 486 | // Discard old data if there is any 487 | let window_base_dt = (s.config.graph_width as f64 * s.config.base_zoom_x) as u32; 488 | let keep_window = s.config.windows_to_store * window_base_dt; 489 | let discard_start = if t_latest >= keep_window { t_latest - keep_window } else { 0 }; 490 | if discard_start > 0 { 491 | s.store.borrow_mut().discard(0, discard_start).unwrap(); 492 | } 493 | 494 | let mut view = s.view_read.borrow().get(); 495 | 496 | view.min_t = s.store.borrow().first_t(); 497 | view.max_t = t_latest; 498 | s.view_write.borrow_mut().set(&view); 499 | 500 | if view.mode == ViewMode::Following || 501 | (view.mode == ViewMode::Scrolled && view.last_drawn_x < s.config.graph_width) { 502 | 503 | // Draw the new data. 504 | 505 | // Calculate the size of the latest patch to render. 506 | // TODO: Handle when patch_dims.0 >= s.config.graph_width. 507 | // TODO: Handle scrolled when new data is offscreen (don't draw) 508 | let patch_dims = 509 | ((((t_latest - view.last_drawn_t) as f64 / view.zoom_x) 510 | .floor() as usize) 511 | .min(s.config.graph_width as usize), 512 | s.config.graph_height as usize); 513 | // If there is more than a pixel's worth of data to render since we last drew, 514 | // then draw it. 515 | if patch_dims.0 > 0 { 516 | let new_t = view.last_drawn_t + (patch_dims.0 as f64 * view.zoom_x) as u32; 517 | 518 | let patch_offset_x = match view.mode { 519 | ViewMode::Following => s.config.graph_width - (patch_dims.0 as u32), 520 | ViewMode::Scrolled => view.last_drawn_x, 521 | }; 522 | 523 | if view.mode == ViewMode::Following { 524 | // Copy existing graph to the temp surface, offsetting it to the left. 525 | let c = cairo::Context::new(&*s.temp_surface.borrow()); 526 | c.set_source_surface(&*s.backing_surface.borrow(), 527 | -(patch_dims.0 as f64) /* x offset*/, 0.0 /* y offset */); 528 | c.rectangle(0.0, // x offset 529 | 0.0, // y offset 530 | patch_offset_x as f64, // width 531 | s.config.graph_height as f64); // height 532 | c.fill(); 533 | 534 | // Present new graph by swapping the surfaces. 535 | s.backing_surface.swap(&s.temp_surface); 536 | } 537 | 538 | let cols = s.config.data_source.borrow().get_colors().unwrap(); 539 | render_patch(&s.backing_surface.borrow(), 540 | &s.store.borrow(), 541 | &cols, 542 | patch_dims.0 /* w */, patch_dims.1 /* h */, 543 | patch_offset_x as usize, 0 /* y */, 544 | view.last_drawn_t, new_t, 545 | 0 /* v0 */, std::u16::MAX /* v1 */, 546 | s.config.point_style); 547 | 548 | view.last_drawn_t = new_t; 549 | view.last_drawn_x = (patch_offset_x + patch_dims.0 as u32) 550 | .min(s.config.graph_width); 551 | s.view_write.borrow_mut().set(&view); 552 | } 553 | 554 | // Invalidate the graph widget so we get a draw request. 555 | s.drawing_area.queue_draw(); 556 | } 557 | } 558 | } 559 | 560 | fn render_patch( 561 | surface: &cairo::Surface, 562 | store: &Store, cols: &[Color], 563 | pw: usize, ph: usize, 564 | x: usize, y: usize, 565 | t0: Time, t1: Time, v0: Value, v1: Value, 566 | point_style: PointStyle, 567 | ) { 568 | trace!("render_patch: pw={}, ph={} x={} y={}", pw, ph, x, y); 569 | let mut patch_bytes = vec![0u8; pw * ph * BYTES_PER_PIXEL]; 570 | render_patch_to_bytes(store, cols, &mut patch_bytes, 571 | pw, ph, 572 | t0, t1, 573 | v0, v1, 574 | point_func_select(point_style) 575 | ).unwrap(); 576 | copy_patch(surface, patch_bytes, 577 | pw, ph, 578 | x, y); 579 | } 580 | 581 | fn point_func_select(s: PointStyle) -> &'static dyn Fn(usize, usize, usize, usize, &mut [u8], Color) { 582 | match s { 583 | PointStyle::Point => &point_func_point, 584 | PointStyle::Cross => &point_func_cross, 585 | } 586 | } 587 | 588 | fn point_func_point(x: usize, y: usize, pbw: usize, pbh: usize, pb: &mut [u8], col: Color) { 589 | if x < pbw && y < pbh { 590 | let i = BYTES_PER_PIXEL * (pbw * y + x); 591 | pb[i+2] = col.0; // R 592 | pb[i+1] = col.1; // G 593 | pb[i+0] = col.2; // B 594 | pb[i+3] = 255; // A 595 | } 596 | } 597 | 598 | fn point_func_cross(x: usize, y: usize, pbw: usize, pbh: usize, pb: &mut [u8], col: Color) { 599 | let mut pixel = |px: usize, py: usize| { 600 | if px < pbw && py < pbh { 601 | let i = BYTES_PER_PIXEL * (pbw * py + px); 602 | pb[i+2] = col.0; // R 603 | pb[i+1] = col.1; // G 604 | pb[i+0] = col.2; // B 605 | pb[i+3] = 255; // A 606 | } 607 | }; 608 | 609 | pixel(x+1, y+1); 610 | if y >= 1 { 611 | pixel(x+1, y-1); 612 | } 613 | pixel(x , y ); 614 | if x >= 1 { 615 | if y >= 1 { 616 | pixel(x-1, y-1); 617 | } 618 | pixel(x-1, y+1); 619 | } 620 | } 621 | 622 | fn render_patch_to_bytes( 623 | store: &Store, cols: &[Color], 624 | pb: &mut [u8], pbw: usize, pbh: usize, 625 | t0: Time, t1: Time, v0: Value, v1: Value, 626 | point_func: &dyn Fn(usize, usize, usize, usize, &mut [u8], Color), 627 | ) -> Result<()> 628 | { 629 | trace!("render_patch_to_bytes: pbw={}", pbw); 630 | assert!(pbw >= 1); 631 | 632 | let points = store.query_range(t0, t1)?; 633 | for p in points { 634 | assert!(p.t >= t0 && p.t <= t1); 635 | 636 | let x = (((p.t-t0) as f32 / (t1-t0) as f32) * pbw as f32) as usize; 637 | if !(x < pbw) { 638 | // Should be guaranteed by store.query. 639 | panic!("x < pbw: x={} pbw={}", x, pbw); 640 | } 641 | 642 | for ch in 0..store.val_len() { 643 | let col = cols[ch as usize % cols.len()]; 644 | let y = (((p.vals()[ch as usize]-v0) as f32 / (v1-v0) as f32) * pbh as f32) as usize; 645 | if y >= pbh { 646 | // Skip points that are outside our render patch. 647 | continue; 648 | } 649 | // Mirror the y-axis 650 | let y = pbh - y; 651 | 652 | point_func(x, y, pbw, pbh, pb, col); 653 | } 654 | } 655 | 656 | Ok(()) 657 | } 658 | 659 | fn copy_patch( 660 | backing_surface: &cairo::Surface, 661 | bytes: Vec, 662 | w: usize, h: usize, 663 | x: usize, y: usize 664 | ) { 665 | 666 | trace!("copy_patch w={} x={}", w, x); 667 | 668 | // Create an ImageSurface from our bytes 669 | let patch_surface = cairo::ImageSurface::create_for_data( 670 | bytes, 671 | cairo::Format::ARgb32, 672 | w as i32, 673 | h as i32, 674 | (w * BYTES_PER_PIXEL) as i32 /* stride */ 675 | ).unwrap(); 676 | 677 | // Copy from the ImageSurface to backing_surface 678 | let c = cairo::Context::new(&backing_surface); 679 | // Fill target area with background colour. 680 | c.rectangle(x as f64, 681 | y as f64, 682 | w as f64, // width 683 | h as f64 /* height */); 684 | c.set_source_rgb(DRAWN_AREA_BACKGROUND_COLOR.0, 685 | DRAWN_AREA_BACKGROUND_COLOR.1, 686 | DRAWN_AREA_BACKGROUND_COLOR.2); 687 | c.fill_preserve(); 688 | // Fill target area with patch data. 689 | c.set_source_surface(&patch_surface, 690 | x as f64, 691 | y as f64); 692 | c.fill(); 693 | } 694 | 695 | fn create_backing_surface(win: &gdk::Window, w: u32, h: u32) -> cairo::Surface { 696 | let surface = 697 | win.create_similar_image_surface( 698 | cairo::Format::Rgb24.into(), 699 | w as i32 /* width */, 700 | h as i32 /* height */, 701 | 1 /* scale */).unwrap(); 702 | { 703 | // Clear backing_surface 704 | let c = cairo::Context::new(&surface); 705 | c.set_source_rgb(BACKGROUND_COLOR.0, 706 | BACKGROUND_COLOR.1, 707 | BACKGROUND_COLOR.2); 708 | c.rectangle(0.0, 0.0, w as f64, h as f64); 709 | c.fill(); 710 | } 711 | surface 712 | } 713 | -------------------------------------------------------------------------------- /src/graph_with_controls.rs: -------------------------------------------------------------------------------- 1 | use crate::{Config, Graph, View, ViewMode}; 2 | use gdk::prelude::*; 3 | use gtk::prelude::*; 4 | use std::{rc::Rc, cell::RefCell}; 5 | 6 | /// A GTK widget that contains a graph and controls to navigate it. 7 | /// 8 | /// If you want a customised graph with your own controls, you might 9 | /// want to try using `Graph`, which is designed for customisation. 10 | pub struct GraphWithControls { 11 | s: Rc, 12 | } 13 | 14 | struct State { 15 | controls_box: gtk::Box, 16 | 17 | scrollbar: gtk::Scrollbar, 18 | btn_zoom_x_out: gtk::Button, 19 | btn_zoom_x_in: gtk::Button, 20 | btn_follow: gtk::Button, 21 | 22 | graph: RefCell, 23 | } 24 | 25 | impl GraphWithControls { 26 | /// Build and show a `GraphWithControls` widget in the target `gtk::Container`. 27 | pub fn build_ui(config: Config, container: &C, gdk_window: &gdk::Window 28 | ) -> GraphWithControls 29 | where C: IsA + IsA 30 | { 31 | // Create the controls 32 | 33 | let controls_box = gtk::BoxBuilder::new() 34 | .orientation(gtk::Orientation::Vertical) 35 | .spacing(0) 36 | .build(); 37 | container.add(&controls_box); 38 | 39 | let graph = Graph::build_ui(config, &controls_box, gdk_window); 40 | 41 | let scrollbar = gtk::ScrollbarBuilder::new() 42 | .orientation(gtk::Orientation::Horizontal) 43 | .halign(gtk::Align::Start) 44 | .build(); 45 | scrollbar.set_property_width_request(graph.width() as i32); 46 | controls_box.add(&scrollbar); 47 | 48 | let buttons_box = gtk::BoxBuilder::new() 49 | .orientation(gtk::Orientation::Horizontal) 50 | .height_request(35) 51 | .build(); 52 | controls_box.add(&buttons_box); 53 | 54 | let btn_follow = gtk::ButtonBuilder::new() 55 | .label("Follow") 56 | .build(); 57 | buttons_box.add(&btn_follow); 58 | 59 | let btn_zoom_x_in = gtk::ButtonBuilder::new() 60 | .label("Zoom X in") 61 | .build(); 62 | buttons_box.add(&btn_zoom_x_in); 63 | 64 | let btn_zoom_x_out = gtk::ButtonBuilder::new() 65 | .label("Zoom X out") 66 | .sensitive(false) 67 | .build(); 68 | buttons_box.add(&btn_zoom_x_out); 69 | 70 | // Set up the state 71 | 72 | let s = Rc::new(State { 73 | controls_box: controls_box.clone(), 74 | 75 | scrollbar: scrollbar.clone(), 76 | btn_zoom_x_out: btn_zoom_x_out.clone(), 77 | btn_zoom_x_in: btn_zoom_x_in.clone(), 78 | btn_follow: btn_follow.clone(), 79 | 80 | graph: RefCell::new(graph), 81 | }); 82 | let g = GraphWithControls { 83 | s: s.clone(), 84 | }; 85 | 86 | update_controls(&g, &g.s.graph.borrow().view()); 87 | 88 | // Event handlers that require state. 89 | 90 | let gc = g.clone(); 91 | scrollbar.connect_change_value(move |_ctrl, _scroll_type, v| { 92 | gc.s.graph.borrow().scroll(v); 93 | Inhibit(false) 94 | }); 95 | 96 | let gc = g.clone(); 97 | btn_follow.connect_clicked(move |_btn| { 98 | gc.s.graph.borrow().set_follow() 99 | }); 100 | 101 | let gc = g.clone(); 102 | btn_zoom_x_in.connect_clicked(move |_btn| { 103 | let new = gc.s.graph.borrow().view().zoom_x / 2.0; 104 | gc.s.graph.borrow().set_zoom_x(new); 105 | }); 106 | 107 | let gc = g.clone(); 108 | btn_zoom_x_out.connect_clicked(move |_btn| { 109 | let new = gc.s.graph.borrow().view().zoom_x * 2.0; 110 | gc.s.graph.borrow().set_zoom_x(new); 111 | }); 112 | 113 | { 114 | // Scope the borrow on view_observable. 115 | let gc = g.clone(); 116 | s.graph.borrow_mut().view_observable().connect(move |view| { 117 | update_controls(&gc, &view); 118 | }); 119 | } 120 | 121 | let gc = g.clone(); 122 | g.s.graph.borrow().drawing_area().add_events(gdk::EventMask::BUTTON_PRESS_MASK); 123 | g.s.graph.borrow().drawing_area().connect_button_press_event(move |_ctrl, ev| { 124 | drawing_area_button_press(&gc, ev) 125 | }); 126 | 127 | // Show everything recursively 128 | controls_box.show_all(); 129 | 130 | g 131 | } 132 | 133 | fn clone(&self) -> GraphWithControls { 134 | GraphWithControls { 135 | s: self.s.clone() 136 | } 137 | } 138 | 139 | /// Show the graph and controls. 140 | pub fn show(&self) { 141 | self.s.controls_box.show(); 142 | self.s.graph.borrow().show(); 143 | } 144 | 145 | /// Hide the graph and controls. 146 | pub fn hide(&self) { 147 | self.s.controls_box.hide(); 148 | self.s.graph.borrow().hide(); 149 | } 150 | } 151 | 152 | /// Update the controls (GTK widgets) from the current state. 153 | fn update_controls(g: &GraphWithControls, view: &View) { 154 | trace!("update_controls view={:?}", view); 155 | let s = &g.s; 156 | let adj = s.scrollbar.get_adjustment(); 157 | let window_width_t = (s.graph.borrow().width() as f64) * view.zoom_x; 158 | 159 | adj.set_upper(s.graph.borrow().last_t() as f64); 160 | adj.set_lower(s.graph.borrow().first_t() as f64); 161 | adj.set_step_increment(window_width_t / 4.0); 162 | adj.set_page_increment(window_width_t / 2.0); 163 | adj.set_page_size(window_width_t); 164 | 165 | match view.mode { 166 | ViewMode::Following => 167 | adj.set_value(s.graph.borrow().last_t() as f64), 168 | ViewMode::Scrolled => adj.set_value(view.last_drawn_t as f64 - 169 | ((s.graph.borrow().width() as f64) * view.zoom_x)), 170 | } 171 | 172 | s.btn_zoom_x_in.set_sensitive(view.zoom_x > s.graph.borrow().max_zoom_x()); 173 | s.btn_zoom_x_out.set_sensitive(view.zoom_x < s.graph.borrow().base_zoom_x()); 174 | s.btn_follow.set_sensitive(view.mode == ViewMode::Scrolled); 175 | } 176 | 177 | fn drawing_area_button_press(g: &GraphWithControls, ev: &gdk::EventButton) -> Inhibit { 178 | let pos = ev.get_position(); 179 | let pt = g.s.graph.borrow().drawing_area_pos_to_point(pos.0, pos.1); 180 | debug!("drawing_area button_press pos={:?} pt={:?}", pos, pt); 181 | 182 | if let Some(pta) = pt { 183 | let info_bar = gtk::InfoBarBuilder::new() 184 | .halign(gtk::Align::Start) 185 | .build(); 186 | g.s.controls_box.add(&info_bar); 187 | info_bar.set_property_width_request(g.s.graph.borrow().width() as i32); 188 | 189 | info_bar.get_content_area().add(>k::Label::new(Some("Time, [Values]:"))); 190 | 191 | let entry = gtk::EntryBuilder::new() 192 | .text(&*format!("{}, {:?}", pta.t, pta.vals())) 193 | .editable(false) 194 | .hexpand(true) 195 | .build(); 196 | info_bar.get_content_area().add(&entry); 197 | 198 | let close_btn = gtk::ButtonBuilder::new() 199 | .label("Close") 200 | .build(); 201 | info_bar.get_action_area().unwrap().add(&close_btn); 202 | 203 | let ibc = info_bar.clone(); 204 | let cbc = g.s.controls_box.clone(); 205 | close_btn.connect_clicked(move |_btn| { 206 | cbc.remove(&ibc); 207 | }); 208 | 209 | info_bar.show_all(); 210 | } 211 | 212 | Inhibit(false) 213 | } 214 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | #![deny(missing_docs)] 3 | 4 | //! A real-time graphing experiment. 5 | //! 6 | //! rt-graph uses GTK (via the gtk-rs Rust bindings) for its UI and is 7 | //! designed to be embedded into any gtk::Container in your 8 | //! application. 9 | 10 | #[macro_use] 11 | extern crate derive_builder; 12 | 13 | #[macro_use] 14 | extern crate log; 15 | 16 | use std::fmt::Debug; 17 | 18 | mod graph; 19 | pub use graph::{Config, ConfigBuilder, Graph, PointStyle, View, ViewMode}; 20 | 21 | mod graph_with_controls; 22 | pub use graph_with_controls::GraphWithControls; 23 | 24 | mod null_data_source; 25 | pub use null_data_source::NullDataSource; 26 | 27 | pub mod observable_value; 28 | 29 | mod signal; 30 | pub use signal::Signal; 31 | 32 | mod store; 33 | use store::Store; 34 | 35 | mod test_data_generator; 36 | pub use test_data_generator::TestDataGenerator; 37 | 38 | /// Represents an error that could occur using the crate 39 | #[derive(Debug)] 40 | pub enum Error { 41 | /// An error described by a `String`. 42 | String(String), 43 | } 44 | 45 | /// Represents either a value or an error from the crate. 46 | pub type Result = std::result::Result; 47 | 48 | /// A point in time when a data point was emitted. 49 | pub type Time = u32; 50 | 51 | /// The value of a data point 52 | pub type Value = u16; 53 | 54 | /// A data point on a graph. 55 | #[derive(Debug, Clone)] 56 | pub struct Point { 57 | /// The time when this data point was emitted. 58 | pub t: Time, 59 | 60 | /// The values this point holds. 61 | pub vs: Vec, 62 | } 63 | 64 | impl Point { 65 | /// Return the time when this data point was emitted. 66 | pub fn t(&self) -> Time { 67 | self.t 68 | } 69 | 70 | /// Return the values that this point holds. 71 | pub fn vals(&self) -> &[Value] { 72 | &self.vs 73 | } 74 | } 75 | 76 | /// A color in RGB format. 77 | /// 78 | /// The tuple values are the red, green, and blue components of the 79 | /// color respectively. 80 | #[derive(Clone, Copy)] 81 | pub struct Color(pub u8, pub u8, pub u8); 82 | 83 | impl Color { 84 | /// Create a color from red, green, and blue components. 85 | pub fn from_rgb(r: u8, g: u8, b: u8) -> Color { 86 | Color(r, g, b) 87 | } 88 | 89 | /// Return the red component of the `Color`. 90 | pub fn r(&self) -> u8 { 91 | self.0 92 | } 93 | 94 | /// Return the green component of the `Color`. 95 | pub fn g(&self) -> u8 { 96 | self.1 97 | } 98 | 99 | /// Return the blue component of the `Color`. 100 | pub fn b(&self) -> u8 { 101 | self.2 102 | } 103 | } 104 | 105 | #[cfg(test)] 106 | mod color_tests { 107 | use super::Color; 108 | 109 | #[test] 110 | fn values() { 111 | let c = Color::from_rgb(10, 20, 30); 112 | assert_eq!(c.r(), 10); 113 | assert_eq!(c.g(), 20); 114 | assert_eq!(c.b(), 30); 115 | } 116 | } 117 | 118 | /// Implement this to get your own data into a `Graph`. 119 | pub trait DataSource: Debug + Send { 120 | /// Return whatever points you have available when this method is called. 121 | /// 122 | /// Each point must have a `t` field greater than the previous point. 123 | /// 124 | /// Each point must have a `vs` field with length equal to the 125 | /// value returned by `get_num_values`. 126 | /// 127 | /// This is currently called once a frame. 128 | fn get_data(&mut self) -> Result>; 129 | 130 | /// The number of values that each Point will have. 131 | fn get_num_values(&self) -> Result; 132 | 133 | /// Return the colors you want to use to display each value of the graph. 134 | /// 135 | /// Some sample colors are returned by default. 136 | /// 137 | /// If you don't supply enough colors for the number of values 138 | /// returned, these colors will be repeated. 139 | fn get_colors(&self) -> Result> { 140 | Ok(vec![Color(255u8, 0u8, 0u8), 141 | Color(0u8, 255u8, 0u8), 142 | Color(0u8, 0u8, 255u8) 143 | ]) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/null_data_source.rs: -------------------------------------------------------------------------------- 1 | use crate::{DataSource, Point, Result}; 2 | 3 | /// A `DataSource` that returns no data points. 4 | #[derive(Debug)] 5 | pub struct NullDataSource; 6 | 7 | impl DataSource for NullDataSource { 8 | fn get_data(&mut self) -> Result> { 9 | Ok(vec![]) 10 | } 11 | 12 | fn get_num_values(&self) -> Result { 13 | Ok(1) 14 | } 15 | } 16 | 17 | impl NullDataSource { 18 | /// Constructs a new instance of `NullDataSource`. 19 | pub fn new() -> NullDataSource { 20 | NullDataSource 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/observable_value.rs: -------------------------------------------------------------------------------- 1 | //! An implementation of the Observer pattern. 2 | 3 | use std::{ 4 | cell::RefCell, 5 | rc::Rc, 6 | }; 7 | 8 | /// A value that implements the Observer pattern. 9 | /// 10 | /// Consumers can connect to receive callbacks when the value changes. 11 | pub struct ObservableValue 12 | where T: Clone 13 | { 14 | value: T, 15 | subs: Vec>, 16 | new_id: usize, 17 | } 18 | 19 | /// The identifier for a subscription, used to disconnect it when no longer required. 20 | #[derive(Clone, Copy, Eq, PartialEq)] 21 | pub struct SubscriptionId(usize); 22 | 23 | struct Subscription { 24 | id: SubscriptionId, 25 | callback: Box 26 | } 27 | 28 | impl ObservableValue 29 | where T: Clone 30 | { 31 | /// Construct an `ObservableValue`. 32 | pub fn new(initial_value: T) -> ObservableValue { 33 | ObservableValue { 34 | value: initial_value, 35 | new_id: 0, 36 | subs: Vec::with_capacity(0), 37 | } 38 | } 39 | 40 | /// Get the current value 41 | pub fn get(&self) -> &T { 42 | &self.value 43 | } 44 | 45 | /// Set a new value and notify all connected subscribers. 46 | pub fn set(&mut self, new_value: &T) { 47 | self.value = new_value.clone(); 48 | self.call_subscribers(); 49 | } 50 | 51 | fn call_subscribers(&self) { 52 | for sub in self.subs.iter() { 53 | (sub.callback)(&self.value) 54 | } 55 | } 56 | 57 | /// Connect a new subscriber that will receive callbacks when the 58 | /// value is set. 59 | /// 60 | /// Returns a SubscriptionId to disconnect the subscription when 61 | /// no longer required. 62 | pub fn connect(&mut self, callback: F) -> SubscriptionId 63 | where F: (Fn(&T)) + 'static 64 | { 65 | let id = SubscriptionId(self.new_id); 66 | self.new_id = self.new_id.checked_add(1).expect("No overflow"); 67 | 68 | self.subs.push(Subscription { 69 | id, 70 | callback: Box::new(callback), 71 | }); 72 | self.subs.shrink_to_fit(); 73 | id 74 | } 75 | 76 | /// Disconnect an existing subscription. 77 | pub fn disconnect(&mut self, sub_id: SubscriptionId) { 78 | self.subs.retain(|sub| sub.id != sub_id); 79 | self.subs.shrink_to_fit(); 80 | } 81 | 82 | /// Divide this instance into a read half (can listen for updates, but cannot 83 | /// write new values) and a write half (can write new values). 84 | pub fn split(self) -> (ReadHalf, WriteHalf) { 85 | let inner = Rc::new(RefCell::new(self)); 86 | ( 87 | ReadHalf { 88 | inner: inner.clone(), 89 | }, 90 | WriteHalf { 91 | inner: inner 92 | } 93 | ) 94 | } 95 | } 96 | 97 | /// The read half of an `ObservableValue`, which can only listen for 98 | /// updates and read the current value. 99 | pub struct ReadHalf 100 | where T: Clone 101 | { 102 | inner: Rc>>, 103 | } 104 | 105 | /// The write half of an `ObservableValue`, which can write new values. 106 | pub struct WriteHalf 107 | where T: Clone 108 | { 109 | inner: Rc>>, 110 | } 111 | 112 | impl ReadHalf 113 | where T: Clone 114 | { 115 | /// Get the current value 116 | pub fn get(&self) -> T { 117 | self.inner.borrow().get().clone() 118 | } 119 | 120 | /// Connect a new subscriber that will receive callbacks when the 121 | /// value is set. 122 | /// 123 | /// Returns a SubscriptionId to disconnect the subscription when 124 | /// no longer required. 125 | pub fn connect(&mut self, callback: F) -> SubscriptionId 126 | where F: (Fn(&T)) + 'static 127 | { 128 | self.inner.borrow_mut().connect(callback) 129 | } 130 | 131 | /// Disconnect an existing subscription. 132 | pub fn disconnect(&mut self, sub_id: SubscriptionId) { 133 | self.inner.borrow_mut().disconnect(sub_id) 134 | } 135 | } 136 | 137 | impl WriteHalf 138 | where T: Clone 139 | { 140 | /// Set a new value and notify all connected subscribers. 141 | pub fn set(&mut self, new_value: &T) { 142 | self.inner.borrow_mut().set(new_value) 143 | } 144 | } 145 | 146 | #[cfg(test)] 147 | mod test { 148 | use std::{ 149 | cell::Cell, 150 | rc::Rc, 151 | }; 152 | use super::ObservableValue; 153 | 154 | #[test] 155 | fn new_get_set() { 156 | let mut ov = ObservableValue::new(17); 157 | assert_eq!(*ov.get(), 17); 158 | 159 | ov.set(&18); 160 | assert_eq!(*ov.get(), 18); 161 | } 162 | 163 | #[test] 164 | fn connect_set() { 165 | let mut ov = ObservableValue::::new(17); 166 | let mirror: Rc> = Rc::new(Cell::new(0)); 167 | 168 | let mc = mirror.clone(); 169 | ov.connect(move |val| { 170 | mc.set(*val); 171 | }); 172 | 173 | // Check callback not yet called. 174 | assert_eq!(mirror.get(), 0); 175 | 176 | ov.set(&18); 177 | 178 | // Check the callback was called with the correct value. 179 | assert_eq!(mirror.get(), 18); 180 | } 181 | 182 | #[test] 183 | fn disconnect() { 184 | let mut ov = ObservableValue::::new(17); 185 | let mirror_1: Rc> = Rc::new(Cell::new(0)); 186 | let mirror_2: Rc> = Rc::new(Cell::new(0)); 187 | 188 | let mc1 = mirror_1.clone(); 189 | let sub_id_1 = ov.connect(move |val| { 190 | mc1.set(*val); 191 | }); 192 | 193 | let mc2 = mirror_2.clone(); 194 | let _sub_id_2 = ov.connect(move |val| { 195 | mc2.set(*val); 196 | }); 197 | 198 | // Both mirrors are connected with callbacks, set() updates both mirror values. 199 | ov.set(&18); 200 | assert_eq!(mirror_1.get(), 18); 201 | assert_eq!(mirror_2.get(), 18); 202 | 203 | ov.disconnect(sub_id_1); 204 | 205 | // Only sub_id_2 is still connected, set() only updates one mirror value. 206 | ov.set(&19); 207 | assert_eq!(mirror_1.get(), 18); 208 | assert_eq!(mirror_2.get(), 19); 209 | } 210 | 211 | #[test] 212 | fn split() { 213 | let ov = ObservableValue::::new(17); 214 | let (mut r, mut w) = ov.split(); 215 | 216 | let mirror: Rc> = Rc::new(Cell::new(0)); 217 | 218 | let mc = mirror.clone(); 219 | r.connect(move |val| { 220 | mc.set(*val); 221 | }); 222 | 223 | // Check callback not yet called. 224 | assert_eq!(mirror.get(), 0); 225 | 226 | w.set(&18); 227 | 228 | // Check the callback was called with the correct value. 229 | assert_eq!(mirror.get(), 18); 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/signal.rs: -------------------------------------------------------------------------------- 1 | /// Implements a broadcast-listener / callback / observable pattern. 2 | /// 3 | /// `Signal` holds a list of subscriptions, each with a callback closure to run 4 | /// on the next broadcast. 5 | /// 6 | /// As `rt-graph` uses GTK, the terminology (`Signal` struct and its method names) match 7 | /// GTK's terms. 8 | pub struct Signal { 9 | subs: Vec>, 10 | new_id: usize, 11 | } 12 | 13 | struct Subscription { 14 | id: SubscriptionId, 15 | callback: Box, 16 | } 17 | 18 | /// The identifier for a subscription, used to disconnect it when no longer required. 19 | #[derive(Clone, Copy, Eq, PartialEq)] 20 | pub struct SubscriptionId(usize); 21 | 22 | impl Signal { 23 | /// Construct a new `Signal`. 24 | pub fn new() -> Signal { 25 | Signal { 26 | subs: Vec::with_capacity(0), 27 | new_id: 0, 28 | } 29 | } 30 | 31 | /// Connect a new subscriber that will receive callbacks when the 32 | /// signal is raised. 33 | /// 34 | /// Returns a SubscriptionId to disconnect the subscription when 35 | /// no longer required. 36 | pub fn connect(&mut self, callback: F) -> SubscriptionId 37 | where F: (Fn(T)) + 'static 38 | { 39 | let id = SubscriptionId(self.new_id); 40 | self.new_id = self.new_id.checked_add(1).expect("No overflow"); 41 | 42 | self.subs.push(Subscription { 43 | id, 44 | callback: Box::new(callback), 45 | }); 46 | self.subs.shrink_to_fit(); 47 | 48 | id 49 | } 50 | 51 | /// Notify existing subscribers. 52 | pub fn raise(&self, value: T) { 53 | for sub in self.subs.iter() { 54 | (sub.callback)(value.clone()) 55 | } 56 | } 57 | 58 | /// Disconnect an existing subscription. 59 | pub fn disconnect(&mut self, id: SubscriptionId) { 60 | self.subs.retain(|sub| sub.id != id); 61 | self.subs.shrink_to_fit(); 62 | } 63 | } 64 | 65 | #[cfg(test)] 66 | mod test { 67 | use crate::Signal; 68 | use std::{cell::Cell, rc::Rc}; 69 | 70 | #[test] 71 | fn signal() { 72 | let mut sig = Signal::new(); 73 | 74 | let data: Rc> = Rc::new(Cell::new(0)); 75 | assert_eq!(data.get(), 0); 76 | 77 | let dc = data.clone(); 78 | let subid = sig.connect(move |v| { 79 | dc.set(dc.get() + v); 80 | }); 81 | assert_eq!(data.get(), 0); 82 | 83 | sig.raise(1); 84 | assert_eq!(data.get(), 1); 85 | 86 | sig.raise(2); 87 | assert_eq!(data.get(), 3); 88 | 89 | sig.disconnect(subid); 90 | 91 | sig.raise(0); 92 | assert_eq!(data.get(), 3); 93 | } 94 | 95 | #[test] 96 | fn signal_multiple_subscriptions() { 97 | let mut sig = Signal::new(); 98 | 99 | let data: Rc> = Rc::new(Cell::new(0)); 100 | assert_eq!(data.get(), 0); 101 | 102 | let dc = data.clone(); 103 | let sub1 = sig.connect(move |_v| { 104 | dc.set(dc.get() + 1); 105 | }); 106 | let dc = data.clone(); 107 | let sub2 = sig.connect(move |_v| { 108 | dc.set(dc.get() + 10); 109 | }); 110 | 111 | sig.raise(0); 112 | assert_eq!(data.get(), 11); 113 | 114 | sig.disconnect(sub1); 115 | 116 | sig.raise(0); 117 | assert_eq!(data.get(), 21); 118 | 119 | sig.disconnect(sub2); 120 | sig.raise(0); 121 | 122 | sig.raise(0); 123 | assert_eq!(data.get(), 21); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/store.rs: -------------------------------------------------------------------------------- 1 | use crate::{Error, Point, Result, Time, Value}; 2 | use std::collections::BTreeMap; 3 | 4 | pub struct Store { 5 | last_t: Time, 6 | val_len: u8, 7 | all: BTreeMap>, 8 | } 9 | 10 | impl Store { 11 | pub fn new(val_len: u8) -> Store { 12 | Store { 13 | last_t: 0, 14 | val_len, 15 | all: BTreeMap::new(), 16 | } 17 | } 18 | 19 | pub fn ingest(&mut self, ps: &[Point]) -> Result<()> { 20 | for p in ps { 21 | if p.t <= self.last_t { 22 | return Err(Error::String("t <= last_t".to_owned())); 23 | } 24 | self.last_t = p.t; 25 | 26 | assert!(p.vs.len() == self.val_len as usize); 27 | self.all.insert(p.t, p.vs.clone()); 28 | } 29 | 30 | trace!("ingest all.len={} last_t={}", self.all.len(), self.last_t); 31 | 32 | Ok(()) 33 | } 34 | 35 | pub fn discard(&mut self, t0: Time, t1: Time) -> Result<()> { 36 | for t in self.all.range(t0..t1).map(|(t,_vs)| *t).collect::>() { 37 | self.all.remove(&t); 38 | } 39 | Ok(()) 40 | } 41 | 42 | /// Returns a Vec of the points with t >= t0, < t1. 43 | pub fn query_range(&self, t0: Time, t1: Time) -> Result> { 44 | let rv: Vec = 45 | self.all.range(t0..t1) 46 | .map(|(t,vs)| Point { t: *t, vs: vs.clone() }) 47 | .collect(); 48 | trace!("query t0={} t1={} rv.len={}", t0, t1, rv.len()); 49 | Ok(rv) 50 | } 51 | 52 | /// Returns the first point with t >= given t. 53 | pub fn query_point(&self, t: Time) -> Result> { 54 | let rv = self.all.range(t..) 55 | .map(|(t,vs)| Point { t: *t, vs: vs.clone() }) 56 | .next(); 57 | Ok(rv) 58 | } 59 | 60 | pub fn last_t(&self) -> Time { 61 | self.last_t 62 | } 63 | 64 | pub fn first_t(&self) -> Time { 65 | self.query_point(0).unwrap() 66 | .map_or(0, |pt| pt.t) 67 | } 68 | 69 | pub fn val_len(&self) -> u8 { 70 | self.val_len 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/test_data_generator.rs: -------------------------------------------------------------------------------- 1 | use crate::{DataSource, Point, Result, Time, Value}; 2 | 3 | const GEN_POINTS: u32 = 200; 4 | const GEN_T_INTERVAL: Time = 20; 5 | 6 | /// A struct that implements `DataSource` by showing dummy test data. 7 | #[derive(Debug)] 8 | pub struct TestDataGenerator { 9 | curr_t: Time, 10 | interval: Time, 11 | interval_inc: bool, 12 | } 13 | 14 | impl TestDataGenerator { 15 | /// Construct a new instance. 16 | pub fn new() -> TestDataGenerator { 17 | TestDataGenerator { 18 | curr_t: 1, 19 | interval: GEN_T_INTERVAL, 20 | interval_inc: false, 21 | } 22 | } 23 | } 24 | 25 | impl DataSource for TestDataGenerator { 26 | fn get_data(&mut self) -> Result> { 27 | let mut rv: Vec = Vec::with_capacity(GEN_POINTS as usize); 28 | for _i in 0..GEN_POINTS { 29 | let t = self.curr_t; 30 | rv.push(Point { 31 | t, 32 | vs: vec![trig_sample(1.0, 1.0/10000.0, 0.0, t), 33 | ((100000.0 / (t as f64)) * trig_sample(1.0, 1.0/10000.0, std::f32::consts::PI / 3.0, t) as f64) as Value, 34 | trig_sample(0.5, 1.0/5000.0, 0.0, t)], 35 | }); 36 | 37 | self.curr_t += self.interval; 38 | } 39 | 40 | let switch = if self.interval_inc { 41 | self.interval += 1; 42 | self.interval == GEN_T_INTERVAL 43 | } else { 44 | self.interval -= 1; 45 | self.interval == 1 46 | }; 47 | if switch { 48 | self.interval_inc = !self.interval_inc; 49 | } 50 | Ok(rv) 51 | } 52 | 53 | fn get_num_values(&self) -> Result { 54 | Ok(3) 55 | } 56 | } 57 | 58 | fn trig_sample(scale: f32, scale_period: f32, offset: f32, t: Time) -> Value { 59 | let float_val = (offset + t as f32 * scale_period).sin() * scale; 60 | let int_val = (((float_val + 1.0) / 2.0) * std::u16::MAX as f32) as Value; 61 | int_val 62 | } 63 | --------------------------------------------------------------------------------