diff --git a/.cargo/config.toml b/.cargo/config.toml index 94a019606a..e6dc44d3f9 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -10,3 +10,7 @@ rustflags = ["-C", "target-cpu=x86-64-v3", "-C", "force-frame-pointers=yes"] # Force frame pointers to make performance and memory profiling much easier [target.aarch64-unknown-linux-gnu] rustflags = ["-C", "target-cpu=neoverse-n1", "-C", "force-frame-pointers=yes"] + +[alias] +desktop = "run --package switchyard-desktop-install --" +desktop-icons = "run --package switchyard-desktop-icons --" diff --git a/Cargo.lock b/Cargo.lock index de2768bb84..9865d60791 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,6 +31,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloc-no-stdlib" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2fb6cfd47bf496ff64095c20eaba0c201404ee38714d4142fcfa1dc334fcc7a" + +[[package]] +name = "alloc-stdlib" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5c1865780388bfa186411ab5f247819487fc4864c6e9c3106611fa347586e1" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -102,6 +117,15 @@ version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + [[package]] name = "arc-swap" version = "1.9.2" @@ -177,6 +201,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "atomic" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" +dependencies = [ + "bytemuck", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -286,27 +319,60 @@ dependencies = [ "tower-service", ] +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec 0.6.3", +] + [[package]] name = "bit-set" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ - "bit-vec", + "bit-vec 0.8.0", ] +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bit-vec" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.13.1" @@ -316,6 +382,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "block2" version = "0.6.2" @@ -331,23 +406,80 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" +[[package]] +name = "brotli" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8b851b75c23ca7873623d612fe49bd1989aeb03d08fb9432187eb253d3d4c6b" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941cd9bd4ddab83cb46fa5a2d428f1c857b24ac78cb876cf7beb710840934bd7" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "bumpalo" version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + [[package]] name = "bytecount" version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +dependencies = [ + "serde", +] [[package]] name = "cairo-rs" @@ -355,7 +487,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ - "bitflags", + "bitflags 2.13.1", "cairo-sys-rs", "glib", "libc", @@ -374,6 +506,58 @@ dependencies = [ "system-deps", ] +[[package]] +name = "camino" +version = "1.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbad30e4b4c14a39e3cc8aed085a12a327257c316619c93581e017bc52be591" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cargo_toml" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f4b26e751e711a5302649417f2da046dce6391b2ea30a4820f37462314f0b9" +dependencies = [ + "semver", + "serde", + "toml 1.1.3+spec-1.1.0", +] + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.2.67" @@ -386,6 +570,23 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a347dcabdae9c31b0825fd6a8bed285ec9c2acb89c47827126d52fa4f59cece3" +dependencies = [ + "fnv", + "uuid", + "web-time", +] + [[package]] name = "cfg-expr" version = "0.15.8" @@ -415,7 +616,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.3.0", "rand_core 0.10.1", ] @@ -496,6 +697,39 @@ dependencies = [ "memchr", ] +[[package]] +name = "compact_str" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cookie" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" +dependencies = [ + "time", + "version_check", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -512,6 +746,39 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.1", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.1", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "cpufeatures" version = "0.3.0" @@ -530,6 +797,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crossbeam-channel" version = "0.5.17" @@ -546,97 +819,403 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" [[package]] -name = "data-encoding" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" - -[[package]] -name = "deadpool" -version = "0.12.3" +name = "crossterm" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "deadpool-runtime", - "lazy_static", - "num_cpus", - "tokio", + "bitflags 2.13.1", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", ] [[package]] -name = "deadpool-runtime" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" - -[[package]] -name = "diff" -version = "0.1.13" +name = "crossterm_winapi" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] [[package]] -name = "dirs" -version = "7.0.0" +name = "crypto-common" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d57d423b3c82e89b9a24ca3091fee61f456a26edbd28d26c65906f4bc1dcd8f" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ - "dirs-sys", + "generic-array", + "typenum", ] [[package]] -name = "dirs-sys" -version = "0.5.0" +name = "csscolorparser" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys 0.61.2", + "lab", + "phf 0.11.3", ] [[package]] -name = "dispatch2" -version = "0.3.1" +name = "cssparser" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +checksum = "8c9cdaae01d5ed7882b04d795e7f752f46ff52d2fa3b50a20d28c464510bba98" dependencies = [ - "bitflags", - "objc2", + "cssparser-macros", + "dtoa-short", + "itoa", + "phf 0.13.1", + "smallvec", ] [[package]] -name = "displaydoc" -version = "0.2.6" +name = "cssparser-macros" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +checksum = "d045de693cb712d0b22c6a64be5b953f67b3ce00ab5ad3dd5d8b441886ab8e1a" dependencies = [ - "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] -name = "dpi" -version = "0.1.2" +name = "ctor" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +checksum = "914a755b7c2d4af2bdcff7ce1739e2db9a1b81a9b07123d8015786ae03c0980d" [[package]] -name = "dunce" -version = "1.0.5" +name = "darling" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", +] [[package]] -name = "either" -version = "1.16.0" +name = "darling" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" +dependencies = [ + "darling_core 0.24.1", + "darling_macro 0.24.1", +] [[package]] -name = "email_address" +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_core" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 3.0.3", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" +dependencies = [ + "darling_core 0.24.1", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + +[[package]] +name = "dbus" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "deadpool" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b" +dependencies = [ + "deadpool-runtime", + "lazy_static", + "num_cpus", + "tokio", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "deltae" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d57d423b3c82e89b9a24ca3091fee61f456a26edbd28d26c65906f4bc1dcd8f" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.1", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "dom_query" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac5fca71e65e94cc718a6e2af65d6e0f9c6027751c2aa562fbb5087fda639bc" +dependencies = [ + "bit-set 0.8.0", + "cssparser", + "foldhash", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "email_address" version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" @@ -644,6 +1223,26 @@ dependencies = [ "serde", ] +[[package]] +name = "embed-resource" +version = "3.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6168487b5e7eb2673e6efe1200b1848016e436a6ca0b8031bf04bd9d493a249f" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.3+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + [[package]] name = "envoy-types" version = "0.6.1" @@ -661,6 +1260,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + [[package]] name = "errno" version = "0.3.14" @@ -671,13 +1281,32 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set 0.5.3", + "regex", +] + [[package]] name = "fancy-regex" version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1e1dacd0d2082dfcf1351c4bdd566bbe89a2b263235a2b50058f1e130a47277" dependencies = [ - "bit-set", + "bit-set 0.8.0", "regex-automata", "regex-syntax", ] @@ -707,12 +1336,35 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "finl_unicode" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bb028c8b4148c9ee0cca68fcd9add6044e81d3619f48577ddf13a263d047a2" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "flate2" version = "1.1.10" @@ -747,6 +1399,33 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -928,6 +1607,57 @@ dependencies = [ "system-deps", ] +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -1007,7 +1737,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" dependencies = [ - "bitflags", + "bitflags 2.13.1", "futures-channel", "futures-core", "futures-executor", @@ -1048,6 +1778,12 @@ dependencies = [ "system-deps", ] +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + [[package]] name = "gobject-sys" version = "0.18.0" @@ -1123,13 +1859,30 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", "tracing", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + [[package]] name = "hashbrown" version = "0.17.1" @@ -1159,6 +1912,22 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8" +dependencies = [ + "log", + "markup5ever", +] + [[package]] name = "http" version = "1.4.2" @@ -1266,7 +2035,7 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-util", @@ -1307,6 +2076,16 @@ dependencies = [ "cc", ] +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + [[package]] name = "icu_collections" version = "2.2.0" @@ -1389,6 +2168,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.1.0" @@ -1410,6 +2195,30 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png 0.18.1", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -1417,35 +2226,161 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "infer" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4200d433cbd5178df7797c9c2e75b348b728e39631cf14520d1e2fc424201f4" +dependencies = [ + "cfb", +] + +[[package]] +name = "instability" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3b5acc1e2fd9375041a388da33d1eb8aed5f7a8c0dd3543e3ea2805adfbe20" +dependencies = [ + "darling 0.24.1", + "indoc", + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jiff" +version = "0.2.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b005715dcbeb0089a3c0dab99f2ff1cc3b2525323552703d648585d342a383" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link", +] + +[[package]] +name = "jiff-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c" +dependencies = [ + "defmt", + "log", ] [[package]] -name = "ipnet" -version = "2.12.0" +name = "jiff-static" +version = "0.2.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +checksum = "2cc9817253cf7c7ee4684451bd327e88d6f3658014e54a29198625590650695c" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] [[package]] -name = "is_terminal_polyfill" -version = "1.70.2" +name = "jiff-tzdb" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +checksum = "fa8377070c6bae868759445e5a77f66d84f0b72f3a054bfb00e6d038b8282da7" [[package]] -name = "itertools" -version = "0.14.0" +name = "jiff-tzdb-platform" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" dependencies = [ - "either", + "jiff-tzdb", ] [[package]] -name = "itoa" -version = "1.0.18" +name = "jni" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] [[package]] name = "jni" @@ -1456,7 +2391,7 @@ dependencies = [ "cfg-if", "combine", "jni-macros", - "jni-sys", + "jni-sys 0.4.1", "log", "simd_cesu8", "thiserror 2.0.18", @@ -1477,6 +2412,15 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + [[package]] name = "jni-sys" version = "0.4.1" @@ -1517,6 +2461,28 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json-patch" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7421438de105a0827e44fadd05377727847d717c80ce29a229f85fd04c427b72" +dependencies = [ + "jsonptr 0.7.1", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "jsonptr" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5a3cc660ba5d72bce0b3bb295bf20847ccbb40fd423f3f05b61273672e561fe" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "jsonptr" version = "0.8.1" @@ -1537,7 +2503,7 @@ dependencies = [ "bytecount", "data-encoding", "email_address", - "fancy-regex", + "fancy-regex 0.18.0", "fraction", "getrandom 0.3.4", "idna", @@ -1579,16 +2545,33 @@ dependencies = [ "serde_json", ] +[[package]] +name = "kasuari" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" +dependencies = [ + "hashbrown 0.16.1", + "portable-atomic", + "thiserror 2.0.18", +] + [[package]] name = "keyboard-types" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fbe853b403ae61a04233030ae8a79d94975281ed9770a1f9e246732b534b28d" dependencies = [ - "bitflags", + "bitflags 2.13.1", "serde", ] +[[package]] +name = "lab" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" + [[package]] name = "lazy_static" version = "1.5.0" @@ -1625,6 +2608,15 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + [[package]] name = "libloading" version = "0.7.4" @@ -1635,6 +2627,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libredox" version = "0.1.25" @@ -1645,22 +2643,12 @@ dependencies = [ ] [[package]] -name = "libxdo" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00333b8756a3d28e78def82067a377de7fa61b24909000aeaa2b446a948d14db" -dependencies = [ - "libxdo-sys", -] - -[[package]] -name = "libxdo-sys" -version = "0.11.0" +name = "line-clipping" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db23b9e7e2b7831bbd8aac0bbeeeb7b68cbebc162b227e7052e8e55829a09212" +checksum = "e752191d037c44ad111a8caa762921926658402f01cc1253f7bef2020ece4f5e" dependencies = [ - "libc", - "x11", + "bitflags 2.13.1", ] [[package]] @@ -1675,6 +2663,12 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + [[package]] name = "lock_api" version = "0.4.14" @@ -1690,12 +2684,42 @@ version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +[[package]] +name = "lru" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef9ac18847474e638e3702b76c65d4eb93428471a74778ef0f1be711717f89b5" +dependencies = [ + "hashbrown 0.17.1", +] + [[package]] name = "lru-slab" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "mac_address" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b367b50a9be9a5d3b5718c1da74e5d6b9c17647f89752ee2562a470cc3c4eb1a" +dependencies = [ + "nix 0.30.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "markup5ever" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7122d987ec5f704ee56f6e5b41a7d93722e9aae27ae07cafa4036c4d3f9757de" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + [[package]] name = "matchers" version = "0.2.0" @@ -1717,6 +2741,12 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "memmem" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" + [[package]] name = "memoffset" version = "0.9.1" @@ -1738,6 +2768,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1765,10 +2801,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.61.2", ] +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "mslnk" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86c97310150b7f496a93f31690da7822b99d95ff68ca9d30fb09d3ad54375c76" +dependencies = [ + "bitflags 1.3.2", + "byteorder", + "log", +] + [[package]] name = "muda" version = "0.20.0" @@ -1779,17 +2837,47 @@ dependencies = [ "dpi", "gtk", "keyboard-types", - "libxdo", "objc2", "objc2-app-kit", "objc2-core-foundation", "objc2-foundation", "once_cell", - "png", + "png 0.18.1", + "serde", "thiserror 2.0.18", "windows-sys 0.61.2", ] +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.1", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + [[package]] name = "nemo-relay-plugin" version = "0.8.4" @@ -1810,7 +2898,7 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e31bccc10dd27dda5035f9ba9dcda9b9a5ad17abe6474e43d7147b006163e95b" dependencies = [ - "bitflags", + "bitflags 2.13.1", "chrono", "serde", "serde_json", @@ -1818,6 +2906,47 @@ dependencies = [ "uuid", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.13.1", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.13.1", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1866,6 +2995,23 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -1915,6 +3061,37 @@ dependencies = [ "libc", ] +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 2.0.2", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "objc2" version = "0.6.4" @@ -1922,6 +3099,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", + "objc2-exception-helper", ] [[package]] @@ -1930,17 +3108,10 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags", + "bitflags 2.13.1", "block2", - "libc", "objc2", - "objc2-cloud-kit", - "objc2-core-data", "objc2-core-foundation", - "objc2-core-graphics", - "objc2-core-image", - "objc2-core-text", - "objc2-core-video", "objc2-foundation", "objc2-quartz-core", ] @@ -1951,7 +3122,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ - "bitflags", + "bitflags 2.13.1", "objc2", "objc2-foundation", ] @@ -1962,7 +3133,6 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" dependencies = [ - "bitflags", "objc2", "objc2-foundation", ] @@ -1973,7 +3143,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags", + "bitflags 2.13.1", "dispatch2", "objc2", ] @@ -1984,7 +3154,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags", + "bitflags 2.13.1", "dispatch2", "objc2", "objc2-core-foundation", @@ -2001,69 +3171,120 @@ dependencies = [ "objc2-foundation", ] +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + [[package]] name = "objc2-core-text" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ - "bitflags", + "bitflags 2.13.1", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.1", "objc2", "objc2-core-foundation", - "objc2-core-graphics", ] [[package]] -name = "objc2-core-video" +name = "objc2-quartz-core" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags", + "bitflags 2.13.1", "objc2", "objc2-core-foundation", - "objc2-core-graphics", - "objc2-io-surface", + "objc2-foundation", ] [[package]] -name = "objc2-encode" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" - -[[package]] -name = "objc2-foundation" +name = "objc2-ui-kit" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags", + "bitflags 2.13.1", "block2", - "libc", "objc2", + "objc2-cloud-kit", + "objc2-core-data", "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", ] [[package]] -name = "objc2-io-surface" +name = "objc2-user-notifications" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" dependencies = [ - "bitflags", "objc2", - "objc2-core-foundation", + "objc2-foundation", ] [[package]] -name = "objc2-quartz-core" +name = "objc2-web-kit" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" dependencies = [ - "bitflags", + "bitflags 2.13.1", + "block2", "objc2", + "objc2-app-kit", + "objc2-core-foundation", "objc2-foundation", ] @@ -2175,12 +3396,54 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + [[package]] name = "outref" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" +[[package]] +name = "palette" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddeed8580d347d2abf3dcf06a5f0b3dc020258338526b277847cd4248a70fc64" +dependencies = [ + "approx", + "libm", + "palette_derive", + "palette_math", +] + +[[package]] +name = "palette_derive" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88537020289b719d81be994ccf1bbf4990f477e2f69ee52fe3e45f43a02e56be" +dependencies = [ + "by_address", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "palette_math" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e6eb142958d64335fb0e345c5b9ead2ecd6fc438c307e9d7d3c4fd428dbaf12" +dependencies = [ + "libm", +] + [[package]] name = "pango" version = "0.18.3" @@ -2235,6 +3498,153 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pest" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45d3aca230fad2e6f6317ca0a72724338c4960cb97168a85cdee66df4a9a21a8" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284b60557f2c4a2e72ad3f2d34d42685a2fa4a6a61d0d2a10c0ae2a5e916c2cf" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9d1f08a115309ee99268cf85e5228e0e56aa9caf8841ec12866b6be07c3109" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pest_meta" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed93ba1a9ffcca32130a5188701c81c0c49cf00d4b7c5007d5148951d743adcb" +dependencies = [ + "pest", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros 0.13.1", + "phf_shared 0.13.1", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.8", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.13" @@ -2267,13 +3677,39 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +[[package]] +name = "plist" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896bade328c13f7042a297ea5ac5b0951f6cf989dea5f32c2fd98da398195cb" +dependencies = [ + "base64 0.23.1", + "indexmap 2.14.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + [[package]] name = "png" version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags", + "bitflags 2.13.1", "crc32fast", "fdeflate", "flate2", @@ -2286,6 +3722,15 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + [[package]] name = "potential_utf" version = "0.1.5" @@ -2295,6 +3740,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -2304,6 +3755,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "prefill-router" version = "0.3.0" @@ -2462,6 +3919,12 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "pxfm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" + [[package]] name = "pyo3" version = "0.28.3" @@ -2544,6 +4007,15 @@ dependencies = [ "serde", ] +[[package]] +name = "quick-xml" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b" +dependencies = [ + "memchr", +] + [[package]] name = "quinn" version = "0.11.11" @@ -2622,6 +4094,15 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.9.5" @@ -2654,28 +4135,141 @@ dependencies = [ ] [[package]] -name = "rand_core" -version = "0.9.5" +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "ratatui" +version = "0.30.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3274ba0a2c5e1bcad2a2005d20f4dc59dad26b2eb0940fb094500dba4099d57d" +dependencies = [ + "instability", + "ratatui-core", + "ratatui-crossterm", + "ratatui-macros", + "ratatui-termina", + "ratatui-termwiz", + "ratatui-widgets", + "serde", +] + +[[package]] +name = "ratatui-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbb175c433c8e28a809d1f5773a2ae96e68c0ce40db865cbab1020bf33ae479c" +dependencies = [ + "bitflags 2.13.1", + "compact_str", + "critical-section", + "hashbrown 0.17.1", + "itertools", + "kasuari", + "lru", + "palette", + "serde", + "strum", + "thiserror 2.0.18", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + +[[package]] +name = "ratatui-crossterm" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567584a3b0e6a8203c23de40b4861497266725eb5363dbfd18a1edd603cca9f0" +dependencies = [ + "cfg-if", + "crossterm", + "instability", + "ratatui-core", +] + +[[package]] +name = "ratatui-macros" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7dc68daa7498a43e4d68e0eb078427e10c38fbcfbb1e42d955f1fa2140d814" +dependencies = [ + "ratatui-core", + "ratatui-widgets", +] + +[[package]] +name = "ratatui-termina" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0bf912d9e66f057a759d92e386a280ea886b352ab757d6ac4d653c7ed2c43c2" +dependencies = [ + "instability", + "ratatui-core", + "termina", +] + +[[package]] +name = "ratatui-termwiz" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +checksum = "faf03e0380b7744054d6cb74224fe3adf062a029754933f575ca1e3b4c2ce977" dependencies = [ - "getrandom 0.3.4", + "ratatui-core", + "termwiz", ] [[package]] -name = "rand_core" -version = "0.10.1" +name = "ratatui-widgets" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +checksum = "66e3d19bcc9130ca376277d93b60767ff121ace3be06f5f95f81dd68956407d1" +dependencies = [ + "bitflags 2.13.1", + "hashbrown 0.17.1", + "indoc", + "instability", + "itertools", + "line-clipping", + "ratatui-core", + "serde", + "strum", + "time", + "unicode-segmentation", + "unicode-width", +] [[package]] -name = "rand_pcg" -version = "0.10.2" +name = "raw-window-handle" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" -dependencies = [ - "rand_core 0.10.1", -] +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" [[package]] name = "redox_syscall" @@ -2683,7 +4277,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.13.1", ] [[package]] @@ -2725,7 +4319,7 @@ dependencies = [ "ahash", "fluent-uri", "getrandom 0.3.4", - "hashbrown", + "hashbrown 0.17.1", "itoa", "micromap", "parking_lot", @@ -2768,7 +4362,7 @@ version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-core", @@ -2803,6 +4397,29 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfd" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20dafead71c16a34e1ff357ddefc8afc11e7d51d6d2b9fbd07eaa48e3e540220" +dependencies = [ + "block2", + "dispatch2", + "js-sys", + "libc", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "percent-encoding", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.61.2", +] + [[package]] name = "ring" version = "0.17.14" @@ -2838,7 +4455,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags", + "bitflags 2.13.1", "errno", "libc", "linux-raw-sys", @@ -2889,7 +4506,7 @@ checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ "core-foundation", "core-foundation-sys", - "jni", + "jni 0.22.4", "log", "once_cell", "rustls", @@ -2950,6 +4567,57 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.119", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -2962,7 +4630,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags", + "bitflags 2.13.1", "core-foundation", "core-foundation-sys", "libc", @@ -2979,11 +4647,45 @@ dependencies = [ "libc", ] +[[package]] +name = "selectors" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8adfa1c298912827b8a28b223b3b874357397ae706e6190acd9bf28cee99114d" +dependencies = [ + "bitflags 2.13.1", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf 0.13.1", + "phf_codegen 0.13.1", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "self-replace" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ec815b5eab420ab893f63393878d89c90fdd94c0bcc44c07abb8ad95552fb7" +dependencies = [ + "fastrand", + "tempfile", + "windows-sys 0.52.0", +] + [[package]] name = "semver" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] [[package]] name = "serde" @@ -2995,6 +4697,18 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -3015,13 +4729,24 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "serde_json" version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ - "indexmap", + "indexmap 2.14.0", "itoa", "memchr", "serde", @@ -3040,6 +4765,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "serde_spanned" version = "0.6.9" @@ -3070,6 +4806,81 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee78f1fbe43ac4a0e47aadb3dbd357b69eb0d3793e948624cd03dd2750ab1c0a" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "jiff", + "schemars 0.9.0", + "schemars 1.2.2", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8705578779c2b6bd90d84d66eb2e206b708b1a4d7b9f17641b293545bf1c7e46" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -3085,6 +4896,27 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -3117,44 +4949,128 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "siphasher" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" + [[package]] name = "slab" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] -name = "smallvec" -version = "1.15.2" +name = "static_assertions" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] -name = "socket2" -version = "0.5.10" +name = "string_cache" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" dependencies = [ - "libc", - "windows-sys 0.52.0", + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.13.1", + "precomputed-hash", ] [[package]] -name = "socket2" -version = "0.6.5" +name = "string_cache_codegen" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" dependencies = [ - "libc", - "windows-sys 0.61.2", + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", ] -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - [[package]] name = "strsim" version = "0.11.1" @@ -3188,6 +5104,67 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "swift-rs" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e45c444e496845d3f2a351146bff59aae4975b2280238df1dfaa0c7d1846f38e" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "switchyard-desktop" +version = "0.3.0" +dependencies = [ + "chrono", + "objc2-app-kit", + "objc2-foundation", + "ratatui", + "rfd", + "security-framework", + "serde", + "serde_json", + "switchyard-desktop-icons", + "switchyard-desktop-install", + "switchyard-runner", + "tauri", + "tauri-build", + "tempfile", + "toml 0.9.12+spec-1.1.0", + "toml_edit 0.25.13+spec-1.1.0", + "url", + "uuid", + "windows", +] + +[[package]] +name = "switchyard-desktop-icons" +version = "0.3.0" +dependencies = [ + "image", + "tempfile", +] + +[[package]] +name = "switchyard-desktop-install" +version = "0.3.0" +dependencies = [ + "mslnk", + "plist", + "security-framework", + "self-replace", + "serde", + "serde_json", + "switchyard-runner", + "tempfile", + "toml 0.9.12+spec-1.1.0", + "toml_edit 0.25.13+spec-1.1.0", + "windows-sys 0.61.2", +] + [[package]] name = "switchyard-libsy" version = "0.3.0" @@ -3195,7 +5172,7 @@ dependencies = [ "async-trait", "futures", "http", - "jsonptr", + "jsonptr 0.8.1", "jsonschema", "opentelemetry", "opentelemetry_sdk", @@ -3241,21 +5218,6 @@ dependencies = [ "wiremock", ] -[[package]] -name = "switchyard-menubar" -version = "0.3.0" -dependencies = [ - "chrono", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "serde", - "serde_json", - "tempfile", - "toml 0.9.12+spec-1.1.0", - "tray-icon", -] - [[package]] name = "switchyard-nemo-relay-plugin" version = "0.3.0" @@ -3366,152 +5328,502 @@ dependencies = [ "parking_lot", "prometheus", "reqwest", - "rustls", + "rustls", + "serde", + "serde_json", + "switchyard-libsy", + "switchyard-llm-client", + "switchyard-protocol", + "switchyard-runner", + "switchyard-translation", + "tempfile", + "tokio", + "tower", + "tracing", + "tracing-opentelemetry", + "tracing-subscriber", +] + +[[package]] +name = "switchyard-skill-distillation" +version = "0.3.0" +dependencies = [ + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", +] + +[[package]] +name = "switchyard-soak" +version = "0.3.0" +dependencies = [ + "axum", + "clap", + "futures-util", + "humantime", + "parking_lot", + "rand 0.10.2", + "reqwest", + "serde", + "serde_json", + "tempfile", + "tokio", +] + +[[package]] +name = "switchyard-translation" +version = "0.3.0" +dependencies = [ + "async-stream", + "base64 0.22.1", + "futures", + "pretty_assertions", + "serde", + "serde_json", + "switchyard-protocol", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.37.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37f381f4e048e6cdf038b5705f8cf14ad108279d46eb968140a7b291aba9400" +dependencies = [ + "bitflags 2.13.1", + "block2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni 0.21.1", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7eeb6d99155545da6150a1795945f16ac9c178deb2a5f2e74d776107bd5849" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "tauri" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed99ee9694a2deb776d91cae48ac7411ddfc89ecae2f9b5041111d8c88f2ace9" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni 0.21.1", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", "serde", "serde_json", - "switchyard-libsy", - "switchyard-llm-client", - "switchyard-protocol", - "switchyard-runner", - "switchyard-translation", - "tempfile", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.18", "tokio", - "tower", - "tracing", - "tracing-opentelemetry", - "tracing-subscriber", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", ] [[package]] -name = "switchyard-skill-distillation" -version = "0.3.0" +name = "tauri-build" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59563ca5b331fd97f27ee9672ec8d36d33bb3c9e2c9f6710a1a5687256052e45" dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", "serde", "serde_json", - "thiserror 2.0.18", - "tokio", + "tauri-utils", + "tauri-winres", + "walkdir", ] [[package]] -name = "switchyard-soak" -version = "0.3.0" +name = "tauri-codegen" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e50213e463086ecc283d7e9f51c30cc0fd304ad457425158018be26dbf0d0a80" dependencies = [ - "axum", - "clap", - "futures-util", - "humantime", - "parking_lot", - "rand 0.10.2", - "reqwest", + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.18.1", + "proc-macro2", + "quote", + "semver", "serde", "serde_json", - "tempfile", - "tokio", + "sha2", + "syn 2.0.119", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", ] [[package]] -name = "switchyard-translation" -version = "0.3.0" +name = "tauri-macros" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8129fb43bb6e0d6b73ad3315cd172beaab17cd28899c594b1b32c80edbbfd99c" dependencies = [ - "async-stream", - "base64", - "futures", - "pretty_assertions", + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-runtime" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a57a2a9b8c8b7fc00d35b510fe808ded7f9f1b44fc1cdda6aee38632ea5413" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni 0.21.1", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", "serde", "serde_json", - "switchyard-protocol", + "tauri-utils", "thiserror 2.0.18", - "tracing", + "url", + "webkit2gtk", + "webview2-com", + "windows", ] [[package]] -name = "syn" -version = "1.0.109" +name = "tauri-runtime-wry" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +checksum = "7f9e7e0c9f80a5130773cfabb6df4dc924518fb1b2c923726966804634b57d22" dependencies = [ - "proc-macro2", - "unicode-ident", + "gtk", + "http", + "jni 0.21.1", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", ] [[package]] -name = "syn" -version = "2.0.119" +name = "tauri-utils" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +checksum = "2ff55a614843b9a3f010f211df637c94df2607175b8d0f6e0f4a7c19e18621ee" dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf 0.13.1", + "plist", "proc-macro2", "quote", - "unicode-ident", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 1.1.3+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", ] [[package]] -name = "syn" -version = "3.0.3" +name = "tauri-winres" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "dunce", + "embed-resource", + "toml 1.1.3+spec-1.1.0", ] [[package]] -name = "sync_wrapper" -version = "1.0.2" +name = "tempfile" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ - "futures-core", + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", ] [[package]] -name = "synstructure" -version = "0.13.2" +name = "tendril" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", + "new_debug_unreachable", ] [[package]] -name = "system-deps" -version = "6.2.2" +name = "termina" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +checksum = "9048a889effe34a5cddee0af7f53285198b16dca3be510858d38dfdb3e62a04e" dependencies = [ - "cfg-expr", - "heck 0.5.0", - "pkg-config", - "toml 0.8.2", - "version-compare", + "bitflags 2.13.1", + "parking_lot", + "rustix", + "signal-hook", + "windows-sys 0.61.2", ] [[package]] -name = "target-lexicon" -version = "0.12.16" +name = "terminfo" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" +dependencies = [ + "fnv", + "nom", + "phf 0.11.3", + "phf_codegen 0.11.3", +] [[package]] -name = "target-lexicon" -version = "0.13.5" +name = "termios" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" +dependencies = [ + "libc", +] [[package]] -name = "tempfile" -version = "3.27.0" +name = "termwiz" +version = "0.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" dependencies = [ - "fastrand", - "getrandom 0.4.3", - "once_cell", - "rustix", - "windows-sys 0.61.2", + "anyhow", + "base64 0.22.1", + "bitflags 2.13.1", + "fancy-regex 0.11.0", + "filedescriptor", + "finl_unicode", + "fixedbitset", + "hex", + "lazy_static", + "libc", + "log", + "memmem", + "nix 0.29.0", + "num-derive", + "num-traits", + "ordered-float", + "pest", + "pest_derive", + "phf 0.11.3", + "sha2", + "signal-hook", + "siphasher", + "terminfo", + "termios", + "thiserror 1.0.69", + "ucd-trie", + "unicode-segmentation", + "vtparse", + "wezterm-bidi", + "wezterm-blob-leases", + "wezterm-color-types", + "wezterm-dynamic", + "wezterm-input-types", + "winapi", ] [[package]] @@ -3563,6 +5875,38 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.8.3" @@ -3669,7 +6013,7 @@ version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ - "indexmap", + "indexmap 2.14.0", "serde_core", "serde_spanned 1.1.1", "toml_datetime 0.7.5+spec-1.1.0", @@ -3684,7 +6028,7 @@ version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" dependencies = [ - "indexmap", + "indexmap 2.14.0", "serde_core", "serde_spanned 1.1.1", "toml_datetime 1.1.1+spec-1.1.0", @@ -3726,7 +6070,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap", + "indexmap 2.14.0", "toml_datetime 0.6.3", "winnow 0.5.40", ] @@ -3737,13 +6081,26 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ - "indexmap", + "indexmap 2.14.0", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.3", "winnow 0.5.40", ] +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + [[package]] name = "toml_parser" version = "1.1.2+spec-1.1.0" @@ -3767,7 +6124,7 @@ checksum = "7e581ba15a835f4d9ea06c55ab1bd4dce26fc53752c69a04aac00703bfb49ba9" dependencies = [ "async-trait", "axum", - "base64", + "base64 0.22.1", "bytes", "h2", "http", @@ -3796,7 +6153,7 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", - "indexmap", + "indexmap 2.14.0", "pin-project-lite", "slab", "sync_wrapper", @@ -3813,7 +6170,7 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags", + "bitflags 2.13.1", "bytes", "futures-util", "http", @@ -3931,7 +6288,8 @@ dependencies = [ "objc2-core-graphics", "objc2-foundation", "once_cell", - "png", + "png 0.18.1", + "serde", "thiserror 2.0.18", "windows-sys 0.61.2", ] @@ -3962,6 +6320,24 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unicode-general-category" version = "1.1.0" @@ -3974,6 +6350,29 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-truncate" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "untrusted" version = "0.9.0" @@ -3990,6 +6389,19 @@ dependencies = [ "idna", "percent-encoding", "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df16f50ef4cc145211879a3867ba757076b25dfee812040dcb0658bd9ae7904b" +dependencies = [ + "icu_properties", + "regex", + "serde", + "url", ] [[package]] @@ -4010,6 +6422,7 @@ version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ + "atomic", "getrandom 0.3.4", "js-sys", "serde", @@ -4045,10 +6458,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] -name = "vsimd" -version = "0.8.0" +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "vtparse" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" +checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" +dependencies = [ + "utf8parse", +] [[package]] name = "walkdir" @@ -4172,6 +6614,62 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web_atoms" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba8b815c1b593dc0baf78dd0f4fc8fdb2de53198fb1163738093e9a311c33fb3" +dependencies = [ + "phf 0.13.1", + "phf_codegen 0.13.1", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + [[package]] name = "webpki-root-certs" version = "1.0.9" @@ -4181,6 +6679,112 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webview2-com" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f89fca7a704cee10dcb3654c1dbb8941d1783132f1917358af75bec37a7d7e6" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "webview2-com-sys" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a07132775117d6065853d9d1178157b8c90e228de47129d6bce2c7edebedfb" +dependencies = [ + "thiserror 2.0.18", + "windows", + "windows-core", +] + +[[package]] +name = "wezterm-bidi" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec" +dependencies = [ + "log", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-blob-leases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" +dependencies = [ + "getrandom 0.3.4", + "mac_address", + "sha2", + "thiserror 1.0.69", + "uuid", +] + +[[package]] +name = "wezterm-color-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296" +dependencies = [ + "csscolorparser", + "deltae", + "lazy_static", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-dynamic" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" +dependencies = [ + "log", + "ordered-float", + "strsim", + "thiserror 1.0.69", + "wezterm-dynamic-derive", +] + +[[package]] +name = "wezterm-dynamic-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "wezterm-input-types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e" +dependencies = [ + "bitflags 1.3.2", + "euclid", + "lazy_static", + "serde", + "wezterm-dynamic", +] + [[package]] name = "winapi" version = "0.3.9" @@ -4212,6 +6816,43 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "window-vibrancy" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "111e51caca442cafd9bab396628ac4d814880eaea4e63dfd76a12e9f342b5580" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "windows-sys 0.61.2", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -4225,6 +6866,17 @@ dependencies = [ "windows-strings", ] +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + [[package]] name = "windows-implement" version = "0.60.2" @@ -4253,6 +6905,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + [[package]] name = "windows-result" version = "0.4.1" @@ -4271,13 +6933,22 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -4289,34 +6960,85 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -4329,24 +7051,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -4373,6 +7119,19 @@ name = "winnow" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" +dependencies = [ + "cfg-if", + "windows-sys 0.61.2", +] [[package]] name = "wiremock" @@ -4381,7 +7140,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031" dependencies = [ "assert-json-diff", - "base64", + "base64 0.22.1", "deadpool", "futures", "http", @@ -4409,6 +7168,50 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "wry" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a819957a01b3119af85e638a38d242af76dbc87d130dca67bfd0441072e21ff0" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni 0.21.1", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core", + "windows-version", + "x11-dl", +] + [[package]] name = "x11" version = "2.21.0" @@ -4419,6 +7222,17 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + [[package]] name = "yansi" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 5e64fbd9e8..a293e50f8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,9 @@ members = [ "crates/protocol", "crates/switchyard-nemo-relay-plugin", "crates/switchyard-runner", - "crates/switchyard-menubar", + "crates/switchyard-desktop", + "crates/switchyard-desktop-icons", + "crates/switchyard-desktop-install", "crates/switchyard-server", "crates/switchyard-skill-distillation", "crates/switchyard-soak", diff --git a/INSTALLATION.md b/INSTALLATION.md index 15755f0c10..25478622db 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -36,6 +36,28 @@ See [Getting Started](docs/getting_started.md#server-path) for a complete TOML deployment and [`switchyard-server`](crates/switchyard-server/README.md) for the configuration reference. +## Server Credentials + +Server configs name credential variables with `api_key_env` in LLM clients or +decision clients. Opening the desktop app from Finder, the Start menu, or a +startup service does not give it your terminal's environment. +On macOS and Windows, run `cargo desktop install` from a terminal where the +declared variables are set. After the server validates the config, the installer +saves those credentials in macOS Keychain or Windows Credential Manager for +that config file. The installed Rust helper owns credential access. Apply and +Check config ask the helper to validate; server startup uses the same helper. +It reads saved values only when the environment lacks a declared variable. +Explicit environment values take precedence; empty or invalid Unicode values +still fail validation. On macOS, rebuilding the helper changes its signing +identity, so Keychain may ask you to approve access again. + +The installer does not put credentials in app settings, service definitions, or +logs. Credentials saved for model discovery stay separate from server +credentials. Reinstalling with a changed variable value updates its saved +credential. Uninstalling keeps credentials along with the config and history. +On Linux, provide the declared variables to both the terminal app and the +systemd user service. The installer does not save Linux credentials. + ## Linux Codex Service This setup is for single-user Linux machines. It requires a systemd user @@ -46,13 +68,16 @@ port while the service is stopped and receive your login and prompts. From a checkout, preview or install the service: ```bash -make install-linux-dry-run -make install-linux +cargo desktop install --dry-run +cargo desktop install systemctl --user status switchyard codex -p sy ``` -The installer builds and installs `~/.switchyard/bin/switchyard-server`. +The installer builds and installs `~/.switchyard/bin/switchyard-server` and +`~/.switchyard/bin/switchyard-desktop`. Run the same command to reinstall from +the current checkout; update the checkout first to install newer code. +Run `~/.switchyard/bin/switchyard-desktop --tui` to open the terminal app. It creates `~/.switchyard/composite.toml` if missing and keeps existing edits. It replaces `~/.config/systemd/user/switchyard.service` and restarts the service. It writes `~/.codex/sy.config.toml`, backing up a changed profile first. @@ -62,20 +87,60 @@ The profile format requires [Codex 0.134.0 or newer](https://developers.openai.c Remove any old `[profiles.sy]` table from `~/.codex/config.toml` before using it. Set `SY_HOME` or `SY_PORT` to change the server directory or port (default 4123). -`SY_HOME` must not contain whitespace, control characters, or a trailing -backslash; `SY_PORT` must contain only digits. `XDG_CONFIG_HOME` and `CODEX_HOME` +`SY_HOME` must be an absolute UTF-8 path without ASCII control characters. +`SY_PORT` must be a number from 1 to 65535. `XDG_CONFIG_HOME` and `CODEX_HOME` set the systemd and Codex config directories. Read server logs with `journalctl --user -u switchyard`. Routing records are stored in `~/.switchyard/routing.jsonl`. Use `systemctl --user edit switchyard` for service changes that survive reinstalling. -Remove the service and profile with `make uninstall-linux`. This also removes +Remove the service and profile with `cargo desktop uninstall`. This also removes marked Codex aliases left by older installs from existing `.bashrc` and `.zshrc` files. If upgrading an older install, uninstall first and run `unalias codex` in any open shell. The server directory, routing records, -profile backups, and systemd drop-in files stay in place. Use the same path -overrides when installing and uninstalling. +profile backups, and systemd drop-in files stay in place. Uninstall uses the +recorded installation. Conflicting environment overrides require +`cargo desktop uninstall --settings FILE` to select it explicitly. + +## Windows Desktop App + +From PowerShell in the repository, run: + +```powershell +cargo desktop install --dry-run +cargo desktop install +``` + +Windows needs the Rust MSVC toolchain, Visual Studio C++ build tools, and +[WebView2](https://v2.tauri.app/start/prerequisites/#windows). +The installer uses Task Scheduler for the current logged-in user. It installs +executables in `%LOCALAPPDATA%\Programs\Switchyard`, falling back to +`%USERPROFILE%\AppData\Local\Programs\Switchyard` when `LOCALAPPDATA` is unset. +It saves that location and adds a Start menu shortcut. Two independent tasks +start the server and desktop app after login with ordinary user privileges. +Quitting the app leaves the server running. + +Run `cargo desktop install` again to build and install the current checkout. +This Cargo command waits for installation to finish. The app's source update +uses the saved checkout and runs from a temporary Rust executable so Windows +can release and replace the installed files. Running the installed installer +also starts a temporary executable and returns before installation finishes; +read `SY_HOME\logs\update.log` for progress and errors. +The installer builds and validates before stopping the tasks. If a binary +replacement fails, it tries to restore the previous binaries. If restoration +also fails, the error names the directory containing recovery files. +Rerun installation if a later task or settings operation fails; the complete +installation is not one transaction. + +Settings, server config, accounts, and routing history stay in +`%USERPROFILE%\.switchyard` unless `SY_HOME` changes the directory. Server and app +logs are in `SY_HOME\logs`. Model-list keys entered in Routes are saved in the +current user's Windows Credential Manager. Coding tools keep their own login files. + +Use `cargo desktop uninstall` to stop and remove both tasks, the shortcut, and +the installed executables. Uninstall preserves settings, accounts, and history, +and makes a recovery copy of the Codex profile before removing it. ## Rust Libraries @@ -106,3 +171,6 @@ uv run pytest tests/ -v The `dev` dependency group contains testing and linting tools and is not exposed in the published wheel metadata. + +See the [desktop and terminal quickstart](crates/switchyard-desktop/README.md) for +first-run setup, reviewed settings changes, usage, recovery, and safe uninstall. diff --git a/Makefile b/Makefile deleted file mode 100644 index 7136e3a263..0000000000 --- a/Makefile +++ /dev/null @@ -1,37 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -.DEFAULT_GOAL := help -.PHONY: help install-linux install-linux-dry-run uninstall-linux install-macos install-macos-dry-run uninstall-macos - -help: - @echo "install-linux Install the systemd user service and Codex profile" - @echo "install-linux-dry-run Preview installation without changes" - @echo "uninstall-linux Remove the service and Codex profile" - @echo "install-macos Install the background server and menu bar app" - @echo "install-macos-dry-run Preview installation without changes" - @echo "uninstall-macos Remove the LaunchAgents and Codex profile" - -## Install the Switchyard background server as a systemd user service. -install-linux: - @scripts/linux/install.sh - -## Print what install-linux would do, without changing anything. -install-linux-dry-run: - @scripts/linux/install.sh --dry-run - -## Remove the systemd user service, the sy Codex profile, and the codex alias. -uninstall-linux: - @scripts/linux/uninstall.sh - -## Install the Switchyard background server and menu bar app for this user. -install-macos: - @scripts/macos/install.sh - -## Print what install-macos would do, without changing anything. -install-macos-dry-run: - @scripts/macos/install.sh --dry-run - -## Remove the LaunchAgents, the sy Codex profile, and the codex alias. -uninstall-macos: - @scripts/macos/uninstall.sh diff --git a/README.md b/README.md index ab16fee2cd..926c879f7d 100644 --- a/README.md +++ b/README.md @@ -93,3 +93,6 @@ releases. Pin the version you integrate. [Report an issue](https://github.com/NVIDIA-NeMo/Switchyard/issues) · [Contribute](CONTRIBUTING.md) · [Code of conduct](CODE_OF_CONDUCT.md) [Apache 2.0](LICENSE). Copyright NVIDIA Corporation. + +The [desktop and terminal quickstart](crates/switchyard-desktop/README.md) covers +local installation, coding-tool connections, sessions, usage, recovery, and uninstall. diff --git a/crates/libsy-llm-client/src/client.rs b/crates/libsy-llm-client/src/client.rs index e1976de291..650f8d591f 100644 --- a/crates/libsy-llm-client/src/client.rs +++ b/crates/libsy-llm-client/src/client.rs @@ -4,7 +4,7 @@ //! [`TranslatingLlmClient`]: encode a neutral //! request, call the configured backend over HTTP, decode the neutral response. -use std::collections::{BTreeMap, BTreeSet, HashMap}; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::future::ready; use std::sync::atomic::{AtomicU64, Ordering}; use std::time::{Duration, SystemTime}; @@ -277,6 +277,9 @@ impl TranslatingLlmClient { if matches!(backend, Backend::OpenAiResponses(_)) { sanitize_openai_responses_provider_body(&mut body); } + if matches!(endpoint, UpstreamEndpoint::Completion) { + define_tools_for_replayed_calls(&mut body, wire_format); + } // Strip before `merge_extra_body` so a target can reinstate either field // deliberately via `extra_body`. if matches!(backend, Backend::Anthropic(_)) { @@ -1074,6 +1077,115 @@ fn ensure_responses_function_tool_description(object: &mut Map) { } } +/// Description of a tool definition that Switchyard adds only so that earlier calls to the +/// tool stay valid. The same request sets `tool_choice` to `none`. +const REPLAYED_TOOL_DESCRIPTION: &str = + "Earlier calls to this tool appear in the conversation. It is not available in this request."; + +// Adds a definition for each tool that the conversation history calls when the request +// defines no tools, and sets `tool_choice` to `none`. +// +// Codex compacts a long conversation by sending the whole history, including its tool calls +// and results, with an empty `tools` list. OpenAI accepts that. LiteLLM in front of Bedrock +// Claude rejects it on both OpenAI APIs ("Bedrock doesn't support tool calling without +// `tools=` param specified"), so compaction could never finish on such a target. A +// same-format request is replayed verbatim, so the fix has to apply to the outbound body. +// +// Bedrock has no `tool_choice` of `none`, and LiteLLM drops it. A Bedrock model can therefore +// still call one of these tools if the prompt asks it to. +fn define_tools_for_replayed_calls(body: &mut Value, wire_format: WireFormat) { + let Value::Object(object) = body else { + return; + }; + if object + .get("tools") + .and_then(Value::as_array) + .is_some_and(|tools| !tools.is_empty()) + { + return; + } + let tools = match wire_format { + WireFormat::OpenAiResponses => replayed_responses_tools(object.get("input")), + WireFormat::OpenAiChat => replayed_chat_tools(object.get("messages")), + WireFormat::AnthropicMessages => return, + }; + if tools.is_empty() { + return; + } + object.insert("tools".to_string(), Value::Array(tools)); + object.insert("tool_choice".to_string(), json!("none")); +} + +// One definition per tool name called in Responses `input`, in first-call order. A freeform +// `custom_tool_call` gets a `custom` definition and a `function_call` gets a `function` one. +fn replayed_responses_tools(input: Option<&Value>) -> Vec { + let Some(Value::Array(items)) = input else { + return Vec::new(); + }; + // A Responses-lite request defines its tools inside `input`. + if items + .iter() + .any(|item| item.get("type").and_then(Value::as_str) == Some("additional_tools")) + { + return Vec::new(); + } + let mut names = HashSet::new(); + let mut tools = Vec::new(); + for item in items { + let kind = match item.get("type").and_then(Value::as_str) { + Some("function_call") => "function", + Some("custom_tool_call") => "custom", + _ => continue, + }; + let Some(name) = item.get("name").and_then(Value::as_str) else { + continue; + }; + if !names.insert(name) { + continue; + } + tools.push(if kind == "function" { + json!({ + "type": "function", + "name": name, + "description": REPLAYED_TOOL_DESCRIPTION, + "parameters": {"type": "object"}, + }) + } else { + json!({"type": "custom", "name": name, "description": REPLAYED_TOOL_DESCRIPTION}) + }); + } + tools +} + +// One function definition per tool name called by assistant messages, in first-call order. +fn replayed_chat_tools(messages: Option<&Value>) -> Vec { + let Some(Value::Array(messages)) = messages else { + return Vec::new(); + }; + let mut names = HashSet::new(); + let mut tools = Vec::new(); + let calls = messages + .iter() + .filter_map(|message| message.get("tool_calls").and_then(Value::as_array)) + .flatten(); + for call in calls { + let Some(name) = call.pointer("/function/name").and_then(Value::as_str) else { + continue; + }; + if names.insert(name) { + tools.push(json!({ + "type": "function", + "function": { + "name": name, + "description": REPLAYED_TOOL_DESCRIPTION, + "parameters": {"type": "object"}, + }, + })); + } + } + tools +} + // Drops fields accepted by OpenAI-like APIs but rejected by Anthropic Messages. // // A router can serve earlier turns of a session from an OpenAI-format target and diff --git a/crates/switchyard-desktop-icons/Cargo.toml b/crates/switchyard-desktop-icons/Cargo.toml new file mode 100644 index 0000000000..13d9746b26 --- /dev/null +++ b/crates/switchyard-desktop-icons/Cargo.toml @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "switchyard-desktop-icons" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +publish = false + +[dependencies] +image = { version = "0.25", default-features = false, features = ["png"] } + +[dev-dependencies] +tempfile = "3" diff --git a/crates/switchyard-desktop-icons/src/lib.rs b/crates/switchyard-desktop-icons/src/lib.rs new file mode 100644 index 0000000000..950e183016 --- /dev/null +++ b/crates/switchyard-desktop-icons/src/lib.rs @@ -0,0 +1,159 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! This crate exports desktop icons from their shared PNG sources on any platform. + +use image::{ImageEncoder, RgbaImage, codecs::png::PngEncoder, imageops::FilterType}; +use std::{fs, path::Path}; + +type Result = std::result::Result>; + +fn read_source(path: &Path, size: u32) -> Result { + let image = image::open(path)?.into_rgba8(); + if image.dimensions() != (size, size) { + return Err(format!("{} must be {size} × {size} pixels", path.display()).into()); + } + Ok(image) +} + +fn png(image: &RgbaImage) -> Result> { + let mut bytes = Vec::new(); + PngEncoder::new(&mut bytes).write_image( + image.as_raw(), + image.width(), + image.height(), + image::ExtendedColorType::Rgba8, + )?; + Ok(bytes) +} + +fn representations(source: &RgbaImage) -> Result)>> { + [16, 32, 64, 128, 256, 512, 1024] + .into_iter() + .map(|size| { + let resized = image::imageops::resize(source, size, size, FilterType::Lanczos3); + Ok((size, png(&resized)?)) + }) + .collect() +} + +fn icns(images: &[(u32, Vec)]) -> Vec { + let mut bytes = b"icns\0\0\0\0".to_vec(); + // Retina entries name the logical size separately from their pixel size. + for (index, tag) in [ + (0, b"icp4"), + (1, b"icp5"), + (1, b"ic11"), + (2, b"ic12"), + (3, b"ic07"), + (4, b"ic08"), + (4, b"ic13"), + (5, b"ic09"), + (5, b"ic14"), + (6, b"ic10"), + ] { + let png = &images[index].1; + bytes.extend(tag); + bytes.extend(((png.len() + 8) as u32).to_be_bytes()); + bytes.extend(png); + } + let length = (bytes.len() as u32).to_be_bytes(); + bytes[4..8].copy_from_slice(&length); + bytes +} + +fn ico(images: &[(u32, Vec)]) -> Vec { + let images = &images[..5]; + let mut bytes = vec![0, 0, 1, 0, images.len() as u8, 0]; + let mut offset = 6 + 16 * images.len(); + for (size, png) in images { + bytes.extend([*size as u8, *size as u8, 0, 0]); + bytes.extend(1_u16.to_le_bytes()); + bytes.extend(32_u16.to_le_bytes()); + bytes.extend((png.len() as u32).to_le_bytes()); + bytes.extend((offset as u32).to_le_bytes()); + offset += png.len(); + } + for (_, png) in images { + bytes.extend(png); + } + bytes +} + +/// This function checks or writes the macOS and Windows icon files and the tray's decoded RGBA pixels. +/// A check preserves all files and reports the command needed to rebuild a stale export. +pub fn export(directory: &Path, check: bool) -> Result<()> { + let source = read_source(&directory.join("icon.png"), 1024)?; + let tray = read_source(&directory.join("tray.png"), 22)?; + let images = representations(&source)?; + for (name, expected) in [ + ("icon.icns", icns(&images)), + ("icon.ico", ico(&images)), + ( + "tray-windows.rgba", + image::imageops::resize(&source, 22, 22, FilterType::Lanczos3).into_raw(), + ), + ("tray.rgba", tray.into_raw()), + ] { + let path = directory.join(name); + if check { + if fs::read(&path).ok().as_deref() != Some(expected.as_slice()) { + return Err(format!("{} is stale; run cargo desktop-icons", path.display()).into()); + } + } else { + fs::write(path, expected)?; + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn exports_all_sizes_and_rejects_a_stale_windows_icon_without_writing() -> Result<()> { + let directory = tempfile::tempdir()?; + let source = Path::new(env!("CARGO_MANIFEST_DIR")).join("../switchyard-desktop/icons"); + for name in ["icon.png", "tray.png"] { + fs::copy(source.join(name), directory.path().join(name))?; + } + export(directory.path(), false)?; + export(directory.path(), true)?; + let ico = fs::read(directory.path().join("icon.ico"))?; + assert_eq!(u16::from_le_bytes([ico[4], ico[5]]), 5); + for (index, size) in [16, 32, 64, 128, 256].into_iter().enumerate() { + let entry = 6 + 16 * index; + let offset = u32::from_le_bytes(ico[entry + 12..entry + 16].try_into()?) as usize; + let length = u32::from_le_bytes(ico[entry + 8..entry + 12].try_into()?) as usize; + assert_eq!( + image::load_from_memory(&ico[offset..offset + length])?.width(), + size + ); + } + let icns = fs::read(directory.path().join("icon.icns"))?; + assert_eq!( + u32::from_be_bytes(icns[4..8].try_into()?) as usize, + icns.len() + ); + let mut offset = 8; + for size in [16, 32, 32, 64, 128, 256, 256, 512, 512, 1024] { + let length = u32::from_be_bytes(icns[offset + 4..offset + 8].try_into()?) as usize; + assert_eq!( + image::load_from_memory(&icns[offset + 8..offset + length])?.width(), + size + ); + offset += length; + } + assert_eq!(offset, icns.len()); + fs::write(directory.path().join("icon.ico"), b"stale")?; + assert!( + export(directory.path(), true) + .unwrap_err() + .to_string() + .contains("icon.ico is stale") + ); + assert_eq!(fs::read(directory.path().join("icon.ico"))?, b"stale"); + Ok(()) + } +} diff --git a/crates/switchyard-desktop-icons/src/main.rs b/crates/switchyard-desktop-icons/src/main.rs new file mode 100644 index 0000000000..67c19ab7e2 --- /dev/null +++ b/crates/switchyard-desktop-icons/src/main.rs @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +fn main() -> Result<(), Box> { + let check = match std::env::args().skip(1).collect::>().as_slice() { + [] => false, + [arg] if arg == "--check" => true, + _ => return Err("usage: cargo desktop-icons [--check]".into()), + }; + let directory = + std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../switchyard-desktop/icons"); + switchyard_desktop_icons::export(&directory, check)?; + println!( + "Desktop icons {}.", + if check { + "match their PNG sources" + } else { + "exported for macOS and Windows" + } + ); + Ok(()) +} diff --git a/crates/switchyard-desktop-install/Cargo.toml b/crates/switchyard-desktop-install/Cargo.toml new file mode 100644 index 0000000000..dac1b9b66c --- /dev/null +++ b/crates/switchyard-desktop-install/Cargo.toml @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "switchyard-desktop-install" +version.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +publish = false + +[dependencies] +switchyard-runner.workspace = true +serde.workspace = true +serde_json.workspace = true +toml = "0.9" +toml_edit = "0.25" +tempfile = "3" + +[target.'cfg(target_os = "macos")'.dependencies] +security-framework = "3" + +[target.'cfg(windows)'.dependencies] +mslnk = "0.1.8" +self-replace = "1.5" +windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Globalization", "Win32_System_Threading", "Win32_System_Diagnostics_ToolHelp", "Win32_Security_Credentials", "Win32_Security_Authorization", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging", "Win32_System_Console"] } + +[dev-dependencies] +plist = "1" diff --git a/crates/switchyard-desktop-install/prices.toml b/crates/switchyard-desktop-install/prices.toml new file mode 100644 index 0000000000..04af38ec8b --- /dev/null +++ b/crates/switchyard-desktop-install/prices.toml @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# The baseline model prices caller-facing calls as if routing had not changed models. +# Savings subtract actual calls, including classifier calls, from that estimate. +baseline_model = "gpt-5.6-sol" + +[prices."gpt-5.6-sol"] +input_per_mtok = 1.25 +cached_input_per_mtok = 0.125 +output_per_mtok = 10.0 + +[prices."gpt-5.6-luna"] +input_per_mtok = 0.25 +cached_input_per_mtok = 0.025 +output_per_mtok = 2.0 + +[prices."gpt-5.6-terra"] +input_per_mtok = 0.05 +cached_input_per_mtok = 0.005 +output_per_mtok = 0.4 + +# With a ChatGPT login, Codex sends "Approve for me" reviews to +# codex-auto-review even without Switchyard, so this price copies the +# baseline_model rates. Reviews then add the same amount to the actual cost and +# the baseline. The dollars saved stay the same, but the percentage drops a +# little. Update this price if you change baseline_model. +[prices."codex-auto-review"] +input_per_mtok = 1.25 +cached_input_per_mtok = 0.125 +output_per_mtok = 10.0 diff --git a/crates/switchyard-desktop-install/src/credentials.rs b/crates/switchyard-desktop-install/src/credentials.rs new file mode 100644 index 0000000000..95c867a110 --- /dev/null +++ b/crates/switchyard-desktop-install/src/credentials.rs @@ -0,0 +1,549 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Server credentials belong to one installed config and remain separate from model-list keys. + +use std::{path::Path, process::Command}; + +fn variables(config: &Path) -> Result, String> { + let text = std::fs::read_to_string(config) + .map_err(|_| "Could not read server credential references.")?; + switchyard_runner::api_key_variables(&text).map_err(|e| e.to_string()) +} + +pub(crate) fn needs_saved(config: &Path) -> Result { + Ok(variables(config)? + .iter() + .any(|name| std::env::var_os(name).is_none())) +} + +// Candidate files use the original config's scope; JSON keeps path/name pairs unambiguous. +fn account(scope: &Path, variable: &str) -> Result { + let scope = match std::fs::canonicalize(scope) { + Ok(scope) => scope, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + let parent = scope + .parent() + .ok_or("The server config must have a parent directory.")?; + std::fs::canonicalize(parent) + .map_err(|_| "Could not resolve the server config directory.")? + .join( + scope + .file_name() + .ok_or("The server config must have a file name.")?, + ) + } + Err(_) => return Err("Could not resolve the installed server config.".into()), + }; + let scope = scope + .to_str() + .ok_or("The server config path must be UTF-8.")?; + serde_json::to_string(&(scope, variable)).map_err(|e| e.to_string()) +} + +fn same_variable(name: &std::ffi::OsStr, variable: &str) -> bool { + #[cfg(windows)] + { + use std::os::windows::ffi::OsStrExt; + use windows_sys::Win32::Globalization::{CSTR_EQUAL, CompareStringOrdinal}; + let name: Vec<_> = name.encode_wide().collect(); + let variable: Vec<_> = variable.encode_utf16().collect(); + let (Ok(name_length), Ok(variable_length)) = + (name.len().try_into(), variable.len().try_into()) + else { + return false; + }; + unsafe { + CompareStringOrdinal( + name.as_ptr(), + name_length, + variable.as_ptr(), + variable_length, + 1, + ) == CSTR_EQUAL + } + } + #[cfg(not(windows))] + { + name == std::ffi::OsStr::new(variable) + } +} + +/// This function adds saved credentials only for absent variables. +/// Explicit values and removals remain authoritative, including empty or invalid values. +pub fn configure(command: &mut Command, config: &Path, scope: &Path) -> Result<(), String> { + resolve( + command, + config, + |name| std::env::var_os(name).is_some(), + |name| read(&account(scope, name)?), + ) +} + +/// The helper validates a candidate using credentials scoped to its original config. +/// It saves credentials only after the real server's dry-run succeeds. +pub fn validate(server: &Path, config: &Path, scope: &Path, save: bool) -> Result<(), String> { + let mut validator = Command::new(server); + configure(&mut validator, config, scope)?; + let output = validator + .arg("--config") + .arg(config) + .arg("--dry-run") + .output() + .map_err(|e| e.to_string())?; + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(if stderr.trim().is_empty() { + String::from_utf8_lossy(&output.stdout).into_owned() + } else { + stderr.into_owned() + }); + } + if save { + capture(config, scope)?; + } + Ok(()) +} + +fn resolve( + command: &mut Command, + config: &Path, + present: impl Fn(&str) -> bool, + saved: impl Fn(&str) -> Result, String>, +) -> Result<(), String> { + for variable in variables(config)? { + if !command + .get_envs() + .any(|(name, _)| same_variable(name, &variable)) + && !present(&variable) + && let Some(key) = saved(&variable)? + { + command.env(variable, key); + } + } + Ok(()) +} + +/// This function captures declared shell credentials after the real server accepts the config. +pub(crate) fn capture(config: &Path, scope: &Path) -> Result<(), String> { + #[cfg(any(target_os = "macos", windows))] + for variable in variables(config)? { + let account = account(scope, &variable)?; + let key = match std::env::var(&variable) { + Ok(key) => Some(key), + Err(std::env::VarError::NotPresent) => read(&account)?, + Err(std::env::VarError::NotUnicode(_)) => None, + }; + if let Some(key) = key.filter(|key| !key.trim().is_empty()) { + write(&account, &key)?; + } + } + #[cfg(not(any(target_os = "macos", windows)))] + let _ = (config, scope); + Ok(()) +} + +#[cfg(target_os = "macos")] +fn read(account: &str) -> Result, String> { + match security_framework::passwords::get_generic_password( + "Switchyard server environment", + account, + ) { + Ok(bytes) => String::from_utf8(bytes) + .map(Some) + .map_err(|_| "The saved server credential is not UTF-8.".into()), + Err(e) if e.code() == -25300 => Ok(None), + Err(_) => Err("Could not read the server credential from Keychain.".into()), + } +} + +#[cfg(target_os = "macos")] +fn write(account: &str, key: &str) -> Result<(), String> { + security_framework::passwords::set_generic_password( + "Switchyard server environment", + account, + key.as_bytes(), + ) + .map_err(|_| "Could not save the server credential to Keychain.".into()) +} + +#[cfg(windows)] +fn read(account: &str) -> Result, String> { + crate::windows::read_credential(&format!("Switchyard:server-env:{account}")) +} +#[cfg(windows)] +fn write(account: &str, key: &str) -> Result<(), String> { + crate::windows::write_credential(&format!("Switchyard:server-env:{account}"), key) +} +#[cfg(not(any(target_os = "macos", windows)))] +fn read(_account: &str) -> Result, String> { + Ok(None) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(any(target_os = "macos", windows))] + #[test] + #[ignore = "This test uses native credential storage and real binaries selected by SWITCHYARD_TEST_SERVER and SWITCHYARD_TEST_INSTALLER."] + fn saved_credential_reaches_real_validator_for_a_candidate_config() { + #[cfg(target_os = "macos")] + if let Some(config) = std::env::var_os("SWITCHYARD_TEST_BROKER_CONFIG") { + let output = Command::new(std::env::var_os("SWITCHYARD_TEST_BROKER").expect("broker")) + .arg("validate") + .arg("--server") + .arg(std::env::var_os("SWITCHYARD_TEST_SERVER").expect("server")) + .arg("--config") + .arg(&config) + .arg("--scope") + .arg(&config) + .output() + .expect("app broker validation"); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + return; + } + let dir = tempfile::tempdir().expect("fixture"); + let config = dir.path().join("composite.toml"); + let candidate = dir.path().join("candidate.toml"); + let source = r#" +schema_version=1 +[llm_clients.gateway] +format="openai_chat" +base_url="https://example.test/v1" +api_key_env="SWITCHYARD_SAVED_SERVER_FIXTURE_TOKEN" +[targets.answer] +id="fixture/model" +llm_client="gateway" +[routes.fixture] +id="fixture/route" +type="passthrough" +target="answer" +"#; + std::fs::write(&config, source).expect("config"); + std::fs::write(&candidate, source).expect("candidate"); + let account = account(&config, "SWITCHYARD_SAVED_SERVER_FIXTURE_TOKEN").expect("account"); + struct Cleanup(String); + impl Drop for Cleanup { + fn drop(&mut self) { + #[cfg(target_os = "macos")] + let _ = security_framework::passwords::delete_generic_password( + "Switchyard server environment", + &self.0, + ); + #[cfg(windows)] + { + let target: Vec<_> = format!("Switchyard:server-env:{}", self.0) + .encode_utf16() + .chain(Some(0)) + .collect(); + unsafe { + windows_sys::Win32::Security::Credentials::CredDeleteW( + target.as_ptr(), + windows_sys::Win32::Security::Credentials::CRED_TYPE_GENERIC, + 0, + ); + } + } + } + } + let _cleanup = Cleanup(account); + { + // The copied helper owns capture and reads; the signed macOS app has a different identity. + let broker = dir + .path() + .join(crate::executable_name("switchyard-desktop-install")); + std::fs::copy( + std::env::var_os("SWITCHYARD_TEST_INSTALLER").expect("installer"), + &broker, + ) + .expect("copied broker"); + let output = Command::new(&broker) + .arg("capture") + .arg("--server") + .arg(std::env::var_os("SWITCHYARD_TEST_SERVER").expect("server")) + .arg("--config") + .arg(&candidate) + .arg("--scope") + .arg(&config) + .env( + "SWITCHYARD_SAVED_SERVER_FIXTURE_TOKEN", + "dummy-local-fixture-key", + ) + .output() + .expect("capture"); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + } + let binary = std::env::var_os("SWITCHYARD_TEST_SERVER").expect("server binary"); + let mut validator = Command::new(&binary); + validator.arg("--config").arg(&candidate).arg("--dry-run"); + assert!( + !validator + .output() + .expect("without credential") + .status + .success() + ); + { + validator = Command::new( + dir.path() + .join(crate::executable_name("switchyard-desktop-install")), + ); + validator + .arg("validate") + .arg("--server") + .arg(&binary) + .arg("--config") + .arg(&candidate) + .arg("--scope") + .arg(&config); + } + assert!( + validator + .output() + .expect("with credential") + .status + .success() + ); + let mut invalid = Command::new( + dir.path() + .join(crate::executable_name("switchyard-desktop-install")), + ); + invalid + .arg("validate") + .arg("--server") + .arg(&binary) + .arg("--config") + .arg(&candidate) + .arg("--scope") + .arg(&config); + #[cfg(target_os = "macos")] + invalid.env("SWITCHYARD_SAVED_SERVER_FIXTURE_TOKEN", ""); + #[cfg(windows)] + invalid.env("switchyard_saved_server_fixture_token", ""); + // An explicitly empty command value also remains authoritative. + assert!(!invalid.output().expect("empty credential").status.success()); + #[cfg(target_os = "macos")] + { + // A separately signed app must delegate validation to the helper that saved the key. + let app = dir.path().join("Fixture.app"); + std::fs::create_dir_all(app.join("Contents/MacOS")).expect("bundle"); + std::fs::copy( + std::env::current_exe().expect("test executable"), + app.join("Contents/MacOS/fixture"), + ) + .expect("app executable"); + std::fs::write(app.join("Contents/Info.plist"), "CFBundleIdentifiercom.nvidia.switchyard.credential-fixtureCFBundleExecutablefixtureCFBundlePackageTypeAPPL").expect("bundle metadata"); + assert!( + Command::new("codesign") + .args(["--force", "--deep", "--sign", "-"]) + .arg(&app) + .status() + .expect("sign app") + .success() + ); + let output = Command::new(app.join("Contents/MacOS/fixture")) + .args(["--exact", "credentials::tests::saved_credential_reaches_real_validator_for_a_candidate_config", "--ignored", "--nocapture"]) + .env("SWITCHYARD_TEST_BROKER_CONFIG", &config) + .env("SWITCHYARD_TEST_BROKER", dir.path().join("switchyard-desktop-install")) + .env_remove("SWITCHYARD_SAVED_SERVER_FIXTURE_TOKEN").output().expect("signed app delegate"); + assert!( + output.status.success(), + "{}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + let defaults = crate::Settings::from_env().expect("defaults"); + let port = std::net::TcpListener::bind("127.0.0.1:0") + .expect("port") + .local_addr() + .expect("address") + .port(); + let settings = crate::Settings { + home: dir.path().join("home"), + sy_home: dir.path().into(), + codex_home: dir.path().join("codex"), + service_dir: dir.path().join("services"), + port, + ..defaults + }; + std::fs::create_dir_all(settings.bin_dir()).expect("installed binaries"); + std::fs::copy( + dir.path().join("switchyard-desktop-install"), + settings.binary("switchyard-desktop-install"), + ) + .expect("installed installer"); + std::fs::copy( + std::env::var_os("SWITCHYARD_TEST_SERVER").expect("server binary"), + settings.binary("switchyard-server"), + ) + .expect("installed server"); + let metadata = dir.path().join("install.toml"); + std::fs::write(&metadata, toml::to_string(&settings).expect("metadata")) + .expect("metadata file"); + // Drop stops and reaps the real server even if readiness assertions fail. + struct Server(std::process::Child); + impl Drop for Server { + fn drop(&mut self) { + let _ = self.0.kill(); + let _ = self.0.wait(); + } + } + let _server = Server( + Command::new(settings.binary("switchyard-desktop-install")) + .args(["serve", "--settings"]) + .arg(metadata) + .env_remove("SWITCHYARD_SAVED_SERVER_FIXTURE_TOKEN") + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .spawn() + .expect("installed serve"), + ); + assert!(crate::wait_for_health(port), "installed server readiness"); + } + } + + #[test] + fn config_drives_llm_and_decision_credentials_without_overriding_explicit_environment() { + let dir = tempfile::tempdir().expect("fixture"); + let config = dir.path().join("candidate.toml"); + let source = r#" +schema_version=1 +[llm_clients.gateway] +format="openai_chat" +base_url="https://example.test/v1" +api_key_env="CUSTOM_GATEWAY_TOKEN" +[decision_clients.judge] +format="system_one" +endpoint="https://example.test/decision" +api_key_env="CUSTOM_JUDGE_TOKEN" +timeout_ms=1000 +[targets] +[routes] +"#; + std::fs::write(&config, source).expect("config"); + let mut command = Command::new("unused"); + resolve( + &mut command, + &config, + |name| name == "CUSTOM_GATEWAY_TOKEN", + |name| { + assert_eq!(name, "CUSTOM_JUDGE_TOKEN"); + Ok(Some("fixture-token".into())) + }, + ) + .expect("resolved"); + assert_eq!( + command.get_envs().collect::>(), + vec![( + std::ffi::OsStr::new("CUSTOM_JUDGE_TOKEN"), + Some(std::ffi::OsStr::new("fixture-token")) + )] + ); + let mut command = Command::new("unused"); + assert!( + resolve( + &mut command, + &config, + |_| false, + |_| Err("store unavailable".into()) + ) + .is_err() + ); + assert_eq!(command.get_envs().count(), 0); + for name in ["A=B", "A\0B", " "] { + let source = format!( + "schema_version=1\n[llm_clients.gateway]\nformat=\"openai_chat\"\nbase_url=\"https://example.test/v1\"\napi_key_env={}\n[targets]\n[routes]", + serde_json::to_string(name).unwrap() + ); + std::fs::write(&config, source).expect("invalid reference fixture"); + assert!( + resolve( + &mut command, + &config, + |_| false, + |_| panic!("invalid name must not read credentials") + ) + .is_err() + ); + } + std::fs::write(&config, source).expect("restore config"); + #[cfg(windows)] + { + // Windows treats case variants as the same variable, including explicit removals and empty values. + let mut explicit = Command::new("unused"); + explicit + .env("custom_gateway_token", "") + .env_remove("custom_judge_token"); + resolve( + &mut explicit, + &config, + |_| false, + |_| panic!("explicit environment must not read credentials"), + ) + .expect("explicit environment"); + assert!(same_variable( + std::ffi::OsStr::new("custom_é_token"), + "CUSTOM_É_TOKEN" + )); + } + std::fs::write(&config, "schema_version=1\nunknown=1\n[targets]\n[routes]") + .expect("malformed config"); + assert!( + resolve( + &mut command, + &config, + |_| false, + |_| panic!("invalid config must not read credentials") + ) + .is_err() + ); + assert_eq!(command.get_envs().count(), 0); + } + + #[test] + fn candidate_uses_installed_config_scope_and_distinct_configs_have_distinct_accounts() { + let dir = tempfile::tempdir().expect("fixture"); + let first = dir.path().join("first.toml"); + let second = dir.path().join("second.toml"); + std::fs::write(&first, "").expect("first"); + std::fs::write(&second, "").expect("second"); + let future = dir.path().join("future.toml"); + let before = account(&future, "CUSTOM_TOKEN").expect("future scope"); + std::fs::write(&future, "").expect("future config"); + assert_eq!( + before, + account(&future, "CUSTOM_TOKEN").expect("installed scope") + ); + assert_ne!( + account(&first, "CUSTOM_TOKEN").expect("first"), + account(&second, "CUSTOM_TOKEN").expect("second") + ); + #[cfg(unix)] + { + let colon = dir.path().join("first.toml:extra"); + std::fs::write(&colon, "").expect("colon scope"); + assert_ne!( + account(&colon, "TOKEN").expect("colon scope"), + account(&first, "extra:TOKEN").expect("colon variable") + ); + } + #[cfg(unix)] + { + let link = dir.path().join("link.toml"); + std::os::unix::fs::symlink(&first, &link).expect("link"); + assert_eq!( + account(&first, "CUSTOM_TOKEN").expect("first"), + account(&link, "CUSTOM_TOKEN").expect("link") + ); + } + } +} diff --git a/crates/switchyard-desktop-install/src/lib.rs b/crates/switchyard-desktop-install/src/lib.rs new file mode 100644 index 0000000000..f06b401934 --- /dev/null +++ b/crates/switchyard-desktop-install/src/lib.rs @@ -0,0 +1,1306 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! The source installer owns app bundles, service definitions, and installation metadata. + +use serde::{Deserialize, Serialize}; +use std::{ + fs, + io::{Read, Write}, + net::{SocketAddr, TcpStream}, + path::{Path, PathBuf}, + process::{Command, Stdio}, + thread, + time::{Duration, Instant, SystemTime, UNIX_EPOCH}, +}; + +pub mod credentials; +#[cfg(windows)] +pub mod windows; + +pub fn executable_name(name: &str) -> String { + format!("{name}{}", std::env::consts::EXE_SUFFIX) +} + +const SERVER: &str = "com.nvidia.switchyard.server"; +const DESKTOP: &str = "com.nvidia.switchyard.desktop"; +// Migration removes this LaunchAgent so the old app cannot restart alongside the desktop app. +const LEGACY_DESKTOP: &str = "com.nvidia.switchyard.menubar"; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Settings { + pub home: PathBuf, + pub sy_home: PathBuf, + pub codex_home: PathBuf, + pub service_dir: PathBuf, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub app_dir: Option, + pub source: PathBuf, + pub cargo: PathBuf, + pub port: u16, + pub profile: String, + pub model: String, +} + +impl Settings { + pub fn for_uninstall() -> Result { + let defaults = Self::from_env()?; + let recorded = defaults.home.join(".switchyard-desktop-install.toml"); + let selected = if recorded.try_exists().map_err(|e| e.to_string())? { + Self::load(&recorded)? + } else { + return Ok(defaults); + }; + for (name, value, saved) in [ + ( + "SY_HOME", + defaults.sy_home.as_os_str(), + selected.sy_home.as_os_str(), + ), + ( + "CODEX_HOME", + defaults.codex_home.as_os_str(), + selected.codex_home.as_os_str(), + ), + ( + "SY_PROFILE", + std::ffi::OsStr::new(&defaults.profile), + std::ffi::OsStr::new(&selected.profile), + ), + ] { + if std::env::var_os(name).is_some() && value != saved { + return Err(format!( + "{name} differs from the recorded installation at {}. Use uninstall --settings FILE to choose a different installation.", + recorded.display() + )); + } + } + Ok(selected) + } + pub fn from_env() -> Result { + let home = std::env::var_os("HOME") + .or_else(|| std::env::var_os("USERPROFILE")) + .map(PathBuf::from) + .ok_or("Could not find your home directory.")?; + let path = |name, default| std::env::var_os(name).map(PathBuf::from).unwrap_or(default); + let config_home = path("XDG_CONFIG_HOME", home.join(".config")); + let value = |name: &str, default: &str| { + std::env::var(name).or_else(|error| match error { + std::env::VarError::NotPresent => Ok(default.to_owned()), + std::env::VarError::NotUnicode(_) => Err(format!("{name} must be UTF-8.")), + }) + }; + let port = value("SY_PORT", "4123")?; + if !port.bytes().all(|b| b.is_ascii_digit()) { + return Err("SY_PORT must be a number from 1 to 65535.".into()); + } + let settings = Self { + sy_home: path("SY_HOME", home.join(".switchyard")), + codex_home: path("CODEX_HOME", home.join(".codex")), + service_dir: if cfg!(target_os = "macos") { + home.join("Library/LaunchAgents") + } else if cfg!(windows) { + path("APPDATA", home.join("AppData/Roaming")) + .join("Microsoft/Windows/Start Menu/Programs") + } else { + config_home.join("systemd/user") + }, + app_dir: if cfg!(windows) { + Some(path("LOCALAPPDATA", home.join("AppData/Local")).join("Programs/Switchyard")) + } else { + None + }, + home, + source: Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(Path::parent) + .ok_or("Could not find the source checkout.")? + .to_path_buf(), + cargo: PathBuf::from(env!("CARGO")), + port: port + .parse() + .map_err(|_| "SY_PORT must be a number from 1 to 65535.")?, + profile: value("SY_PROFILE", "sy")?, + model: value("SY_MODEL", "composite-gpt-6-sol-gpt-6-luna")?, + }; + settings.validate()?; + Ok(settings) + } + + pub fn load(file: &Path) -> Result { + let text = fs::read_to_string(file).map_err(|e| format!("Read {}: {e}", file.display()))?; + let settings: Self = + toml::from_str(&text).map_err(|e| format!("Parse {}: {e}", file.display()))?; + settings.validate()?; + Ok(settings) + } + + pub fn validate(&self) -> Result<(), String> { + if self.profile.is_empty() + || self.profile.len() > 128 + || !self.profile.as_bytes()[0].is_ascii_alphanumeric() + || !self + .profile + .bytes() + .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-') + { + return Err("SY_PROFILE must start with an ASCII letter or digit and contain only ASCII letters, digits, underscores, or hyphens, up to 128 characters.".into()); + } + if self.model.is_empty() || self.model.bytes().any(|b| b.is_ascii_control()) { + return Err( + "SY_MODEL must be a nonempty public route ID without ASCII control characters." + .into(), + ); + } + if self.port == 0 { + return Err("SY_PORT must be a number from 1 to 65535.".into()); + } + for (name, path) in [ + ("HOME", &self.home), + ("SY_HOME", &self.sy_home), + ("CODEX_HOME", &self.codex_home), + ("service directory", &self.service_dir), + ("source checkout", &self.source), + ("Cargo executable", &self.cargo), + ] + .into_iter() + .chain(self.app_dir.as_ref().map(|path| ("LOCALAPPDATA", path))) + { + if !path.is_absolute() + || path + .to_str() + .is_none_or(|s| s.bytes().any(|b| b.is_ascii_control())) + { + return Err(format!( + "{name} must be an absolute UTF-8 path without ASCII control characters." + )); + } + } + Ok(()) + } + + pub fn app(&self) -> PathBuf { + self.home.join("Applications/Switchyard.app") + } + pub fn bin_dir(&self) -> PathBuf { + if cfg!(windows) { + self.app_dir + .clone() + .unwrap_or_else(|| self.home.join("AppData/Local/Programs/Switchyard")) + } else { + self.sy_home.join("bin") + } + } + pub fn binary(&self, name: &str) -> PathBuf { + self.bin_dir().join(executable_name(name)) + } + pub fn desktop_settings(&self) -> PathBuf { + self.sy_home.join("desktop.toml") + } + fn profile_file(&self) -> PathBuf { + self.codex_home + .join(format!("{}.config.toml", self.profile)) + } + fn release(&self, binary: &str) -> PathBuf { + self.source + .join("target/release") + .join(executable_name(binary)) + } +} + +/// The app reads this metadata to retain a custom SY_HOME when Finder or a Windows shortcut starts it. +pub fn bundle_metadata(executable: &Path) -> PathBuf { + if cfg!(windows) { + return executable + .parent() + .unwrap_or(Path::new(".")) + .join("install.toml"); + } + executable + .parent() + .and_then(Path::parent) + .unwrap_or(Path::new(".")) + .join("Resources/install.toml") +} + +/// The updater runs separately because installation restarts the app that requested it. +pub fn start_update(settings_file: &Path) -> Result { + let settings = Settings::load(settings_file)?; + let guard = update_lock(&settings)?; + let status = update_status(settings_file)?; + if update_starting(&status) { + return Err( + "A Switchyard update is already starting. View its progress in Settings.".into(), + ); + } + set_update_state( + &settings, + "starting", + "start", + "Starting the source update.", + )?; + let log_path = settings.sy_home.join("logs/update.log"); + let log = fs::OpenOptions::new() + .create(true) + .append(true) + .open(&log_path) + .map_err(|e| e.to_string())?; + let error_log = log.try_clone().map_err(|e| e.to_string())?; + #[cfg(windows)] + let updater_path = windows::temporary_updater(&settings.binary("switchyard-desktop-install"))?; + #[cfg(not(windows))] + let updater_path = settings.binary("switchyard-desktop-install"); + let mut updater = Command::new(&updater_path); + #[cfg(windows)] + windows::hide_console(&mut updater); + #[cfg(unix)] + { + use std::os::unix::process::CommandExt; + // A separate process group keeps launchd from stopping the updater with the app. + updater.process_group(0); + } + // The child must acquire the update lock, so the parent releases it before spawning. + drop(guard); + let mut child = updater + .args(["update", "--settings"]) + .arg(settings_file) + .stdin(Stdio::null()) + .stdout(log) + .stderr(error_log) + .spawn() + .map_err(|e| { + let message = format!("Could not start the source update: {e}"); + let _ = set_update_state(&settings, "failed", "start", &message); + message + })?; + #[cfg(windows)] + updater_path.keep().map_err(|e| e.to_string())?; + // This thread reaps the updater if it exits before installation stops the app. + thread::spawn(move || { + let _ = child.wait(); + }); + Ok(log_path) +} + +pub fn execute(settings: &Settings, action: &str, dry_run: bool) -> Result<(), String> { + settings.validate()?; + if !matches!(action, "install" | "uninstall" | "update") { + return Err("Choose install, update, or uninstall.".into()); + } + if !cfg!(any(target_os = "macos", target_os = "linux", windows)) { + return Err("Installation supports macOS, Linux, and Windows.".into()); + } + if dry_run { + println!("{}", installation_plan(settings, action)?); + return Ok(()); + } + if action == "uninstall" { + uninstall(settings) + } else { + let _job = update_lock(settings)?; + if action == "update" { + set_update_state(settings, "running", "build", "Building the app and server.")?; + } + let result = install(settings, action == "update"); + match &result { + Ok(ready) => set_update_state( + settings, + if *ready { "completed" } else { "warning" }, + if *ready { "completed" } else { "readiness" }, + if *ready { + "Installed the app and server. Switchyard is ready." + } else { + "Installed the app and server, but Switchyard did not answer /health. Open the server log and retry its restart." + }, + )?, + Err(error) => { + if settings.sy_home.exists() { + let stage = update_status(&settings.sy_home.join("install.toml")) + .ok() + .and_then(|s| s["stage"].as_str().map(str::to_owned)) + .unwrap_or_else(|| "installation".into()); + let _ = set_update_state(settings, "failed", &stage, error); + } + } + } + result.map(|_| ()) + } +} + +fn update_lock(settings: &Settings) -> Result { + let file = fs::OpenOptions::new() + .create(true) + .truncate(false) + .read(true) + .write(true) + .open(settings.home.join(".switchyard-desktop-update.lock")) + .map_err(|e| e.to_string())?; + file.try_lock().map_err(|_| { + "A Switchyard update is already running. Open Settings to view its progress.".to_string() + })?; + Ok(file) +} +fn set_update_state( + settings: &Settings, + state: &str, + stage: &str, + message: &str, +) -> Result<(), String> { + let record = serde_json::json!({"state":state,"stage":stage,"message":message,"source":settings.source,"updated_at":SystemTime::now().duration_since(UNIX_EPOCH).map_err(|e|e.to_string())?.as_secs()}); + write( + &settings.sy_home.join("update-status.toml"), + toml::to_string(&record) + .map_err(|e| e.to_string())? + .as_bytes(), + false, + ) +} +// A marker under 10 seconds old marks the child as starting before it acquires the lock. +fn update_starting(record: &serde_json::Value) -> bool { + record["state"] == "starting" + && record["updated_at"].as_u64().is_some_and(|stamp| { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .is_ok_and(|now| now.as_secs().saturating_sub(stamp) < 10) + }) +} +pub fn update_status(settings_file: &Path) -> Result { + let settings = Settings::load(settings_file)?; + let path = settings.sy_home.join("update-status.toml"); + let text = match fs::read_to_string(&path) { + Ok(text) => text, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + return Ok(serde_json::json!({"state":"idle"})); + } + Err(error) => return Err(error.to_string()), + }; + let value: toml::Value = toml::from_str(&text).map_err(|e| e.to_string())?; + let mut record = serde_json::to_value(value).map_err(|e| e.to_string())?; + let starting = update_starting(&record); + if !starting + && matches!(record["state"].as_str(), Some("starting" | "running")) + && let Ok(lock) = fs::OpenOptions::new() + .read(true) + .write(true) + .open(settings.home.join(".switchyard-desktop-update.lock")) + && lock.try_lock().is_ok() + { + record["state"] = serde_json::json!("interrupted"); + record["message"] = serde_json::json!( + "The update stopped before recording completion. Open the update log, then retry." + ); + } + Ok(record) +} +pub fn preview_update(settings_file: &Path) -> Result { + let settings = Settings::load(settings_file)?; + let git = |args: &[&str]| -> String { + let mut command = Command::new("git"); + #[cfg(windows)] + windows::hide_console(&mut command); + command + .arg("-C") + .arg(&settings.source) + .args(args) + .output() + .ok() + .filter(|o| o.status.success()) + .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()) + .unwrap_or_else(|| "Unavailable".into()) + }; + Ok( + serde_json::json!({"source":settings.source,"revision":git(&["rev-parse","HEAD"]),"branch":git(&["branch","--show-current"]),"changes":git(&["status","--short"]),"plan":installation_plan(&settings,"update")?,"status":update_status(settings_file)?}), + ) +} +fn installation_plan(s: &Settings, action: &str) -> Result { + let removing = action == "uninstall"; + let mut rows = vec![ + format!( + "{} {} and local server from {}", + if removing { + "Remove" + } else { + "Build and install" + }, + if cfg!(target_os = "macos") { + "macOS desktop app" + } else { + if cfg!(windows) { + "Windows desktop app" + } else { + "Linux terminal app" + } + }, + s.source.display() + ), + format!("Route: {} · Server: http://127.0.0.1:{}", s.model, s.port), + format!( + "{} service definitions in {}. The server {}.", + if removing { "Remove" } else { "Replace" }, + s.service_dir.display(), + if removing { + "will stop" + } else { + "will restart after build and validation" + } + ), + ]; + for (path, label) in [ + (s.desktop_settings(), "app settings"), + (s.sy_home.join("composite.toml"), "server config"), + (s.sy_home.join("routing.jsonl"), "usage history"), + ] { + rows.push(format!( + "{} {label}: {}", + if removing || path.exists() { + "Preserve" + } else { + "Create" + }, + path.display() + )); + } + rows.push(format!( + "{} Codex profile: {}", + if removing { + "Preserve recovery copy, then remove" + } else if action == "update" && s.profile_file().exists() { + "Preserve" + } else if s.profile_file().exists() { + "Back up, then update" + } else { + "Create" + }, + s.profile_file().display() + )); + rows.push(format!( + "{} binaries: {}", + if !removing { + "Replace" + } else if cfg!(windows) { + "Remove" + } else { + "Preserve" + }, + s.bin_dir().display() + )); + rows.push(format!( + "Preserve accounts and logs: {}", + s.sy_home.display() + )); + if cfg!(target_os = "macos") { + rows.push(format!( + "{} app bundle: {}", + if removing { "Remove" } else { "Replace" }, + s.app().display() + )); + } + rows.push("Preview changes no files. Build prerequisites, full server config validation, and readiness are checked during installation.".into()); + if removing { + let mut destinations = vec![ + s.codex_home.join("sy.config.toml"), + s.codex_home.join("config.toml"), + s.home.join(".claude/settings.json"), + s.home.join(".pi/agent/models.json"), + s.home.join(".pi/agent/settings.json"), + ]; + for tool in ["codex", "claude"] { + let root = s.home.join(".switchyard/accounts").join(tool); + if fs::symlink_metadata(&root).is_ok_and(|m| m.is_dir() && !m.file_type().is_symlink()) + && let Ok(accounts) = fs::read_dir(&root) + { + for account in accounts.flatten() { + if account.file_type().is_ok_and(|t| t.is_dir()) { + destinations.push(account.path().join(if tool == "codex" { + "config.toml" + } else { + "settings.json" + })); + } + } + } + } + for destination in destinations { + let backup = PathBuf::from(format!("{}.switchyard-original", destination.display())); + let missing = PathBuf::from(format!( + "{}.switchyard-original-missing", + destination.display() + )); + if backup.exists() || missing.exists() { + rows.push(format!("Preserve coding-tool destination with a Switchyard backup: {}. Restore it in Connect tools before removal.",destination.display())); + } + } + for path in [ + s.codex_home.join("config.sy.toml"), + s.codex_home.join("config.toml.direct"), + s.home.join(".zshrc"), + s.home.join(".bashrc"), + ] { + if path.exists() { + rows.push(format!("Review legacy Switchyard cleanup: {}. Active defaults are copied before restoring config.toml.direct; marked shell/profile blocks are removed.",path.display())); + } + } + rows.push("Custom destinations outside these known locations are not inventoried.".into()); + rows.push("Restore routing from Connect tools before uninstalling. Custom and account tool settings are preserved and may still point at this server.".into()); + } + Ok(rows.join("\n")) +} + +fn wait_for_health(port: u16) -> bool { + let deadline = Instant::now() + Duration::from_secs(10); + let address: SocketAddr = ([127, 0, 0, 1], port).into(); + while Instant::now() < deadline { + if let Ok(mut stream) = TcpStream::connect_timeout(&address, Duration::from_millis(250)) { + let _ = stream.set_read_timeout(Some(Duration::from_millis(300))); + let _ = stream.set_write_timeout(Some(Duration::from_millis(300))); + if write!( + stream, + "GET /health HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\nConnection: close\r\n\r\n" + ) + .is_ok() + { + let mut response = String::new(); + if stream.take(8192).read_to_string(&mut response).is_ok() + && let Some((headers, body)) = response.split_once("\r\n\r\n") + && headers + .lines() + .next() + .and_then(|line| line.split_whitespace().nth(1)) + == Some("200") + && serde_json::from_str::(body) + .is_ok_and(|v| v["status"] == "ok") + { + return true; + } + } + } + thread::sleep(Duration::from_millis(200)); + } + false +} + +fn run(command: &mut Command) -> Result<(), String> { + #[cfg(windows)] + windows::hide_console(command); + let invocation = format!( + "{:?} {:?}", + command.get_program(), + command.get_args().collect::>() + ); + println!("Running {invocation}"); + let status = command + .status() + .map_err(|e| format!("Could not run {invocation}: {e}"))?; + if status.success() { + Ok(()) + } else { + Err(format!("{invocation} exited with {status}")) + } +} + +// Atomic replacement keeps existing executables readable while an update is staged. +fn write(path: &Path, bytes: &[u8], executable: bool) -> Result<(), String> { + let parent = path + .parent() + .ok_or("The destination has no parent directory.")?; + fs::create_dir_all(parent).map_err(|e| e.to_string())?; + let mut temp = tempfile::NamedTempFile::new_in(parent).map_err(|e| e.to_string())?; + temp.write_all(bytes).map_err(|e| e.to_string())?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + temp.as_file() + .set_permissions(fs::Permissions::from_mode(if executable { + 0o755 + } else { + 0o600 + })) + .map_err(|e| e.to_string())?; + } + #[cfg(not(unix))] + let _ = executable; + temp.as_file().sync_all().map_err(|e| e.to_string())?; + temp.persist(path) + .map_err(|e| format!("Write {}: {e}", path.display()))?; + Ok(()) +} + +fn write_once(path: &Path, text: &str) -> Result<(), String> { + if path.try_exists().map_err(|e| e.to_string())? { + return Ok(()); + } + write(path, text.as_bytes(), false) +} + +fn backup(path: &Path, label: &str) -> Result<(), String> { + let mut copy = tempfile::Builder::new() + .prefix(&format!( + "{}.{label}.", + path.file_name() + .ok_or("Missing file name.")? + .to_string_lossy() + )) + .tempfile_in(path.parent().ok_or("Missing parent directory.")?) + .map_err(|e| e.to_string())?; + copy.write_all(&fs::read(path).map_err(|e| e.to_string())?) + .map_err(|e| e.to_string())?; + copy.as_file().sync_all().map_err(|e| e.to_string())?; + let (_, saved) = copy.keep().map_err(|e| e.to_string())?; + println!("Preserved {} at {}", path.display(), saved.display()); + Ok(()) +} + +fn remove(path: &Path) -> Result<(), String> { + match fs::remove_file(path) { + Ok(()) => Ok(()), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()), + Err(e) => Err(format!("Remove {}: {e}", path.display())), + } +} + +fn install(s: &Settings, preserve_profile: bool) -> Result { + let profile = s.profile_file(); + let profile_text = if profile.exists() && !preserve_profile { + merge_codex_profile(s, &fs::read_to_string(&profile).map_err(|e| e.to_string())?)? + } else { + codex_profile(s) + }; + if cfg!(target_os = "macos") { + run(Command::new("launchctl").arg("help"))?; + } else if cfg!(target_os = "linux") { + run(Command::new("systemctl") + .args(["--user", "show-environment"]) + .stdout(Stdio::null()))?; + } + let reconciled = reconcile_desktop_port(s)?; + println!( + "Building the desktop app and server from {}", + s.source.display() + ); + let mut build = Command::new(&s.cargo); + build + .current_dir(&s.source) + .args(["build", "--locked", "--release", "--manifest-path"]) + .arg(s.source.join("Cargo.toml")) + .arg("--target-dir") + .arg(s.source.join("target")) + .args([ + "-p", + "switchyard-server", + "-p", + "switchyard-desktop", + "-p", + "switchyard-desktop-install", + ]); + // Cargo and rustc must come from the same toolchain, including GUI-triggered updates. + let cargo_dir = s.cargo.parent().ok_or("Cargo has no parent directory.")?; + let mut paths = vec![cargo_dir.to_path_buf()]; + if let Some(path) = std::env::var_os("PATH") { + paths.extend(std::env::split_paths(&path)); + } + build.env( + "PATH", + std::env::join_paths(paths).map_err(|e| e.to_string())?, + ); + run(&mut build)?; + let config = s.sy_home.join("composite.toml"); + let existing = config.try_exists().map_err(|e| e.to_string())?; + let staged = tempfile::NamedTempFile::new().map_err(|e| e.to_string())?; + if !existing { + fs::write( + staged.path(), + include_str!("../../../scripts/config/composite.toml"), + ) + .map_err(|e| e.to_string())?; + } + let checked_config = if existing { + config.as_path() + } else { + staged.path() + }; + // On macOS, the installed helper can read its Keychain entries while a rebuilt helper has a new signature. + if existing + && credentials::needs_saved(checked_config)? + && s.binary("switchyard-desktop-install") + .try_exists() + .map_err(|e| e.to_string())? + { + run(Command::new(s.binary("switchyard-desktop-install")) + .arg("validate") + .arg("--server") + .arg(s.release("switchyard-server")) + .arg("--config") + .arg(checked_config) + .arg("--scope") + .arg(&config))?; + } else { + run(Command::new(s.release("switchyard-server")) + .arg("--config") + .arg(checked_config) + .arg("--dry-run"))?; + } + if !preserve_profile || !s.profile_file().exists() { + check_public_route(if existing { &config } else { staged.path() }, &s.model)?; + } + let _lock = lock_installation(s)?; + set_update_state( + s, + "running", + "install", + "Build and config validation passed. Replacing the installation.", + )?; + let metadata = toml::to_string(s).map_err(|e| e.to_string())?; + let bundle = if cfg!(target_os = "macos") { + Some(stage_bundle(s, &metadata)?) + } else { + None + }; + fs::create_dir_all(&s.sy_home).map_err(|e| e.to_string())?; + // The release helper saves credentials under the identity used by later validation and startup. + let mut broker = Command::new(s.release("switchyard-desktop-install")); + run(broker + .arg("capture") + .arg("--server") + .arg(s.release("switchyard-server")) + .arg("--config") + .arg(checked_config) + .arg("--scope") + .arg(&config))?; + set_update_state( + s, + "running", + "services", + "Stopping services and replacing app files. Existing config and history are retained.", + )?; + stop_services(s)?; + fs::create_dir_all(s.sy_home.join("logs")).map_err(|e| e.to_string())?; + #[cfg(windows)] + windows::replace_binaries(s)?; + #[cfg(not(windows))] + for binary in [ + "switchyard-server", + "switchyard-desktop", + "switchyard-desktop-install", + ] { + write( + &s.binary(binary), + &fs::read(s.release(binary)).map_err(|e| e.to_string())?, + true, + )?; + } + write_once( + &config, + include_str!("../../../scripts/config/composite.toml"), + )?; + let legacy = s.sy_home.join("menubar.toml"); + if legacy.try_exists().map_err(|e| e.to_string())? + && !s + .desktop_settings() + .try_exists() + .map_err(|e| e.to_string())? + { + fs::rename(&legacy, s.desktop_settings()).map_err(|e| e.to_string())?; + } + write_once(&s.desktop_settings(), &desktop_config(s))?; + if let Some(text) = reconciled { + write(&s.desktop_settings(), text.as_bytes(), false)?; + } + remove(&s.sy_home.join("bin/switchyard-menubar"))?; + write(&s.sy_home.join("install.toml"), metadata.as_bytes(), false)?; + write( + &s.home.join(".switchyard-desktop-install.toml"), + metadata.as_bytes(), + false, + )?; + if let Some(bundle) = bundle { + install_macos(s, bundle)?; + } else if cfg!(target_os = "linux") { + install_linux(s)?; + } else { + #[cfg(windows)] + windows::install(s)?; + } + if !preserve_profile || !profile.try_exists().map_err(|e| e.to_string())? { + set_update_state( + s, + "running", + "profile", + "Saving the selected Codex profile.", + )?; + if profile.try_exists().map_err(|e| e.to_string())? + && fs::read(&profile).map_err(|e| e.to_string())? != profile_text.as_bytes() + { + backup(&profile, "switchyard-backup")?; + } + write(&profile, profile_text.as_bytes(), false)?; + } + println!( + "Installed. Server: http://127.0.0.1:{}\nSettings: {}\nUse: codex -p {}", + s.port, + s.desktop_settings().display(), + s.profile + ); + set_update_state( + s, + "running", + "readiness", + "Installed. Waiting for the server to answer /health.", + )?; + let ready = wait_for_health(s.port); + if !ready { + println!( + "Warning: installed, but the server did not become ready. Check logs at {} and restart the server.", + s.sy_home.join("logs").display() + ); + } + Ok(ready) +} + +fn check_public_route(config: &Path, model: &str) -> Result<(), String> { + let document: toml::Value = + toml::from_str(&fs::read_to_string(config).map_err(|e| e.to_string())?) + .map_err(|e| e.to_string())?; + let ids = document + .get("routes") + .and_then(toml::Value::as_table) + .map(|routes| { + routes + .values() + .filter_map(|route| route.get("id").and_then(toml::Value::as_str)) + .collect::>() + }) + .unwrap_or_default(); + if ids.contains(&model) { + Ok(()) + } else { + Err(format!( + "Route {model:?} is not in {}. Choose SY_MODEL from: {}. No services or coding-tool settings were changed.", + config.display(), + ids.join(", ") + )) + } +} + +fn reconcile_desktop_port(s: &Settings) -> Result, String> { + let path = if s.desktop_settings().exists() { + s.desktop_settings() + } else { + s.sy_home.join("menubar.toml") + }; + let text = match fs::read_to_string(&path) { + Ok(text) => text, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None), + Err(error) => return Err(format!("Read {}: {error}", path.display())), + }; + let mut document: toml_edit::DocumentMut = text.parse().map_err(|e| { + format!( + "Parse {}: {e}. Fix this file before installing.", + path.display() + ) + })?; + let previous = s.sy_home.join("install.toml"); + let old_port = if previous.exists() { + Settings::load(&previous)?.port + } else { + 4123 + }; + let expected = format!("http://127.0.0.1:{old_port}"); + let current = document + .get("server_url") + .map(|value| { + value + .as_str() + .ok_or_else(|| std::io::Error::from(std::io::ErrorKind::InvalidData)) + }) + .transpose() + .map_err(|e| { + format!( + "The server_url value in {} must be a string: {e}. Fix this file before installing.", + path.display() + ) + })?; + if s.port != old_port && current.is_some_and(|url| url != expected) { + return Err(format!( + "{} uses a custom server_url, but SY_PORT changes from {old_port} to {}. Review the app URL and port together before reinstalling. No services were stopped.", + path.display(), + s.port + )); + } + // Unchanged and custom URLs must retain the original TOML bytes, including quotes and line endings. + if current.is_some_and(|url| url != expected || s.port == old_port) { + return Ok(None); + } + document["server_url"] = toml_edit::value(format!("http://127.0.0.1:{}", s.port)); + let updated = document.to_string(); + Ok((updated != text).then_some(updated)) +} + +fn codex_profile(s: &Settings) -> String { + let mut value: toml::Value = toml::from_str( + include_str!("../../../scripts/config/codex.sy.toml") + .replace("@SY_PORT@", &s.port.to_string()) + .as_str(), + ) + .expect("bundled profile is valid TOML"); + value["model"] = s.model.clone().into(); + toml::to_string(&value).expect("profile values serialize") +} +fn merge_codex_profile(s: &Settings, current: &str) -> Result { + let mut document: toml_edit::DocumentMut = current + .parse() + .map_err(|e| format!("Read Codex profile: {e}"))?; + let template: toml_edit::DocumentMut = codex_profile(s) + .parse() + .map_err(|e| format!("Read generated Codex profile: {e}"))?; + for key in ["model", "model_provider"] { + document[key] = template[key].clone(); + } + if document.get("model_providers").is_none() { + document["model_providers"] = toml_edit::Item::Table(toml_edit::Table::new()); + } + if !document["model_providers"].is_table_like() { + return Err( + "Codex model_providers must be a table. No services or profiles were changed.".into(), + ); + } + if document["model_providers"] + .get("sy") + .is_some_and(|item| !item.is_table_like()) + { + return Err( + "Codex model_providers.sy must be a table. No services or profiles were changed." + .into(), + ); + } + if document["model_providers"].get("sy").is_none() { + document["model_providers"]["sy"] = toml_edit::Item::Table(toml_edit::Table::new()); + } + for key in ["name", "base_url", "wire_api", "requires_openai_auth"] { + document["model_providers"]["sy"][key] = template["model_providers"]["sy"][key].clone(); + } + Ok(document.to_string()) +} + +fn desktop_config(s: &Settings) -> String { + format!( + "server_url = {:?}\nrouting_log = {}\nconfig_file = {}\nlaunchd_label = {:?}\nrefresh_seconds = 30\n\n{}", + format!("http://127.0.0.1:{}", s.port), + toml_string(&s.sy_home.join("routing.jsonl").to_string_lossy()), + toml_string(&s.sy_home.join("composite.toml").to_string_lossy()), + SERVER, + include_str!("../prices.toml") + ) +} +fn toml_string(value: &str) -> String { + toml::Value::String(value.into()).to_string() +} +fn xml(value: &str) -> String { + value + .replace('&', "&") + .replace('<', "<") + .replace('>', ">") +} +fn plist(label: &str, args: &[String], log: &Path, app: bool) -> String { + let args: String = args + .iter() + .map(|a| format!("{}", xml(a))) + .collect(); + let keep_alive = if app { + "SuccessfulExitLimitLoadToSessionTypeAqua" + } else { + "EnvironmentVariablesRUST_LOGinfo" + }; + format!( + "Label{label}ProgramArguments{args}RunAtLoadKeepAlive{keep_alive}StandardOutPath{}StandardErrorPath{}", + xml(&log.with_extension("log").to_string_lossy()), + xml(&log.with_extension("err.log").to_string_lossy()) + ) +} + +fn domain() -> Result { + let output = Command::new("id") + .arg("-u") + .output() + .map_err(|e| e.to_string())?; + if !output.status.success() { + return Err("Could not read the current user ID.".into()); + } + Ok(format!( + "gui/{}", + String::from_utf8_lossy(&output.stdout).trim() + )) +} +fn stop_services(s: &Settings) -> Result<(), String> { + #[cfg(windows)] + return windows::stop(s); + #[cfg(not(windows))] + { + if cfg!(target_os = "macos") { + let domain = domain()?; + for label in [SERVER, DESKTOP, LEGACY_DESKTOP] { + let job = format!("{domain}/{label}"); + let status = Command::new("launchctl") + .args(["print", &job]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .map_err(|e| e.to_string())?; + if !status.success() { + continue; + } + run(Command::new("launchctl").args(["bootout", &job]))?; + let mut stopped = false; + for _ in 0..50 { + if !Command::new("launchctl") + .args(["print", &job]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .map_err(|e| e.to_string())? + .success() + { + stopped = true; + break; + } + thread::sleep(Duration::from_millis(100)); + } + if !stopped { + return Err(format!( + "{job} did not stop within five seconds. Try again." + )); + } + } + remove(&s.service_dir.join(format!("{LEGACY_DESKTOP}.plist")))?; + } else if s + .service_dir + .join("switchyard.service") + .try_exists() + .map_err(|e| e.to_string())? + { + run(Command::new("systemctl").args(["--user", "stop", "switchyard.service"]))?; + } + Ok(()) + } +} + +fn stage_bundle(s: &Settings, metadata: &str) -> Result { + let parent = s + .app() + .parent() + .ok_or("The app has no parent directory.")? + .to_path_buf(); + fs::create_dir_all(&parent).map_err(|e| e.to_string())?; + let stage = tempfile::tempdir_in(parent).map_err(|e| e.to_string())?; + let app = stage.path().join("Switchyard.app"); + let contents = app.join("Contents"); + write( + &contents.join("MacOS/switchyard-desktop"), + &fs::read(s.release("switchyard-desktop")).map_err(|e| e.to_string())?, + true, + )?; + write( + &contents.join("Resources/install.toml"), + metadata.as_bytes(), + false, + )?; + write( + &contents.join("Resources/Switchyard.icns"), + include_bytes!("../../switchyard-desktop/icons/icon.icns"), + false, + )?; + let info = format!( + "CFBundleIdentifiercom.nvidia.switchyardCFBundleNameSwitchyardCFBundleDisplayNameSwitchyardCFBundleExecutableswitchyard-desktopCFBundleIconFileSwitchyard.icnsCFBundlePackageTypeAPPLCFBundleShortVersionString{}LSMinimumSystemVersion12.0NSHighResolutionCapable", + env!("CARGO_PKG_VERSION") + ); + write(&contents.join("Info.plist"), info.as_bytes(), false)?; + run(Command::new("codesign") + .args(["--force", "--deep", "--sign", "-"]) + .arg(&app))?; + Ok(stage) +} + +fn install_macos(s: &Settings, stage: tempfile::TempDir) -> Result<(), String> { + let previous = stage.path().join("Previous.app"); + let existed = s.app().try_exists().map_err(|e| e.to_string())?; + if existed { + fs::rename(s.app(), &previous).map_err(|e| e.to_string())?; + } + if let Err(error) = fs::rename(stage.path().join("Switchyard.app"), s.app()) { + if existed { + fs::rename(&previous, s.app()).map_err(|restore| { + format!("Replace app: {error}. Restore previous app: {restore}") + })?; + } + return Err(format!("Replace app: {error}")); + } + let contents = s.app().join("Contents"); + let args = vec![ + s.binary("switchyard-desktop-install").display().to_string(), + "serve".into(), + "--settings".into(), + s.sy_home.join("install.toml").display().to_string(), + ]; + write( + &s.service_dir.join(format!("{SERVER}.plist")), + plist(SERVER, &args, &s.sy_home.join("logs/server"), false).as_bytes(), + false, + )?; + let args = vec![ + contents + .join("MacOS/switchyard-desktop") + .display() + .to_string(), + s.desktop_settings().display().to_string(), + ]; + write( + &s.service_dir.join(format!("{DESKTOP}.plist")), + plist(DESKTOP, &args, &s.sy_home.join("logs/desktop"), true).as_bytes(), + false, + )?; + let domain = domain()?; + for label in [SERVER, DESKTOP] { + run(Command::new("launchctl") + .arg("bootstrap") + .arg(&domain) + .arg(s.service_dir.join(format!("{label}.plist"))))?; + } + Ok(()) +} + +fn systemd_arg(path: &Path) -> String { + // systemd expands percent specifiers and environment variables inside quoted arguments. + format!( + "\"{}\"", + path.to_string_lossy() + .replace('\\', "\\\\") + .replace('"', "\\\"") + .replace('%', "%%") + .replace('$', "$$") + ) +} +fn install_linux(s: &Settings) -> Result<(), String> { + let unit = format!( + "[Unit]\nDescription=Switchyard local server\n\n[Service]\nType=simple\nExecStart={} serve --settings {}\nRestart=on-failure\nEnvironment=RUST_LOG=info\n\n[Install]\nWantedBy=default.target\n", + systemd_arg(&s.binary("switchyard-desktop-install")), + systemd_arg(&s.sy_home.join("install.toml")), + ); + write( + &s.service_dir.join("switchyard.service"), + unit.as_bytes(), + false, + )?; + run(Command::new("systemctl").args(["--user", "daemon-reload"]))?; + run(Command::new("systemctl").args(["--user", "enable", "switchyard.service"]))?; + run(Command::new("systemctl").args(["--user", "restart", "switchyard.service"]))?; + thread::sleep(Duration::from_secs(2)); + run(Command::new("systemctl").args(["--user", "is-active", "--quiet", "switchyard.service"])) + .map_err(|e| format!("{e}. See journalctl --user -u switchyard.")) +} + +fn strip_block(path: &Path, start: &str, end: &str) -> Result<(), String> { + if !path.try_exists().map_err(|e| e.to_string())? { + return Ok(()); + } + let text = fs::read_to_string(path).map_err(|e| e.to_string())?; + let mut output = String::new(); + let mut skipping = false; + for line in text.split_inclusive('\n') { + if line.contains(start) { + skipping = true; + } + if !skipping { + output.push_str(line); + } + if line.contains(end) { + skipping = false; + } + } + if skipping { + return Err(format!( + "{} has an unfinished Switchyard block. No edits were made to this file.", + path.display() + )); + } + if text != output { + write(path, output.as_bytes(), false)?; + } + Ok(()) +} +// The persistent file keeps concurrent commands locking the same inode; exit releases the lock. +fn lock_installation(s: &Settings) -> Result { + let lock = fs::OpenOptions::new() + .create(true) + .truncate(false) + .read(true) + .write(true) + .open(s.home.join(".switchyard-desktop-install.lock")) + .map_err(|e| e.to_string())?; + lock.try_lock() + .map_err(|e| format!("Could not lock the desktop installation: {e}"))?; + Ok(lock) +} + +fn uninstall(s: &Settings) -> Result<(), String> { + let _lock = lock_installation(s)?; + println!("{}", installation_plan(s, "uninstall")?); + if s.profile_file().exists() { + backup(&s.profile_file(), "switchyard-uninstall-recovery")?; + } + stop_services(s)?; + if cfg!(target_os = "macos") { + for label in [SERVER, DESKTOP, LEGACY_DESKTOP] { + remove(&s.service_dir.join(format!("{label}.plist")))?; + } + if s.app().try_exists().map_err(|e| e.to_string())? { + fs::remove_dir_all(s.app()).map_err(|e| e.to_string())?; + } + } else if cfg!(windows) { + #[cfg(windows)] + windows::uninstall(s)?; + } else { + let unit = s.service_dir.join("switchyard.service"); + if unit.try_exists().map_err(|e| e.to_string())? { + run(Command::new("systemctl").args(["--user", "disable", "switchyard.service"]))?; + remove(&unit)?; + } + run(Command::new("systemctl").args(["--user", "daemon-reload"]))?; + } + remove(&s.profile_file())?; + remove(&s.codex_home.join("config.sy.toml"))?; + let config = s.codex_home.join("config.toml"); + strip_block( + &config, + "# >>> switchyard sy profile >>>", + "# <<< switchyard sy profile <<<", + )?; + let direct = s.codex_home.join("config.toml.direct"); + if direct.try_exists().map_err(|e| e.to_string())? { + if config.try_exists().map_err(|e| e.to_string())? { + backup(&config, "switchyard-current")?; + } + write( + &config, + &fs::read(&direct).map_err(|e| e.to_string())?, + false, + )?; + remove(&direct)?; + } + for name in [".zshrc", ".bashrc"] { + strip_block( + &s.home.join(name), + "# >>> switchyard codex alias >>>", + "# <<< switchyard codex alias <<<", + )?; + } + println!( + "Removed services, app, and profile. Kept settings, accounts, {}and history at {}.", + if cfg!(windows) { "" } else { "binaries, " }, + s.sy_home.display() + ); + let recorded = s.home.join(".switchyard-desktop-install.toml"); + if recorded.exists() && Settings::load(&recorded)?.sy_home == s.sy_home { + remove(&recorded)?; + } + Ok(()) +} diff --git a/crates/switchyard-desktop-install/src/main.rs b/crates/switchyard-desktop-install/src/main.rs new file mode 100644 index 0000000000..bcd4946818 --- /dev/null +++ b/crates/switchyard-desktop-install/src/main.rs @@ -0,0 +1,274 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +#![cfg_attr(windows, windows_subsystem = "windows")] + +use std::{path::Path, process::ExitCode}; +use switchyard_desktop_install::{Settings, execute}; + +const HELP: &str = "Usage: cargo desktop [--dry-run]\n\nBuild and install the local server and macOS or Windows desktop app, or Linux terminal app.\nUninstall uses the recorded installation and preserves settings, history, and recovery copies.\n\nDefaults: SY_HOME=~/.switchyard, SY_PORT=4123, SY_PROFILE=sy,\nSY_MODEL=composite-gpt-6-sol-gpt-6-luna, CODEX_HOME=~/.codex.\nExample: SY_PROFILE=team SY_MODEL=my-route cargo desktop install\n\nPreview the installation or removal plan with --dry-run. Installation requires\na Rust toolchain and LaunchAgents (macOS) or a systemd user session (Linux).\nWindows requires MSVC build tools, WebView2, and a Task Scheduler user session.\nThe Cargo command waits for completion. On Windows, running the installed installer\nstarts a temporary process and returns; read SY_HOME/logs/update.log for progress and errors.\nUse uninstall --settings FILE to select another recorded installation.\nPreview its removal with: cargo desktop uninstall --settings FILE --dry-run"; + +fn run() -> Result<(), String> { + let args: Vec<_> = std::env::args().skip(1).collect(); + if args + .first() + .is_some_and(|a| matches!(a.as_str(), "capture" | "validate")) + { + let [ + action, + server_flag, + server, + config_flag, + config, + scope_flag, + scope, + ] = args.as_slice() + else { + return Err("Invalid credential command arguments.".into()); + }; + if server_flag != "--server" + || config_flag != "--config" + || scope_flag != "--scope" + || [server, config, scope] + .iter() + .any(|p| !Path::new(p).is_absolute() || p.chars().any(|c| c.is_ascii_control())) + { + return Err("Invalid credential command arguments.".into()); + } + return switchyard_desktop_install::credentials::validate( + Path::new(server), + Path::new(config), + Path::new(scope), + action == "capture", + ); + } + #[cfg(unix)] + if let [action, flag, file] = std::env::args().skip(1).collect::>().as_slice() + && action == "serve" + && flag == "--settings" + { + use std::os::unix::process::CommandExt; + // Exec preserves the service manager's PID while the helper supplies saved credentials. + let s = Settings::load(Path::new(file))?; + let config = s.sy_home.join("composite.toml"); + let mut server = std::process::Command::new(s.binary("switchyard-server")); + switchyard_desktop_install::credentials::configure(&mut server, &config, &config)?; + let error = server + .arg("--config") + .arg(config) + .args([ + "--host", + "127.0.0.1", + "--port", + &s.port.to_string(), + "--routing-log-file", + ]) + .arg(s.sy_home.join("routing.jsonl")) + .exec(); + return Err(format!("Could not start the server: {error}")); + } + #[cfg(windows)] + if let Some(result) = windows_command()? { + return result; + } + let args: Vec<_> = std::env::args().skip(1).collect(); + if args == ["--help"] + || args == ["-h"] + || (args.len() == 2 + && matches!(args[0].as_str(), "install" | "uninstall") + && matches!(args[1].as_str(), "--help" | "-h")) + { + println!("{HELP}"); + return Ok(()); + } + let (action, dry_run, file) = match args.as_slice() { + [action] if action == "install" || action == "uninstall" => (action.as_str(), false, None), + [action, flag] if (action == "install" || action == "uninstall") && flag == "--dry-run" => { + (action.as_str(), true, None) + } + [action, flag, file] + if (action == "update" || action == "uninstall") && flag == "--settings" => + { + (action.as_str(), false, Some(Path::new(file))) + } + [action, settings, file, dry] + if action == "uninstall" && settings == "--settings" && dry == "--dry-run" => + { + (action.as_str(), true, Some(Path::new(file))) + } + [action, dry, settings, file] + if action == "uninstall" && settings == "--settings" && dry == "--dry-run" => + { + (action.as_str(), true, Some(Path::new(file))) + } + _ => return Err(HELP.into()), + }; + let settings = if let Some(file) = file { + Settings::load(file)? + } else if action == "uninstall" { + Settings::for_uninstall()? + } else { + Settings::from_env()? + }; + #[cfg(windows)] + if !dry_run + && settings + .binary("switchyard-desktop-install") + .try_exists() + .map_err(|e| e.to_string())? + && std::fs::canonicalize(std::env::current_exe().map_err(|e| e.to_string())?) + .map_err(|e| e.to_string())? + == std::fs::canonicalize(settings.binary("switchyard-desktop-install")) + .map_err(|e| e.to_string())? + { + return handoff(&settings, action); + } + execute(&settings, action, dry_run) +} + +fn main() -> ExitCode { + #[cfg(windows)] + if !std::env::args_os().nth(1).is_some_and(|arg| { + matches!( + arg.to_str(), + Some("serve" | "desktop" | "apply" | "update" | "validate" | "capture") + ) + }) { + unsafe { + windows_sys::Win32::System::Console::AttachConsole(u32::MAX); + } + } + let result = run(); + #[cfg(windows)] + if std::env::current_exe() + .ok() + .and_then(|p| { + p.file_name() + .map(|n| n.to_string_lossy().starts_with("switchyard-update-")) + }) + .unwrap_or(false) + && let Err(error) = self_replace::self_delete() + { + eprintln!("switchyard-desktop-install: {error}"); + return ExitCode::FAILURE; + } + match result { + Ok(()) => ExitCode::SUCCESS, + Err(error) => { + eprintln!("switchyard-desktop-install: {error}"); + ExitCode::FAILURE + } + } +} + +#[cfg(windows)] +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +struct Handoff { + settings: T, +} + +#[cfg(windows)] +fn windows_command() -> Result>, String> { + let args: Vec<_> = std::env::args().skip(1).collect(); + match args.as_slice() { + [action, flag, file] + if matches!(action.as_str(), "serve" | "desktop") && flag == "--settings" => + { + let settings = Settings::load(Path::new(file))?; + Ok(Some(switchyard_desktop_install::windows::launch_managed( + &settings, + action == "desktop", + ))) + } + [apply, flag, file, action, operation, wait, pid] + if apply == "apply" + && flag == "--handoff-record" + && action == "--action" + && wait == "--wait" + && matches!(operation.as_str(), "install" | "update" | "uninstall") => + { + let path = Path::new(file); + let metadata = std::fs::symlink_metadata(path).map_err(|e| e.to_string())?; + let parent = path.parent().ok_or("Invalid handoff record.")?; + let name = path + .file_name() + .and_then(|name| name.to_str()) + .ok_or("Invalid handoff record.")?; + if !metadata.file_type().is_file() + || !name.starts_with("switchyard-handoff-") + || !name.ends_with(".toml") + || std::fs::canonicalize(parent).map_err(|e| e.to_string())? + != std::fs::canonicalize(std::env::temp_dir()).map_err(|e| e.to_string())? + { + return Err("Invalid handoff record.".into()); + } + let handoff: Handoff = + toml::from_str(&std::fs::read_to_string(path).map_err(|e| e.to_string())?) + .map_err(|e| e.to_string())?; + handoff.settings.validate()?; + let pid = pid + .parse::() + .map_err(|_| "Invalid parent process ID.")?; + if pid == 0 { + return Err("Invalid parent process ID.".into()); + } + let _record = tempfile::TempPath::try_from_path(path).map_err(|e| e.to_string())?; + let result = switchyard_desktop_install::windows::wait_parent(pid) + .and_then(|()| execute(&handoff.settings, operation, false)); + Ok(Some(result)) + } + _ => Ok(None), + } +} +#[cfg(windows)] +// The installed executable cannot replace itself while Windows holds it open. +fn handoff(settings: &Settings, action: &str) -> Result<(), String> { + use std::io::Write; + use std::process::{Command, Stdio}; + switchyard_desktop_install::windows::record_handoff(settings)?; + let helper = switchyard_desktop_install::windows::temporary_updater( + &std::env::current_exe().map_err(|e| e.to_string())?, + )?; + let mut record = tempfile::Builder::new() + .prefix("switchyard-handoff-") + .suffix(".toml") + .tempfile() + .map_err(|e| e.to_string())?; + record + .write_all( + toml::to_string(&Handoff { settings }) + .map_err(|e| e.to_string())? + .as_bytes(), + ) + .map_err(|e| e.to_string())?; + let file = record.into_temp_path(); + std::fs::create_dir_all(settings.sy_home.join("logs")).map_err(|e| e.to_string())?; + let log = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(settings.sy_home.join("logs/update.log")) + .map_err(|e| e.to_string())?; + let mut child = Command::new(&helper); + switchyard_desktop_install::windows::hide_console(&mut child); + child + .arg("apply") + .arg("--handoff-record") + .arg(&file) + .arg("--action") + .arg(action) + .arg("--wait") + .arg(std::process::id().to_string()) + .stdin(Stdio::null()) + .stderr(log.try_clone().map_err(|e| e.to_string())?) + .stdout(log) + .spawn() + .map_err(|e| e.to_string())?; + helper.keep().map_err(|e| e.to_string())?; + file.keep().map_err(|e| e.to_string())?; + println!( + "Started {action}. Progress and errors: {}", + settings.sy_home.join("logs/update.log").display() + ); + Ok(()) +} diff --git a/crates/switchyard-desktop-install/src/windows.rs b/crates/switchyard-desktop-install/src/windows.rs new file mode 100644 index 0000000000..fb9236a461 --- /dev/null +++ b/crates/switchyard-desktop-install/src/windows.rs @@ -0,0 +1,575 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! This module installs per-user Windows tasks and shortcuts and replaces stopped app executables. + +use super::*; +use std::os::windows::{ + ffi::{OsStrExt, OsStringExt}, + process::CommandExt, +}; +use windows_sys::Win32::{ + Foundation::{CloseHandle, ERROR_NOT_FOUND, HANDLE, INVALID_HANDLE_VALUE, WAIT_OBJECT_0}, + Security::Credentials::{ + CRED_PERSIST_LOCAL_MACHINE, CRED_TYPE_GENERIC, CREDENTIALW, CredFree, CredReadW, CredWriteW, + }, + System::{ + Diagnostics::ToolHelp::{ + CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW, + TH32CS_SNAPPROCESS, + }, + Threading::{ + CREATE_NO_WINDOW, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_SYNCHRONIZE, + PROCESS_TERMINATE, QueryFullProcessImageNameW, TerminateProcess, WaitForSingleObject, + }, + }, +}; + +struct Handle(HANDLE); +impl Drop for Handle { + fn drop(&mut self) { + unsafe { + CloseHandle(self.0); + } + } +} +fn wide(value: &std::ffi::OsStr) -> Vec { + value.encode_wide().chain(Some(0)).collect() +} +fn error() -> String { + std::io::Error::last_os_error().to_string() +} +pub fn hide_console(command: &mut Command) { + command.creation_flags(CREATE_NO_WINDOW); +} + +pub fn task_name(s: &Settings, desktop: bool) -> String { + // The binary directory keeps task names stable when SY_HOME changes or a Rust toolchain changes. + let mut directory = PathBuf::new(); + for component in s.bin_dir().components() { + match component { + std::path::Component::CurDir => {} + std::path::Component::ParentDir => { + directory.pop(); + } + _ => directory.push(component.as_os_str()), + } + } + let key = directory + .to_string_lossy() + .replace('\\', "/") + .to_lowercase(); + let key = if let Some(unc) = key.strip_prefix("//?/unc/") { + format!("//{unc}") + } else { + key.strip_prefix("//?/").unwrap_or(&key).to_owned() + }; + let hash = key.bytes().fold(0xcbf29ce484222325u64, |h, b| { + (h ^ u64::from(b)).wrapping_mul(0x100000001b3) + }); + format!( + "Switchyard-{}-{hash:016x}", + if desktop { "Desktop" } else { "Server" } + ) +} +fn scheduler() -> Command { + let mut c = Command::new("schtasks.exe"); + hide_console(&mut c); + c +} +fn exists(s: &Settings, desktop: bool) -> Result { + scheduler() + .args(["/Query", "/TN", &task_name(s, desktop)]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .map(|s| s.success()) + .map_err(|e| e.to_string()) +} +fn identity() -> Result { + use windows_sys::Win32::{ + Foundation::LocalFree, + Security::{ + Authorization::ConvertSidToStringSidW, GetTokenInformation, TOKEN_QUERY, TOKEN_USER, + TokenUser, + }, + System::Threading::{GetCurrentProcess, OpenProcessToken}, + }; + unsafe { + let mut token = std::ptr::null_mut(); + if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) == 0 { + return Err(error()); + } + let token = Handle(token); + let mut length = 0; + GetTokenInformation(token.0, TokenUser, std::ptr::null_mut(), 0, &mut length); + if length < std::mem::size_of::() as u32 { + return Err(error()); + } + let mut buffer = vec![0usize; (length as usize).div_ceil(std::mem::size_of::())]; + if GetTokenInformation( + token.0, + TokenUser, + buffer.as_mut_ptr().cast(), + length, + &mut length, + ) == 0 + { + return Err(error()); + } + let user = &*buffer.as_ptr().cast::(); + let mut sid = std::ptr::null_mut(); + if ConvertSidToStringSidW(user.User.Sid, &mut sid) == 0 { + return Err(error()); + } + let length = (0..256).find(|&index| *sid.add(index) == 0); + let result = length + .ok_or_else(|| "Invalid Windows account SID.".to_owned()) + .and_then(|length| { + String::from_utf16(std::slice::from_raw_parts(sid, length)) + .map_err(|e| e.to_string()) + }); + LocalFree(sid.cast()); + result + } +} +// Separate tasks let the server continue after the desktop app exits. +fn task_xml(s: &Settings, user: &str, desktop: bool) -> String { + let program = s.binary("switchyard-desktop-install"); + let args = format!( + "{} --settings {}", + if desktop { "desktop" } else { "serve" }, + quote_arg(&s.sy_home.join("install.toml").display().to_string()) + ); + let user = xml(user); + let restart = if desktop { + "" + } else { + "PT1M999" + }; + format!( + "true{user}{user}InteractiveTokenLeastPrivilegeIgnoreNewfalsefalsetruetruePT0S{restart}{}{}{}", + xml(&program.display().to_string()), + xml(&args), + xml(&s.bin_dir().display().to_string()) + ) +} +// Task Scheduler parses a Windows argument string; trailing slashes must not escape its closing quote. +pub fn quote_arg(value: &str) -> String { + let mut out = String::from("\""); + let mut slashes = 0; + for ch in value.chars() { + if ch == '\\' { + slashes += 1; + continue; + } + out.extend(std::iter::repeat_n( + '\\', + if ch == '"' { slashes * 2 + 1 } else { slashes }, + )); + out.push(ch); + slashes = 0; + } + out.extend(std::iter::repeat_n('\\', slashes * 2)); + out.push('"'); + out +} +fn shortcut_paths(s: &Settings) -> [PathBuf; 2] { + [ + s.service_dir.join("Switchyard.lnk"), + s.service_dir.join("Startup/Switchyard.lnk"), + ] +} +pub fn install(s: &Settings) -> Result<(), String> { + write( + &s.bin_dir().join("install.toml"), + toml::to_string(s).map_err(|e| e.to_string())?.as_bytes(), + false, + )?; + let user = identity()?; + for desktop in [false, true] { + let mut file = tempfile::NamedTempFile::new().map_err(|e| e.to_string())?; + let data: Vec = [0xfeffu16] + .into_iter() + .chain(task_xml(s, &user, desktop).encode_utf16()) + .flat_map(u16::to_le_bytes) + .collect(); + file.write_all(&data).map_err(|e| e.to_string())?; + // Closing the writer lets schtasks read the XML; TempPath keeps cleanup active during import. + let path = file.into_temp_path(); + run(scheduler() + .args(["/Create", "/F", "/TN", &task_name(s, desktop), "/XML"]) + .arg(&path))?; + } + let mut link = + mslnk::ShellLink::new(s.binary("switchyard-desktop")).map_err(|e| e.to_string())?; + link.set_arguments(Some(quote_arg(&s.desktop_settings().display().to_string()))); + for path in shortcut_paths(s).into_iter().take(1) { + fs::create_dir_all(path.parent().ok_or("Shortcut has no directory.")?) + .map_err(|e| e.to_string())?; + link.create_lnk(path).map_err(|e| e.to_string())?; + } + for desktop in [false, true] { + run(scheduler().args(["/Run", "/TN", &task_name(s, desktop)]))?; + } + Ok(()) +} +fn stop_task(s: &Settings, desktop: bool) -> Result<(), String> { + if exists(s, desktop)? { + // /End reports an error for a task that has already exited. Path-checked handles below verify shutdown. + let _ = scheduler() + .args(["/End", "/TN", &task_name(s, desktop)]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .map_err(|e| e.to_string())?; + } + Ok(()) +} +// Full executable paths distinguish this installation from other copies running under the same user. +fn stop_images(images: &[PathBuf]) -> Result<(), String> { + let targets = images + .iter() + .filter(|p| p.exists()) + .map(fs::canonicalize) + .collect::, _>>() + .map_err(|e| e.to_string())?; + unsafe { + let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if snapshot == INVALID_HANDLE_VALUE { + return Err(error()); + } + let snapshot = Handle(snapshot); + let mut entry: PROCESSENTRY32W = std::mem::zeroed(); + entry.dwSize = std::mem::size_of::() as u32; + let mut found = Process32FirstW(snapshot.0, &mut entry); + while found != 0 { + if entry.th32ProcessID != std::process::id() { + let process = OpenProcess( + PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_TERMINATE | PROCESS_SYNCHRONIZE, + 0, + entry.th32ProcessID, + ); + if !process.is_null() { + let process = Handle(process); + let mut buffer = vec![0u16; 32768]; + let mut length = buffer.len() as u32; + if QueryFullProcessImageNameW(process.0, 0, buffer.as_mut_ptr(), &mut length) + != 0 + { + let path = PathBuf::from(std::ffi::OsString::from_wide( + &buffer[..length as usize], + )); + if fs::canonicalize(path).is_ok_and(|p| targets.iter().any(|t| t == &p)) { + if TerminateProcess(process.0, 0) == 0 + && WaitForSingleObject(process.0, 0) != WAIT_OBJECT_0 + { + return Err(error()); + } + if WaitForSingleObject(process.0, 10000) != WAIT_OBJECT_0 { + return Err( + "A managed Windows process did not stop within ten seconds." + .into(), + ); + } + } + } + } + } + found = Process32NextW(snapshot.0, &mut entry); + } + } + Ok(()) +} +pub fn stop(s: &Settings) -> Result<(), String> { + for desktop in [false, true] { + stop_task(s, desktop)?; + } + stop_images(&[ + s.binary("switchyard-server"), + s.binary("switchyard-desktop"), + s.binary("switchyard-desktop-install"), + ]) +} +pub fn restart(s: &Settings) -> Result<(), String> { + stop_task(s, false)?; + stop_images(&[s.binary("switchyard-server")])?; + run(scheduler().args(["/Run", "/TN", &task_name(s, false)])) +} +pub fn uninstall(s: &Settings) -> Result<(), String> { + for desktop in [false, true] { + if exists(s, desktop)? { + run(scheduler().args(["/Delete", "/F", "/TN", &task_name(s, desktop)]))?; + } + } + for path in shortcut_paths(s) { + remove(&path)?; + } + // Config and history live in SY_HOME. Only the managed application directory is removed. + if s.bin_dir().exists() { + fs::remove_dir_all(s.bin_dir()).map_err(|e| e.to_string())?; + } + Ok(()) +} +/// This function stages all binaries and attempts to restore earlier replacements if one fails. +pub fn replace_binaries(s: &Settings) -> Result<(), String> { + let names = [ + "switchyard-server", + "switchyard-desktop", + "switchyard-desktop-install", + ]; + fs::create_dir_all(s.bin_dir()).map_err(|e| e.to_string())?; + let stage = tempfile::tempdir_in(s.bin_dir()).map_err(|e| e.to_string())?; + for name in names { + fs::copy(s.release(name), stage.path().join(executable_name(name))) + .map_err(|e| e.to_string())?; + } + let mut changed: Vec<(&str, bool)> = Vec::new(); + for name in names { + let dest = s.binary(name); + let previous = stage.path().join(format!("{name}.previous")); + let existed = dest.exists(); + let result = (|| { + if existed { + fs::rename(&dest, &previous).map_err(|e| e.to_string())?; + } + changed.push((name, existed)); + fs::rename(stage.path().join(executable_name(name)), &dest).map_err(|e| e.to_string()) + })(); + if let Err(error) = result { + let mut failures = Vec::new(); + for (name, existed) in changed.into_iter().rev() { + let restored = (|| { + remove(&s.binary(name))?; + if existed { + fs::rename( + stage.path().join(format!("{name}.previous")), + s.binary(name), + ) + .map_err(|e| e.to_string())?; + } + Ok::<(), String>(()) + })(); + if let Err(restore) = restored { + failures.push(format!("{name}: {restore}")); + } + } + if !failures.is_empty() { + // Failed restoration must retain the previous files for manual recovery. + let recovery = stage.keep(); + return Err(format!( + "Could not replace app files: {error}. Restore failures: {}. Previous files remain at {}.", + failures.join("; "), + recovery.display() + )); + } + return Err(format!( + "Could not replace app files; restored the previous binaries: {error}" + )); + } + } + Ok(()) +} +/// The updater runs from a temporary copy because Windows locks running executables against replacement. +pub fn temporary_updater(source: &Path) -> Result { + let file = tempfile::Builder::new() + .prefix("switchyard-update-") + .suffix(".exe") + .tempfile() + .map_err(|e| e.to_string())?; + fs::copy(source, file.path()).map_err(|e| e.to_string())?; + Ok(file.into_temp_path()) +} +pub fn wait_parent(pid: u32) -> Result<(), String> { + use windows_sys::Win32::Foundation::{ERROR_INVALID_PARAMETER, GetLastError}; + if pid == 0 { + return Err("Invalid parent process ID.".into()); + } + unsafe { + let handle = OpenProcess(PROCESS_SYNCHRONIZE, 0, pid); + if handle.is_null() { + return if GetLastError() == ERROR_INVALID_PARAMETER { + Ok(()) + } else { + Err(error()) + }; + } + let handle = Handle(handle); + if WaitForSingleObject(handle.0, 30000) != WAIT_OBJECT_0 { + return Err("The installer parent did not exit within thirty seconds.".into()); + } + } + Ok(()) +} +/// This function waits for its child so Task Scheduler tracks the app or server's lifetime. +pub fn launch_managed(s: &Settings, desktop: bool) -> Result<(), String> { + fs::create_dir_all(s.sy_home.join("logs")).map_err(|e| e.to_string())?; + let log = fs::OpenOptions::new() + .create(true) + .append(true) + .open(s.sy_home.join(if desktop { + "logs/desktop.log" + } else { + "logs/server.log" + })) + .map_err(|e| e.to_string())?; + let mut c = Command::new(s.binary(if desktop { + "switchyard-desktop" + } else { + "switchyard-server" + })); + hide_console(&mut c); + c.env("HOME", &s.home) + .env("USERPROFILE", &s.home) + .env("CODEX_HOME", &s.codex_home) + .env("SY_HOME", &s.sy_home) + .current_dir(s.bin_dir()); + if desktop { + c.arg(s.desktop_settings()); + } else { + c.arg("--config") + .arg(s.sy_home.join("composite.toml")) + .args([ + "--host", + "127.0.0.1", + "--port", + &s.port.to_string(), + "--routing-log-file", + ]) + .arg(s.sy_home.join("routing.jsonl")); + } + if !desktop { + crate::credentials::configure( + &mut c, + &s.sy_home.join("composite.toml"), + &s.sy_home.join("composite.toml"), + )?; + } + let status = c + .stdin(Stdio::null()) + .stderr(log.try_clone().map_err(|e| e.to_string())?) + .stdout(log) + .status() + .map_err(|e| e.to_string())?; + if status.success() { + Ok(()) + } else { + Err(format!("Managed application exited with {status}.")) + } +} +pub fn open(value: &str) -> Result<(), String> { + use windows_sys::Win32::UI::{Shell::ShellExecuteW, WindowsAndMessaging::SW_SHOWNORMAL}; + let value = wide(std::ffi::OsStr::new(value)); + let verb = wide(std::ffi::OsStr::new("open")); + let status = unsafe { + ShellExecuteW( + std::ptr::null_mut(), + verb.as_ptr(), + value.as_ptr(), + std::ptr::null(), + std::ptr::null(), + SW_SHOWNORMAL, + ) + } as isize; + if status > 32 { + Ok(()) + } else { + Err(format!( + "Windows could not open the file or URL (error {status})." + )) + } +} +pub fn saved_key(base_url: &str) -> Result, String> { + read_credential(&format!("Switchyard:model-list:{base_url}")) +} + +pub(crate) fn read_credential(target: &str) -> Result, String> { + let target = wide(std::ffi::OsStr::new(target)); + unsafe { + let mut credential: *mut CREDENTIALW = std::ptr::null_mut(); + if CredReadW(target.as_ptr(), CRED_TYPE_GENERIC, 0, &mut credential) == 0 { + if std::io::Error::last_os_error().raw_os_error() == Some(ERROR_NOT_FOUND as i32) { + return Ok(None); + } + return Err(error()); + } + let credential_ref = &*credential; + let bytes = if credential_ref.CredentialBlobSize == 0 { + Vec::new() + } else { + std::slice::from_raw_parts( + credential_ref.CredentialBlob, + credential_ref.CredentialBlobSize as usize, + ) + .to_vec() + }; + CredFree(credential.cast()); + String::from_utf8(bytes) + .map(|s| Some(s).filter(|s| !s.trim().is_empty())) + .map_err(|e| e.to_string()) + } +} +pub fn save_key(base_url: &str, key: &str) -> Result<(), String> { + write_credential(&format!("Switchyard:model-list:{base_url}"), key.trim()) +} + +pub(crate) fn write_credential(target: &str, key: &str) -> Result<(), String> { + let mut target = wide(std::ffi::OsStr::new(target)); + let mut bytes = key.as_bytes().to_vec(); + let mut credential: CREDENTIALW = unsafe { std::mem::zeroed() }; + credential.Type = CRED_TYPE_GENERIC; + credential.TargetName = target.as_mut_ptr(); + credential.Persist = CRED_PERSIST_LOCAL_MACHINE; + credential.CredentialBlobSize = bytes.len().try_into().map_err(|_| "The key is too long.")?; + credential.CredentialBlob = bytes.as_mut_ptr(); + if unsafe { CredWriteW(&credential, 0) } == 0 { + Err(error()) + } else { + Ok(()) + } +} + +pub fn record_handoff(s: &Settings) -> Result<(), String> { + let _lock = update_lock(s)?; + set_update_state( + s, + "starting", + "start", + "Starting the Windows source installer.", + ) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn tasks_follow_the_installed_app_directory() { + let mut settings = Settings::from_env().expect("settings"); + settings.app_dir = Some(PathBuf::from(r"C:\Users\Team\Apps\Switchyard")); + let names = [task_name(&settings, false), task_name(&settings, true)]; + assert_ne!(names[0], names[1]); + settings.sy_home = PathBuf::from(r"C:\Users\Team\New settings"); + for path in [ + r"c:/users/team/apps/switchyard/", + r"C:\Users\Team\Apps\.\Other\..\Switchyard", + r"\\?\C:\Users\Team\Apps\Switchyard", + ] { + settings.app_dir = Some(PathBuf::from(path)); + assert_eq!( + [task_name(&settings, false), task_name(&settings, true)], + names + ); + } + settings.app_dir = Some(PathBuf::from(r"C:\Users\Team\Apps\Another")); + assert_ne!(task_name(&settings, false), names[0]); + settings.app_dir = Some(PathBuf::from(r"\\server\share\Apps\Switchyard")); + let unc = task_name(&settings, false); + settings.app_dir = Some(PathBuf::from(r"\\?\UNC\SERVER\SHARE\Apps\Switchyard")); + assert_eq!(task_name(&settings, false), unc); + } + #[test] + fn windows_arguments_escape_quotes_and_trailing_slashes() { + assert_eq!(quote_arg("C:\\équipe space\\"), "\"C:\\équipe space\\\\\""); + assert_eq!(quote_arg("a\"b"), "\"a\\\"b\""); + } +} diff --git a/crates/switchyard-desktop-install/tests/fixture.txt b/crates/switchyard-desktop-install/tests/fixture.txt new file mode 100644 index 0000000000..da51c5e143 --- /dev/null +++ b/crates/switchyard-desktop-install/tests/fixture.txt @@ -0,0 +1,37 @@ +use std::{env, fs, path::Path}; +fn main() { + let args: Vec<_> = env::args().collect(); + let exe = env::current_exe().unwrap(); + let name = Path::new(&args[0]).file_name().unwrap().to_str().unwrap(); + if matches!(name, "launchctl" | "systemctl") { + use std::io::Write; + let mut calls = fs::OpenOptions::new().create(true).append(true) + .open(Path::new(&env::var_os("HOME").unwrap()).join("service_calls")).unwrap(); + writeln!(calls, "{args:?}").unwrap(); + } + match name { + // macOS codesign rejects --version, so this fixture must not accept it as a preflight check. + "codesign" if args.get(1).is_some_and(|s| s == "--version") => std::process::exit(2), + "cargo" => { + let target = &args[args.iter().position(|s| s == "--target-dir").unwrap() + 1]; + let release = Path::new(target).join("release"); + fs::create_dir_all(&release).unwrap(); + for bin in ["switchyard-server", "switchyard-desktop"] { fs::copy(&exe, release.join(bin)).unwrap(); } + fs::copy(env::var("INSTALLER_BIN").unwrap(), release.join("switchyard-desktop-install")).unwrap(); + } + "switchyard-desktop-install" => { + let output = std::process::Command::new("ps") + .args(["-o", "pgid=", "-p", &std::process::id().to_string()]) + .output().unwrap(); + assert!(output.status.success()); + print!("{}", String::from_utf8(output.stdout).unwrap()); + } + "systemctl" if args.get(2).is_some_and(|s| s == "disable") + && !Path::new(&env::var_os("HOME").unwrap()).join(".config/systemd/user/switchyard.service").exists() + => std::process::exit(1), + "id" => println!("501"), + "launchctl" if args.get(1).is_some_and(|s| s == "print") => std::process::exit(1), + "switchyard-server" if env::var("FIXTURE_FAIL_VALIDATE").as_deref() == Ok("1") => std::process::exit(1), + _ => {} + } +} diff --git a/crates/switchyard-desktop-install/tests/install.rs b/crates/switchyard-desktop-install/tests/install.rs new file mode 100644 index 0000000000..90a40cd5b7 --- /dev/null +++ b/crates/switchyard-desktop-install/tests/install.rs @@ -0,0 +1,721 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +#![cfg(unix)] + +use std::{ + fs, + path::{Path, PathBuf}, + process::Command, +}; +use switchyard_desktop_install::Settings; + +// This child process isolates HOME and PATH so service fixtures cannot reach real jobs. +#[test] +fn fixture_process() { + if let Some(file) = std::env::var_os("INSTALL_FIXTURE_SETTINGS") { + let settings = Settings::load(Path::new(&file)).expect("fixture settings"); + let action = std::env::var("INSTALL_FIXTURE_ACTION").expect("fixture action"); + if action == "start_update" { + switchyard_desktop_install::start_update(Path::new(&file)).expect("start updater"); + assert!(switchyard_desktop_install::start_update(Path::new(&file)).is_err()); + for _ in 0..1500 { + let status = + switchyard_desktop_install::update_status(Path::new(&file)).expect("status"); + if status["state"] == "completed" { + return; + } + assert_ne!(status["state"], "failed", "{status}"); + std::thread::sleep(std::time::Duration::from_millis(20)); + } + panic!( + "updater did not complete: {:?}\n{:?}", + switchyard_desktop_install::update_status(Path::new(&file)), + fs::read_to_string(settings.sy_home.join("logs/update.log")), + ); + } + if let Err(error) = switchyard_desktop_install::execute(&settings, &action, false) { + eprintln!("{error}"); + std::process::exit(1); + } + } +} + +// Native command fixtures replace build and service commands without invoking a shell. +struct Fixture { + _temp: tempfile::TempDir, + settings: Settings, + bin: PathBuf, + metadata: PathBuf, + health: HealthFixture, +} +// The local health server wakes its listener during cleanup so failed assertions cannot hang tests. +struct HealthFixture { + port: u16, + stop: std::sync::Arc, + thread: Option>, +} +impl HealthFixture { + fn new() -> Self { + let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("health socket"); + let port = listener.local_addr().expect("address").port(); + let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); + let flag = stop.clone(); + let thread = std::thread::spawn(move || { + use std::io::{Read, Write}; + for connection in listener.incoming() { + if flag.load(std::sync::atomic::Ordering::Relaxed) { + break; + } + if let Ok(mut stream) = connection { + stream + .set_read_timeout(Some(std::time::Duration::from_secs(1))) + .expect("timeout"); + let mut request = [0; 1024]; + let _ = stream.read(&mut request); + let body = "{\"status\":\"ok\"}"; + let _ = write!( + stream, + "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", + body.len(), + body + ); + } + } + }); + Self { + port, + stop, + thread: Some(thread), + } + } +} +impl Drop for HealthFixture { + fn drop(&mut self) { + self.stop.store(true, std::sync::atomic::Ordering::Relaxed); + let _ = std::net::TcpStream::connect(("127.0.0.1", self.port)); + if let Some(thread) = self.thread.take() { + thread.join().expect("health cleanup"); + } + } +} +impl Fixture { + fn new() -> Self { + let temp = tempfile::tempdir().expect("fixture directory"); + let home = temp.path().join("home"); + let bin = temp.path().join("bin"); + fs::create_dir_all(&home).expect("home"); + fs::create_dir_all(&bin).expect("bin"); + // These characters must remain literal in TOML, plist, and systemd arguments. + let source = temp.path().join("checkout with ' $() & "); + fs::create_dir_all(&source).expect("checkout"); + let fixture_src = temp.path().join("fixture.rs"); + fs::write(&fixture_src, include_str!("fixture.txt")).expect("fixture source"); + let status = Command::new("rustc") + .arg(&fixture_src) + .arg("-o") + .arg(bin.join("cargo")) + .status() + .expect("compile fixture"); + assert!(status.success()); + for name in ["id", "launchctl", "codesign", "systemctl"] { + fs::copy(bin.join("cargo"), bin.join(name)).expect("copy native command fixture"); + } + let health = HealthFixture::new(); + let s = Settings { + app_dir: None, + sy_home: home.join("Switchyard ' $(touch unsafe) \\ & % $"), + codex_home: home.join(".codex"), + service_dir: if cfg!(target_os = "macos") { + home.join("Library/LaunchAgents") + } else { + home.join(".config/systemd/user") + }, + home, + source, + cargo: bin.join("cargo"), + port: health.port, + profile: "team-dev".into(), + model: "composite-gpt-6-sol-gpt-6-luna".into(), + }; + let metadata = temp.path().join("install.toml"); + let f = Self { + _temp: temp, + settings: s, + bin, + metadata, + health, + }; + f.save(); + f + } + fn save(&self) { + fs::write( + &self.metadata, + toml::to_string(&self.settings).expect("serialize settings"), + ) + .expect("write metadata"); + } + fn run(&self, action: &str, fail_validation: bool) -> std::process::Output { + let path = std::env::join_paths(std::iter::once(self.bin.clone()).chain( + std::env::split_paths(&std::env::var_os("PATH").expect("PATH")), + )) + .expect("fixture PATH"); + Command::new(std::env::current_exe().expect("test binary")) + .args(["--exact", "fixture_process", "--nocapture"]) + .env("INSTALL_FIXTURE_SETTINGS", &self.metadata) + .env("INSTALL_FIXTURE_ACTION", action) + .env("PATH", path) + .env("HOME", &self.settings.home) + .env( + "INSTALLER_BIN", + env!("CARGO_BIN_EXE_switchyard-desktop-install"), + ) + .env( + "FIXTURE_FAIL_VALIDATE", + if fail_validation { "1" } else { "0" }, + ) + .output() + .expect("run fixture") + } +} + +#[test] +fn reinstall_migrates_settings_preserves_data_and_generates_native_launchers() { + let mut f = Fixture::new(); + let s = &f.settings; + fs::create_dir_all(&s.sy_home).expect("existing settings"); + fs::create_dir_all(&s.codex_home).expect("existing profile"); + fs::write( + s.sy_home.join("menubar.toml"), + "baseline_model = 'custom'\n", + ) + .expect("legacy settings"); + fs::write(s.sy_home.join("routing.jsonl"), "history").expect("history"); + fs::write(s.codex_home.join("config.toml"), "user defaults").expect("user defaults"); + let output = f.run("install", false); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert_eq!( + fs::read_to_string(s.desktop_settings()).expect("settings"), + format!( + "baseline_model = 'custom'\nserver_url = \"http://127.0.0.1:{}\"\n", + s.port + ) + ); + assert!(!s.sy_home.join("menubar.toml").exists()); + let profile = s.codex_home.join("team-dev.config.toml"); + let original = fs::read_to_string(&profile).expect("profile"); + let parsed: toml::Value = toml::from_str(&original).expect("profile TOML"); + assert_eq!(parsed["model"].as_str(), Some(s.model.as_str())); + assert!(parsed.get("approval_policy").is_none()); + assert!(parsed.get("sandbox_mode").is_none()); + fs::write( + s.sy_home.join("composite.toml"), + include_str!("../../../scripts/config/composite.toml"), + ) + .expect("server config"); + f.health = HealthFixture::new(); + f.settings.port = f.health.port; + f.save(); + let output = f.run("install", false); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let s = &f.settings; + assert_eq!( + fs::read_to_string(s.sy_home.join("routing.jsonl")).expect("history"), + "history" + ); + assert_eq!( + fs::read_to_string(s.codex_home.join("config.toml")).expect("user defaults"), + "user defaults" + ); + assert_eq!( + fs::read_to_string(s.sy_home.join("composite.toml")).expect("config"), + include_str!("../../../scripts/config/composite.toml") + ); + let backups: Vec<_> = fs::read_dir(&s.codex_home) + .expect("profiles") + .filter_map(Result::ok) + .filter(|e| { + e.file_name() + .to_string_lossy() + .contains("switchyard-backup") + }) + .collect(); + assert_eq!(backups.len(), 1); + assert_eq!( + fs::read_to_string(backups[0].path()).expect("backup"), + original + ); + let recorded = Settings::load(&s.sy_home.join("install.toml")).expect("saved installation"); + assert_eq!(recorded.port, f.health.port); + assert_eq!(recorded.model, s.model); + assert_eq!(recorded.source, s.source); + if cfg!(target_os = "macos") { + let contents = s.app().join("Contents"); + let info = plist::Value::from_file(contents.join("Info.plist")).expect("Info.plist"); + assert_eq!( + info.as_dictionary().expect("dictionary")["CFBundleExecutable"].as_string(), + Some("switchyard-desktop") + ); + let icon = info.as_dictionary().expect("dictionary")["CFBundleIconFile"] + .as_string() + .expect("bundle icon name"); + let bytes = fs::read(contents.join("Resources").join(icon)).expect("bundle icon"); + assert_eq!( + bytes, + include_bytes!("../../switchyard-desktop/icons/icon.icns") + ); + assert_eq!(&bytes[..4], b"icns"); + assert_eq!( + u32::from_be_bytes(bytes[4..8].try_into().expect("icon size")) as usize, + bytes.len() + ); + assert!(contents.join("MacOS/switchyard-desktop").is_file()); + assert!(!contents.join("MacOS/Switchyard").exists()); + assert!(!contents.join("Resources/Update.command").exists()); + let metadata = + switchyard_desktop_install::bundle_metadata(&contents.join("MacOS/switchyard-desktop")); + assert_eq!( + Settings::load(&metadata).expect("bundle settings").sy_home, + s.sy_home + ); + let job = plist::Value::from_file(s.service_dir.join("com.nvidia.switchyard.server.plist")) + .expect("server plist"); + let args = job.as_dictionary().expect("job")["ProgramArguments"] + .as_array() + .expect("args"); + assert_eq!( + args[0].as_string(), + Some( + s.sy_home + .join("bin/switchyard-desktop-install") + .to_str() + .expect("UTF-8") + ) + ); + assert!( + !s.service_dir + .join("com.nvidia.switchyard.menubar.plist") + .exists() + ); + } else { + let unit = fs::read_to_string(s.service_dir.join("switchyard.service")).expect("unit"); + assert!(unit.contains("%% $$")); + } + assert!(!s.home.join("unsafe").exists()); +} + +// A failed server check must prevent the installer from creating app, service, or settings files. +#[test] +fn validator_failure_leaves_installation_and_profile_untouched() { + let f = Fixture::new(); + let output = f.run("install", true); + assert!(!output.status.success()); + assert!(!f.settings.sy_home.exists()); + assert!(!f.settings.codex_home.exists()); + assert!(!f.settings.app().exists()); + assert!(!f.settings.service_dir.exists()); +} + +#[test] +fn uninstall_restores_snapshot_with_a_recovery_copy_and_keeps_history() { + let f = Fixture::new(); + assert!(f.run("install", false).status.success()); + let s = &f.settings; + fs::write(s.codex_home.join("config.toml"), "current settings").expect("current"); + fs::write(s.codex_home.join("config.toml.direct"), "original settings").expect("direct"); + fs::write(s.sy_home.join("routing.jsonl"), "history").expect("history"); + fs::write(s.home.join(".bashrc"), "before\n# >>> switchyard codex alias >>>\nalias codex=old\n# <<< switchyard codex alias <<<\nafter\n").expect("legacy rc"); + let output = f.run("uninstall", false); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert_eq!( + fs::read_to_string(s.codex_home.join("config.toml")).expect("restored"), + "original settings" + ); + let backups: Vec<_> = fs::read_dir(&s.codex_home) + .expect("configs") + .filter_map(Result::ok) + .filter(|e| { + e.file_name() + .to_string_lossy() + .contains("switchyard-current") + }) + .collect(); + assert_eq!(backups.len(), 1); + assert_eq!( + fs::read_to_string(backups[0].path()).expect("recovery"), + "current settings" + ); + assert_eq!( + fs::read_to_string(s.sy_home.join("routing.jsonl")).expect("history"), + "history" + ); + assert_eq!( + fs::read_to_string(s.home.join(".bashrc")).expect("rc"), + "before\nafter\n" + ); + assert!(!s.codex_home.join("team-dev.config.toml").exists()); + assert!(!s.app().exists()); + assert!(f.run("uninstall", false).status.success()); + let unfinished = "before\n# >>> switchyard codex alias >>>\nalias codex=old\nafter\n"; + fs::write(s.home.join(".bashrc"), unfinished).expect("unfinished block"); + assert!(!f.run("uninstall", false).status.success()); + assert_eq!( + fs::read_to_string(s.home.join(".bashrc")).expect("unchanged rc"), + unfinished + ); +} + +#[test] +fn invalid_inputs_and_dry_run_have_no_installation_side_effects() { + let f = Fixture::new(); + // Fixture commands come first on PATH so a validation regression cannot stop real services. + let path = std::env::join_paths(std::iter::once(f.bin.clone()).chain(std::env::split_paths( + &std::env::var_os("PATH").expect("PATH"), + ))) + .expect("fixture PATH"); + let binary = env!("CARGO_BIN_EXE_switchyard-desktop-install"); + for args in [ + vec!["capture"], + vec![ + "validate", + "--server", + "relative", + "--config", + "/candidate", + "--scope", + "/original", + ], + vec![ + "capture", + "--server", + "/missing", + "--config", + "/candidate", + "--scope", + "/original", + "extra", + ], + vec![ + "validate", + "--server", + "/missing", + "--config", + "/candidate\n", + "--scope", + "/original", + ], + ] { + let output = Command::new(binary) + .args(args) + .env("PATH", &path) + .env("HOME", &f.settings.home) + .output() + .expect("reject broker input"); + assert!(!output.status.success()); + assert_eq!(fs::read_dir(&f.settings.home).expect("home").count(), 0); + } + for (key, value) in [ + ("SY_PROFILE", "../escape"), + ("SY_PROFILE", "team.dev"), + ("SY_PROFILE", ""), + ("SY_MODEL", ""), + ("SY_MODEL", "model\nname"), + ("SY_MODEL", "model\x7fname"), + ("SY_PORT", "0"), + ("SY_PORT", "65536"), + ("SY_PORT", "+4123"), + ("SY_PORT", "4123\n"), + ] { + let output = Command::new(binary) + .arg("install") + .env("PATH", &path) + .env("HOME", &f.settings.home) + .env(key, value) + .output() + .expect("reject input"); + assert!(!output.status.success(), "{key}={value:?}"); + assert!(String::from_utf8_lossy(&output.stderr).contains(key)); + } + for key in ["SY_PROFILE", "SY_MODEL", "SY_PORT"] { + use std::os::unix::ffi::OsStringExt; + let output = Command::new(binary) + .arg("install") + .env("PATH", &path) + .env("HOME", &f.settings.home) + .env(key, std::ffi::OsString::from_vec(vec![0xff])) + .output() + .expect("reject raw input"); + assert!(!output.status.success(), "{key}"); + assert!(String::from_utf8_lossy(&output.stderr).contains(key)); + } + for args in [ + vec!["install", "--dryrun"], + vec!["uninstall", "extra"], + vec!["install", "--dry-run", "extra"], + ] { + assert!( + !Command::new(binary) + .args(args) + .env("PATH", &path) + .env("HOME", &f.settings.home) + .output() + .expect("reject args") + .status + .success() + ); + } + for action in ["install", "uninstall"] { + assert!( + Command::new(binary) + .args([action, "--dry-run"]) + .env("PATH", &path) + .env("HOME", &f.settings.home) + .output() + .expect("preview") + .status + .success() + ); + } + assert_eq!(fs::read_dir(&f.settings.home).expect("home").count(), 0); + let text = fs::read_to_string(&f.metadata).expect("metadata"); + let selected = f.metadata.to_str().expect("settings path"); + for args in [ + ["uninstall", "--settings", selected, "--dry-run"], + ["uninstall", "--dry-run", "--settings", selected], + ] { + let output = Command::new(binary) + .args(args) + .env("PATH", &path) + .env("HOME", &f.settings.home) + .output() + .expect("preview selected removal"); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert!(String::from_utf8_lossy(&output.stdout).contains("team-dev")); + assert_eq!( + fs::read_to_string(&f.metadata).expect("unchanged metadata"), + text + ); + assert_eq!( + fs::read_dir(&f.settings.home) + .expect("unchanged home") + .count(), + 0 + ); + } + fs::write(&f.metadata, format!("{text}\nunknown_field = true\n")).expect("invalid metadata"); + assert!(Settings::load(&f.metadata).is_err()); +} + +#[test] +fn updater_has_a_separate_process_group() { + let f = Fixture::new(); + fs::create_dir_all(f.settings.sy_home.join("bin")).expect("installed binaries"); + fs::create_dir_all(f.settings.sy_home.join("logs")).expect("installed logs"); + fs::copy( + f.bin.join("cargo"), + f.settings.sy_home.join("bin/switchyard-desktop-install"), + ) + .expect("updater fixture"); + let parent = Command::new("ps") + .args(["-o", "pgid=", "-p", &std::process::id().to_string()]) + .output() + .expect("parent process group"); + assert!(parent.status.success()); + let log = switchyard_desktop_install::start_update(&f.metadata).expect("start updater"); + assert!(switchyard_desktop_install::start_update(&f.metadata).is_err()); + let mut group = String::new(); + for _ in 0..100 { + group = fs::read_to_string(&log).expect("update log"); + if !group.trim().is_empty() { + break; + } + std::thread::sleep(std::time::Duration::from_millis(20)); + } + assert!(!group.trim().is_empty()); + assert_ne!( + group.trim(), + String::from_utf8(parent.stdout) + .expect("parent group") + .trim() + ); +} + +// Install and uninstall must use the same lock before stopping services or replacing files. +#[test] +fn concurrent_install_and_uninstall_reject_before_changing_managed_files() { + let f = Fixture::new(); + let lock = fs::OpenOptions::new() + .create(true) + .truncate(false) + .read(true) + .write(true) + .open(f.settings.home.join(".switchyard-desktop-install.lock")) + .expect("lock file"); + lock.lock().expect("hold installation lock"); + for action in ["install", "uninstall"] { + let output = f.run(action, false); + assert!(!output.status.success()); + assert!( + String::from_utf8_lossy(&output.stderr).contains("Could not lock"), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let calls = fs::read_to_string(f.settings.home.join("service_calls")).unwrap_or_default(); + assert!( + !calls.contains("bootout") + && !calls.contains("bootstrap") + && !calls.contains("stop") + && !calls.contains("disable"), + "{calls}" + ); + assert!(!f.settings.sy_home.exists()); + assert!(!f.settings.codex_home.exists()); + assert!(!f.settings.app().exists()); + assert!(!f.settings.service_dir.exists()); + } +} + +#[test] +fn unknown_route_rejects_before_services_or_profiles_change() { + let mut f = Fixture::new(); + f.settings.model = "missing-public-route".into(); + f.save(); + let output = f.run("install", false); + assert!(!output.status.success()); + assert!(String::from_utf8_lossy(&output.stderr).contains("missing-public-route")); + assert!(!f.settings.codex_home.exists()); + assert!(!f.settings.app().exists()); + assert!(!f.settings.desktop_settings().exists()); + let calls = fs::read_to_string(f.settings.home.join("service_calls")).unwrap_or_default(); + assert!(!calls.contains("bootout") && !calls.contains("bootstrap") && !calls.contains("stop")); +} +#[test] +fn source_update_preserves_profile_bytes_and_uninstall_keeps_recovery() { + let f = Fixture::new(); + assert!(f.run("install", false).status.success()); + let profile = f.settings.codex_home.join("team-dev.config.toml"); + let current = "# Personal settings\nmodel='custom-selection'\nexperimental='retained'\n"; + fs::write(&profile, current).expect("edited profile"); + let settings = f.settings.desktop_settings(); + // Mixed line endings and single quotes expose a rewrite even when the parsed settings stay the same. + let retained_settings = format!( + "# Personal settings\r\nserver_url = 'http://127.0.0.1:{}'\nbaseline_model='custom'\r\n[prices.custom]\r\ninput_per_mtok=9.25\noutput_per_mtok=12.5\r\n", + f.settings.port + ); + fs::write(&settings, &retained_settings).expect("edited settings"); + let output = f.run("update", false); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert_eq!(fs::read_to_string(&profile).expect("profile"), current); + assert_eq!( + fs::read(&settings).expect("settings"), + retained_settings.as_bytes() + ); + assert_eq!( + switchyard_desktop_install::update_status(&f.metadata).expect("status")["state"], + "completed" + ); + assert!(f.run("uninstall", false).status.success()); + assert!(!profile.exists()); + let copies: Vec<_> = fs::read_dir(&f.settings.codex_home) + .expect("profiles") + .filter_map(Result::ok) + .filter(|entry| { + entry + .file_name() + .to_string_lossy() + .contains("switchyard-uninstall-recovery") + }) + .collect(); + assert_eq!(copies.len(), 1); + assert_eq!( + fs::read_to_string(copies[0].path()).expect("recovery"), + current + ); +} +#[test] +fn malformed_profile_table_rejects_before_service_changes() { + let f = Fixture::new(); + fs::create_dir_all(&f.settings.codex_home).expect("codex"); + let profile = f.settings.codex_home.join("team-dev.config.toml"); + let original = "model_providers='bad table'\n"; + fs::write(&profile, original).expect("profile"); + let output = f.run("install", false); + assert!(!output.status.success()); + assert!(String::from_utf8_lossy(&output.stderr).contains("model_providers must be a table")); + assert_eq!( + fs::read_to_string(profile).expect("unchanged profile"), + original + ); + assert!(!f.settings.home.join("service_calls").exists()); +} + +#[test] +// The installed updater must finish after an old running record and refuse a second start. +fn updater_child_completes_through_the_installed_entrypoint() { + let f = Fixture::new(); + fs::create_dir_all(f.settings.sy_home.join("bin")).expect("installed binaries"); + fs::create_dir_all(f.settings.sy_home.join("logs")).expect("installed logs"); + fs::copy( + env!("CARGO_BIN_EXE_switchyard-desktop-install"), + f.settings.sy_home.join("bin/switchyard-desktop-install"), + ) + .expect("installed updater"); + fs::write( + f.settings.sy_home.join("update-status.toml"), + "state='running'\nupdated_at=0\n", + ) + .expect("interrupted update"); + let output = f.run("start_update", false); + assert!( + output.status.success(), + "{}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + assert_eq!( + switchyard_desktop_install::update_status(&f.metadata).expect("final status")["state"], + "completed" + ); +} + +#[test] +// A non-string server_url must not be replaced with a managed URL during installation. +fn malformed_app_address_rejects_without_replacing_settings() { + let f = Fixture::new(); + fs::create_dir_all(&f.settings.sy_home).expect("settings directory"); + let path = f.settings.desktop_settings(); + let original = "server_url=42\n# user settings\n"; + fs::write(&path, original).expect("invalid address"); + let output = f.run("install", false); + assert!(!output.status.success()); + assert_eq!( + fs::read_to_string(path).expect("preserved settings"), + original + ); + assert!(!f.settings.codex_home.join("team-dev.config.toml").exists()); + let calls = fs::read_to_string(f.settings.home.join("service_calls")).expect("preflight"); + assert!( + !calls.contains("bootout") && !calls.contains("disable") && !calls.contains("bootstrap") + ); +} diff --git a/crates/switchyard-desktop-install/tests/windows-update-fixture.txt b/crates/switchyard-desktop-install/tests/windows-update-fixture.txt new file mode 100644 index 0000000000..58fefc7337 --- /dev/null +++ b/crates/switchyard-desktop-install/tests/windows-update-fixture.txt @@ -0,0 +1,12 @@ +use std::{env, fs, path::Path, thread, time::Duration}; + +fn main() { + let mut args = env::args_os().skip(1); + let metadata = args.next().expect("metadata"); + let started = args.next().expect("marker"); + switchyard_desktop_install::start_update(Path::new(&metadata)).expect("update"); + fs::write(started, b"started").expect("marker"); + // The helper stays alive until the updater stops the desktop task that owns this process. + thread::sleep(Duration::from_secs(180)); + std::process::exit(1); +} diff --git a/crates/switchyard-desktop-install/tests/windows.rs b/crates/switchyard-desktop-install/tests/windows.rs new file mode 100644 index 0000000000..ee972460d7 --- /dev/null +++ b/crates/switchyard-desktop-install/tests/windows.rs @@ -0,0 +1,505 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +#![cfg(windows)] + +use std::{ + fs, + io::{Read, Write}, + net::TcpStream, + path::Path, + process::Command, + time::{Duration, Instant}, +}; +use switchyard_desktop_install::{Settings, execute, update_status, windows}; + +fn wait(mut condition: impl FnMut() -> bool) { + let deadline = Instant::now() + Duration::from_secs(180); + while !condition() { + assert!( + Instant::now() < deadline, + "Windows operation did not finish within three minutes." + ); + std::thread::sleep(Duration::from_millis(250)); + } +} +fn health(port: u16) -> bool { + let Ok(mut socket) = TcpStream::connect_timeout( + &format!("127.0.0.1:{port}").parse().expect("address"), + Duration::from_secs(1), + ) else { + return false; + }; + socket + .set_read_timeout(Some(Duration::from_secs(1))) + .expect("timeout"); + if socket + .write_all(b"GET /health HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n") + .is_err() + { + return false; + } + let mut response = String::new(); + socket.read_to_string(&mut response).is_ok() && response.starts_with("HTTP/1.1 200") +} +fn task_xml(s: &Settings, desktop: bool) -> String { + let output = Command::new("schtasks.exe") + .args(["/Query", "/TN", &windows::task_name(s, desktop), "/XML"]) + .output() + .expect("query task"); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + // schtasks emits UTF-16 XML when stdout is redirected. + if output.stdout.starts_with(&[0xff, 0xfe]) || output.stdout.contains(&0) { + let bytes = output + .stdout + .strip_prefix(&[0xff, 0xfe]) + .unwrap_or(&output.stdout); + String::from_utf16( + &bytes + .as_chunks::<2>() + .0 + .iter() + .map(|c| u16::from_le_bytes([c[0], c[1]])) + .collect::>(), + ) + .expect("task XML") + } else { + String::from_utf8(output.stdout).expect("task XML") + } +} +fn desktop_running(binary: &Path) -> bool { + // Windows may report an 8.3 path; canonical paths still identify the exact installed executable. + let Ok(binary) = fs::canonicalize(binary) else { + return false; + }; + // PowerShell is used only to observe the test process; installation and launch stay in Rust. + let output=Command::new("powershell.exe").args(["-NoProfile","-Command", "[Console]::OutputEncoding=[Text.UTF8Encoding]::new(); Get-Process switchyard-desktop -ErrorAction SilentlyContinue | ForEach-Object { $_.Path }"]).output().expect("query desktop"); + String::from_utf8_lossy(&output.stdout) + .lines() + .any(|p| fs::canonicalize(p.trim()).is_ok_and(|actual| actual == binary)) +} +// The helper starts the real updater from the desktop task, retaining its principal and logon settings. +fn start_scheduled_update(s: &Settings, metadata: &Path, fixture: &Path) { + let source = fixture.join("update-context.rs"); + let helper = fixture.join("update-context.exe"); + let started = fixture.join("update-context-started"); + fs::write(&source, include_str!("windows-update-fixture.txt")).expect("helper source"); + let output = Command::new(s.cargo.parent().expect("toolchain").join("rustc.exe")) + .args(["--edition=2024", "-Cpanic=abort", "--extern"]) + .arg(format!( + "switchyard_desktop_install={}", + s.source + .join("target/release/libswitchyard_desktop_install.rlib") + .display() + )) + .arg("-L") + .arg(format!( + "dependency={}", + s.source.join("target/release/deps").display() + )) + .arg(&source) + .arg("-o") + .arg(&helper) + .output() + .expect("compile update helper"); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let mut xml = tempfile::NamedTempFile::new().expect("task XML"); + xml.write_all(&[0xff, 0xfe]).expect("XML BOM"); + for word in task_xml(s, true).encode_utf16() { + xml.write_all(&word.to_le_bytes()).expect("XML"); + } + let xml = xml.into_temp_path(); + let arguments = format!( + "{} {}", + windows::quote_arg(metadata.to_str().expect("metadata path")), + windows::quote_arg(started.to_str().expect("marker path")) + ); + let output = Command::new("powershell.exe") + .args(["-NoProfile", "-Command", "$ErrorActionPreference='Stop'; $xml=[xml](Get-Content -LiteralPath $env:SWITCHYARD_CONTEXT_XML -Raw); $xml.Task.Actions.Exec.Command=$env:SWITCHYARD_CONTEXT_HELPER; $xml.Task.Actions.Exec.Arguments=$env:SWITCHYARD_CONTEXT_ARGUMENTS; $xml.Save($env:SWITCHYARD_CONTEXT_XML)"]) + .env("SWITCHYARD_CONTEXT_XML", &xml) + .env("SWITCHYARD_CONTEXT_HELPER", &helper) + .env("SWITCHYARD_CONTEXT_ARGUMENTS", arguments) + .output() + .expect("set task action"); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let task = windows::task_name(s, true); + for args in [ + vec!["/End", "/TN", &task], + vec![ + "/Create", + "/F", + "/TN", + &task, + "/XML", + xml.to_str().expect("XML path"), + ], + vec!["/Run", "/TN", &task], + ] { + let output = Command::new("schtasks.exe") + .args(&args) + .output() + .expect("scheduled update"); + assert!( + output.status.success(), + "{args:?}: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + // This marker prevents a previous completed update from satisfying the next status check. + wait(|| started.is_file()); +} +// Cleanup preserves logs and removes test tasks and executables even after an assertion fails. +struct Cleanup<'a>(&'a Settings); +impl Drop for Cleanup<'_> { + fn drop(&mut self) { + if let Some(destination) = std::env::var_os("SWITCHYARD_WINDOWS_LOG_DIR") { + let destination = std::path::PathBuf::from(destination); + let _ = fs::create_dir_all(&destination); + for name in ["update.log", "desktop.log", "server.log"] { + let _ = fs::copy( + self.0.sy_home.join("logs").join(name), + destination.join(name), + ); + } + } + let _ = windows::stop(self.0); + let _ = windows::uninstall(self.0); + } +} + +#[test] +#[ignore = "Registers real per-user tasks and builds the release app; run explicitly on a Windows test machine."] +fn fresh_install_running_update_restart_and_uninstall_preserve_user_data() { + let root = tempfile::Builder::new() + .prefix("Switchyard équipe space ") + .tempdir() + .expect("fixture"); + let home = root.path().join("User équipe space"); + fs::create_dir_all(&home).expect("home"); + let port = std::net::TcpListener::bind("127.0.0.1:0") + .expect("port") + .local_addr() + .expect("address") + .port(); + let defaults = Settings::from_env().expect("defaults"); + let s = Settings { + home: home.clone(), + sy_home: home.join(".switchyard"), + codex_home: home.join(".codex"), + service_dir: home.join("AppData/Roaming/Microsoft/Windows/Start Menu/Programs"), + app_dir: Some(home.join("AppData/Local/Programs/Switchyard")), + port, + ..defaults + }; + let _cleanup = Cleanup(&s); + execute(&s, "install", true).expect("preview"); + assert!(!s.sy_home.exists()); + execute(&s, "install", false).expect("fresh install"); + assert!(health(port), "server failed readiness"); + wait(|| desktop_running(&s.binary("switchyard-desktop"))); + for desktop in [false, true] { + let xml = task_xml(&s, desktop); + // The registered task's COM principal reports its effective privilege and logon settings. + let output = Command::new("powershell.exe") + .args(["-NoProfile", "-Command", r"$ErrorActionPreference='Stop'; $scheduler=New-Object -ComObject Schedule.Service; $scheduler.Connect(); $principal=$scheduler.GetFolder('\').GetTask($env:SWITCHYARD_TEST_TASK).Definition.Principal; @{run_level=[int]$principal.RunLevel; logon_type=[int]$principal.LogonType} | ConvertTo-Json -Compress"]) + .env("SWITCHYARD_TEST_TASK", windows::task_name(&s, desktop)) + .output() + .expect("task principal"); + assert!( + output.status.success(), + "{}\n{xml}", + String::from_utf8_lossy(&output.stderr) + ); + let principal: serde_json::Value = + serde_json::from_slice(&output.stdout).expect("principal JSON"); + // Task Scheduler defines LUA privileges as 0 and an interactive-token logon as 3. + assert_eq!(principal["run_level"], 0, "{principal}\n{xml}"); + assert_eq!(principal["logon_type"], 3, "{principal}\n{xml}"); + assert!( + xml.contains("LogonTrigger") && xml.contains("PT0S"), + "{xml}" + ); + } + assert!(s.service_dir.join("Switchyard.lnk").is_file()); + let settings = s.desktop_settings(); + let profile = s.codex_home.join("sy.config.toml"); + let original_settings = fs::read(&settings).expect("settings"); + let original_profile = fs::read(&profile).expect("profile"); + let history = s.sy_home.join("routing.jsonl"); + fs::write(&history, b"\n").expect("history"); + let accounts = s.home.join(".switchyard/accounts/codex/team"); + fs::create_dir_all(&accounts).expect("account"); + fs::write(accounts.join("auth.json"), b"preserved-login-fixture").expect("login fixture"); + let metadata = s.sy_home.join("install.toml"); + start_scheduled_update(&s, &metadata, root.path()); + wait(|| { + let status = update_status(&metadata).expect("update status"); + assert_ne!( + status["state"], + "failed", + "{status}\n{}", + fs::read_to_string(s.sy_home.join("logs/update.log")).unwrap_or_default() + ); + status["state"] == "completed" + }); + assert!(health(port)); + wait(|| desktop_running(&s.binary("switchyard-desktop"))); + assert_eq!( + fs::read(&settings).expect("retained settings"), + original_settings + ); + assert_eq!( + fs::read(&profile).expect("retained profile"), + original_profile + ); + assert_eq!(fs::read(&history).expect("history"), b"\n"); + windows::restart(&s).expect("restart"); + wait(|| health(port)); + // The Cargo alias must rebuild and reinstall from its checkout while the release app is running. + let status = Command::new(&s.cargo) + .current_dir(&s.source) + .args(["desktop", "install"]) + .env("HOME", &s.home) + .env("USERPROFILE", &s.home) + .env("SY_HOME", &s.sy_home) + .env("CODEX_HOME", &s.codex_home) + .env("SY_PORT", s.port.to_string()) + .env("APPDATA", s.home.join("AppData/Roaming")) + .env("LOCALAPPDATA", s.home.join("AppData/Local")) + .status() + .expect("cargo desktop install"); + assert!(status.success()); + wait(|| { + let status = update_status(&metadata).expect("reinstall status"); + assert_ne!(status["state"], "failed", "{status}"); + status["state"] == "completed" + }); + wait(|| health(port)); + execute(&s, "uninstall", false).expect("uninstall"); + assert!(!health(port)); + assert!(!desktop_running(&s.binary("switchyard-desktop"))); + assert!(!s.bin_dir().exists()); + for desktop in [false, true] { + assert!( + !Command::new("schtasks.exe") + .args(["/Query", "/TN", &windows::task_name(&s, desktop)]) + .output() + .expect("removed task query") + .status + .success() + ); + } + assert!(!s.service_dir.join("Switchyard.lnk").exists()); + assert_eq!(fs::read(&settings).expect("settings"), original_settings); + assert_eq!(fs::read(&history).expect("history"), b"\n"); + assert_eq!( + fs::read(accounts.join("auth.json")).expect("account"), + b"preserved-login-fixture" + ); + assert!(fs::read_dir(&s.codex_home).expect("recovery").any(|f| { + f.expect("file") + .file_name() + .to_string_lossy() + .contains("switchyard-uninstall-recovery") + })); + execute(&s, "uninstall", false).expect("repeat uninstall"); +} + +#[test] +fn executable_replacement_rolls_back_when_another_process_locks_a_file() { + use std::os::windows::fs::OpenOptionsExt; + let root = tempfile::tempdir().expect("fixture"); + let defaults = Settings::from_env().expect("defaults"); + let s = Settings { + home: root.path().join("home"), + sy_home: root.path().join("settings"), + codex_home: root.path().join("codex"), + service_dir: root.path().join("shortcuts"), + source: root.path().join("source"), + app_dir: Some(root.path().join("home/AppData/Local/Programs/Switchyard")), + ..defaults + }; + fs::create_dir_all(s.source.join("target/release")).expect("release fixtures"); + fs::create_dir_all(s.bin_dir()).expect("binaries"); + for name in [ + "switchyard-server", + "switchyard-desktop", + "switchyard-desktop-install", + ] { + fs::write( + s.source + .join("target/release") + .join(switchyard_desktop_install::executable_name(name)), + b"new", + ) + .expect("new binary"); + fs::write(s.binary(name), b"old").expect("old binary"); + } + // Locking the second executable requires rollback of the first replacement before a retry. + let lock = fs::OpenOptions::new() + .read(true) + .share_mode(0) + .open(s.binary("switchyard-desktop")) + .expect("exclusive lock"); + assert!(windows::replace_binaries(&s).is_err()); + drop(lock); + for name in [ + "switchyard-server", + "switchyard-desktop", + "switchyard-desktop-install", + ] { + assert_eq!( + fs::read(s.binary(name)).expect("previous executable"), + b"old" + ); + } + windows::replace_binaries(&s).expect("retry after releasing lock"); + for name in [ + "switchyard-server", + "switchyard-desktop", + "switchyard-desktop-install", + ] { + assert_eq!(fs::read(s.binary(name)).expect("new executable"), b"new"); + } +} + +#[test] +fn credential_manager_stores_only_the_exact_endpoint_and_survives_a_read() { + use std::os::windows::ffi::OsStrExt; + use windows_sys::Win32::Security::Credentials::{CRED_TYPE_GENERIC, CredDeleteW}; + struct Credential(String); + impl Drop for Credential { + fn drop(&mut self) { + let name: Vec = std::ffi::OsStr::new(&format!("Switchyard:model-list:{}", self.0)) + .encode_wide() + .chain(Some(0)) + .collect(); + unsafe { + CredDeleteW(name.as_ptr(), CRED_TYPE_GENERIC, 0); + } + } + } + let endpoint = Credential(format!( + "http://localhost/windows-key-fixture-{}-{}", + std::process::id(), + fixture_timestamp() + )); + assert_eq!( + windows::saved_key(&endpoint.0).expect("missing credential"), + None + ); + windows::save_key(&endpoint.0, "test-key-only").expect("save credential"); + assert_eq!( + windows::saved_key(&endpoint.0).expect("read credential"), + Some("test-key-only".into()) + ); + assert_eq!( + windows::saved_key(&format!("{}/other", endpoint.0)).expect("different endpoint"), + None + ); +} +fn fixture_timestamp() -> u128 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("timestamp") + .as_nanos() +} + +#[test] +fn invalid_windows_install_locations_reject_before_creating_files() { + use std::os::windows::ffi::OsStringExt; + let root = tempfile::tempdir().expect("fixture"); + let home = root.path().join("home"); + fs::create_dir(&home).expect("home"); + for location in [ + std::ffi::OsString::from("relative"), + std::ffi::OsString::from("C:\\invalid\npath"), + std::ffi::OsString::from_wide(&[b'C' as u16, b':' as u16, b'\\' as u16, 0xd800]), + ] { + let output = Command::new(env!("CARGO_BIN_EXE_switchyard-desktop-install")) + .args(["install", "--dry-run"]) + .env("HOME", &home) + .env("USERPROFILE", &home) + .env("LOCALAPPDATA", location) + .output() + .expect("invalid location"); + assert!(!output.status.success()); + assert!(String::from_utf8_lossy(&output.stderr).contains("absolute UTF-8 path")); + assert_eq!(fs::read_dir(&home).expect("unchanged home").count(), 0); + } +} + +#[test] +fn malformed_private_handoff_records_are_retained_without_dispatch() { + let root = tempfile::tempdir().expect("fixture"); + let defaults = Settings::from_env().expect("defaults"); + let s = Settings { + home: root.path().join("home"), + sy_home: root.path().join("settings"), + codex_home: root.path().join("codex"), + service_dir: root.path().join("shortcuts"), + app_dir: Some(root.path().join("app")), + ..defaults + }; + fs::create_dir(&s.home).expect("home"); + let plain = toml::to_string(&s).expect("settings"); + let wrapped = format!("[settings]\n{plain}"); + for (text, pid, reason) in [ + (plain, "1", "unknown"), + (format!("unknown=true\n{wrapped}"), "1", "unknown"), + (wrapped.clone(), "0", "parent process"), + ] { + let record = tempfile::Builder::new() + .prefix("switchyard-handoff-") + .suffix(".toml") + .tempfile() + .expect("private record"); + fs::write(record.path(), &text).expect("record"); + let output = Command::new(env!("CARGO_BIN_EXE_switchyard-desktop-install")) + .arg("apply") + .arg("--handoff-record") + .arg(record.path()) + .args(["--action", "uninstall", "--wait", pid]) + .output() + .expect("rejected handoff"); + assert!(!output.status.success()); + assert!( + String::from_utf8_lossy(&output.stderr) + .to_lowercase() + .contains(reason), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert_eq!( + fs::read_to_string(record.path()).expect("record retained"), + text + ); + assert_eq!(fs::read_dir(&s.home).expect("home unchanged").count(), 0); + } + let outside = root.path().join("switchyard-handoff-outside.toml"); + fs::write(&outside, &wrapped).expect("outside record"); + let output = Command::new(env!("CARGO_BIN_EXE_switchyard-desktop-install")) + .arg("apply") + .arg("--handoff-record") + .arg(&outside) + .args(["--action", "uninstall", "--wait", "1"]) + .output() + .expect("outside record"); + assert!(!output.status.success()); + assert_eq!( + fs::read_to_string(outside).expect("outside record retained"), + wrapped + ); +} diff --git a/crates/switchyard-desktop/.gitignore b/crates/switchyard-desktop/.gitignore new file mode 100644 index 0000000000..2c41ac3ead --- /dev/null +++ b/crates/switchyard-desktop/.gitignore @@ -0,0 +1 @@ +/gen/ diff --git a/crates/switchyard-desktop/Cargo.toml b/crates/switchyard-desktop/Cargo.toml new file mode 100644 index 0000000000..98fb80b731 --- /dev/null +++ b/crates/switchyard-desktop/Cargo.toml @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "switchyard-desktop" +version.workspace = true +description = "Desktop and terminal app for a local Switchyard server" +authors.workspace = true +edition.workspace = true +homepage = "https://github.com/NVIDIA-NeMo/Switchyard" +license.workspace = true +repository.workspace = true +rust-version.workspace = true +publish = false + +[dependencies] +switchyard-desktop-install = { path = "../switchyard-desktop-install" } +chrono = { version = "0.4", default-features = false, features = ["std", "clock", "serde"] } +serde.workspace = true +serde_json.workspace = true +tempfile = "3" +toml = "0.9" +toml_edit = "0.25" +url = "2" +uuid.workspace = true +ratatui = "0.30.2" + +[target.'cfg(target_os = "macos")'.dependencies] +objc2-app-kit = { version = "0.3.2", default-features = false, features = ["NSWorkspace"] } +objc2-foundation = { version = "0.3.2", default-features = false, features = ["NSString", "NSURL"] } +security-framework = "3" +[target.'cfg(any(target_os = "macos", windows))'.dependencies] +tauri = { version = "2", features = ["tray-icon", "custom-protocol"] } +rfd = { version = "0.17", default-features = false } + +[target.'cfg(windows)'.dependencies] +windows = { version = "0.62.2", features = ["Management_Deployment", "ApplicationModel"] } + +[target.'cfg(any(target_os = "macos", windows))'.build-dependencies] +tauri-build = "2" + +[build-dependencies] +switchyard-desktop-icons = { path = "../switchyard-desktop-icons" } + +[dev-dependencies] +# The tests check each edited config with the server's own config parser. +switchyard-runner.workspace = true diff --git a/crates/switchyard-desktop/README.md b/crates/switchyard-desktop/README.md new file mode 100644 index 0000000000..23d2b860e4 --- /dev/null +++ b/crates/switchyard-desktop/README.md @@ -0,0 +1,365 @@ +# Switchyard desktop and terminal apps + +The macOS and Windows desktop apps and terminal app show usage, edit routes, install +coding-tool settings, and open sessions in separate Git worktrees. The server +handles routing and continues when the app quits. The desktop app uses Tauri 2; +the terminal app uses Ratatui. + +## Install and update + +Install the app from the repository root with the repository's pinned Rust +toolchain (currently Rust 1.99). Install Rust through rustup and keep Cargo on +`PATH`. Native dependencies also need a C compiler and linker: Xcode Command +Line Tools on macOS, or GCC/Clang and the system development libraries on Linux. +Cargo downloads dependencies on the first build. The default Codex profile +requires Codex CLI 0.134.0 or newer and a ChatGPT login. Sign in with Codex +before using it. + +```sh +cargo desktop install +``` + +Run the same command to replace an existing installation. The command builds +the current checkout; update the checkout first to install newer code. + +On macOS, the Rust installer builds the desktop app and server, installs +`~/Applications/Switchyard.app`, and loads two per-user LaunchAgents. It signs +the app ad hoc for local use. The app has no Developer ID signature or +notarization. Reinstalling keeps existing server settings, prices, accounts, and +log history. The installer moves older `menubar.toml` settings to `desktop.toml` +when the new file is absent and replaces the old app LaunchAgent. + +The installer builds the binaries and validates the server config before +stopping services. On macOS, it also signs the replacement app before stopping +services. Each file replacement is atomic, but the complete installation is not. +If installation fails after services stop, fix the reported error and run `cargo +desktop install` again. Install and uninstall share a lock for your home +directory. If another installation holds it, wait for that command to finish and +retry. + +By default, the source installer writes `~/.codex/sy.config.toml` for +`codex -p sy` and keeps `~/.codex/config.toml` unchanged. Set `SY_PROFILE` to +choose another profile name. The installer writes `$SY_PROFILE.config.toml` in +the Codex config directory. For example: + +```sh +export SY_PROFILE=composite-gpt-6-sol-gpt-6-luna +cargo desktop install +codex -p "$SY_PROFILE" +``` + +Remove the app and recorded profile with `cargo desktop uninstall`. Uninstall +uses the saved installation location and profile. Conflicting shell overrides +require an explicit `uninstall --settings FILE` selection. Uninstall keeps an +exact recovery copy of the selected profile before removal. Restore coding-tool +routing from **Connect tools** first; custom and account settings remain and +may still point at the removed server. Preview removal of a selected installation +without changing files or services: + +```sh +cargo desktop uninstall --settings FILE --dry-run +``` + +Set `SY_MODEL` to choose an existing public route ID. New installs use +`composite-gpt-6-sol-gpt-6-luna`. Existing server configs stay saved, so set +`SY_MODEL` to one of their routes when installing with a custom config. The +profile leaves `approval_policy` and `sandbox_mode` to your existing Codex +settings. Reinstalling backs up the profile before updating its managed keys. It +preserves unrelated profile settings. Updates from Settings preserve the entire +profile. `SY_PROFILE` must start with an ASCII letter or digit, use only ASCII +letters, digits, underscores, or hyphens, and contain at most 128 characters. +For example, the installer rejects `SY_PROFILE=team.dev` before creating app or +config files. + +Open the app from Finder or Spotlight. Closing the window removes the running +app from the Dock and keeps the tray menu. Opening the window from the tray +restores the Dock icon. A shortcut you pinned to the Dock stays there. +The tray includes **Connect tools…**, the three models with the most recorded +tokens, **View model usage…**, and **Settings…**. Model totals cover the +retained log and include classifier calls. A final row combines any remaining +models. Clicking a model row opens Overview filtered to that model. Choose +**Open Switchyard** to return or **Quit Switchyard** to exit. Quit waits for a +queued or running operation to finish. The server continues running after the +app quits. + +**Settings → Review update…** shows the checkout and planned changes. **Build +and reinstall** starts the Rust installer from the checkout used for this +installation. It builds that checkout's current commit and preserves the current +Codex profile. Review the checkout, local changes, and planned actions before +starting. Settings shows the update stage and outcome. The updater keeps running +when installation stops the desktop app. Only one update runs at a time. +Progress and errors go to `~/.switchyard/logs/update.log` (under `SY_HOME` when +customized). Keep the checkout and a Rust toolchain available. Update the +checkout first to choose the code you want to build. + +Preview installation with `cargo desktop install --dry-run`. On Linux, the same +command installs the terminal app and a systemd user service for the server. +Linux installation requires an active systemd user session. Containers, +SSH sessions without a user service manager, and distributions without systemd need +separate service setup. The repository targets x86-64-v3 CPUs on Linux x86_64 +and Neoverse N1 CPUs on Linux aarch64; older CPUs may not run the binaries. +The desktop GUI runs on macOS and Windows. Windows requires the MSVC build tools, WebView2, and a Task Scheduler user session. +On Linux, the terminal app provides usage views and config editing. Login, +session launch, and open-file actions require macOS or Windows. Run coding-tool +login commands and open files yourself on Linux. Model discovery needs `curl`; +session creation on macOS and Windows also needs Git and the selected coding tool. +With a custom Linux `SY_HOME`, pass the settings file explicitly: + +```sh +"$SY_HOME/bin/switchyard-desktop" "$SY_HOME/desktop.toml" --tui +``` + +Run the terminal app with: + +```sh +~/.switchyard/bin/switchyard-desktop --tui +# This command runs the terminal app from a checkout. +cargo run -p switchyard-desktop -- --tui +``` + +Tab changes sections. The footer lists navigation keys. In Routes, `/` searches +and the arrow keys select matching routes; `d` deletes a route only after you +type its name. In Coding tools, `p` previews a diff, `x` selects a custom file, +and `i` shows the preview before a second `i` installs the route. Restore asks +you to type `RESTORE` before changing coding-tool settings. Changing the tool, +account, or route clears the preview. In Routes, press `e` to edit the algorithm +and model roles. Tab changes form fields, Ctrl-U clears a field, arrow keys +choose algorithms and endpoints, Enter reviews the changes, and Escape cancels. +Route Save, session Launch, Restore, and source Update show a review before +typed confirmation. Cursor keys, Home, End, Delete, and paste edit text. Failed +submissions keep the form and values. The terminal restores its screen and input +mode on exit. `--print` prints the usage summary without opening either app. +`--tui` cannot be combined with `--print` or `--validate-toml`. + +## Routes + +The route library searches public model IDs, algorithms, endpoints, and model +choices. Select a route to edit its algorithm and model roles. The app opens one +editor at a time, even when the config contains hundreds of routes. **Refresh +suggestions** loads the endpoint's list; you can also type an unlisted model ID. +ChatGPT's private Codex endpoint uses the saved Codex model list and configured +targets. **Review and save…** shows the changes before **Save and restart** +applies them. The app keeps drafts during the app session until saving or +**Discard unsaved edits…**. Discarding changes only resets the editor. **Delete +route…** removes the selected route after confirmation, preserves its targets +and endpoint settings, and uses the same validation, backup, and restart as +saving. Refresh keeps drafts and scroll and restores focus to matching form fields. +Drafts belong to their config file. Selecting another file keeps the drafts for +the original file. Switching algorithms preserves each algorithm's full set of choices. + +**Save and restart** asks the installed helper to check the complete edited file +with the installed server's `--dry-run`. The helper reads saved credentials for +the original config file when its environment lacks a declared variable. See +[Server Credentials](../../INSTALLATION.md#server-credentials) for credential +setup and macOS Keychain approval after rebuilding the helper. +The app preserves comments and unrelated tables, saves a backup +beside the config, and restarts the server. A failed check leaves the file +unchanged. A restart failure after saving is reported separately. The app re-reads +the config and rejects concurrent changes detected before replacement. A draft +from a different server config file is rejected before saving. + +Switching algorithms writes the new algorithm's default settings and removes +settings specific to the old algorithm. Targets keep their existing model +settings, such as `extra_body` and `omit_body_fields`; the review names those +settings before saving when a model family changes. Check them if the new +provider rejects requests. Shared targets are copied when an edit would change +another route. + +API keys entered for model lists stay in macOS Keychain or Windows Credential Manager. The app sends keys to +curl through stdin and omits them from frontend snapshots. An endpoint must be +configured to use authentication before it accepts a model-list key. An entered +key applies only to that endpoint's exact `base_url`. On macOS and Windows, submitting a key +always requests a fresh model list; a rejected key is not saved and leaves +the cached list unchanged. Model-list keys do not replace the server's +`api_key_env` credentials. The app and server LaunchAgents do not load shell +startup files. + +On other platforms, the app rejects entered model-list keys; set the environment +variable named by the endpoint's `api_key_env` and make it available to the app +instead. + +## Install coding-tool settings + +**Connect tools** has one coding-tool selector for Codex CLI, Codex app, Claude +Code, and Pi. macOS finds the Codex app by its registered bundle identity, +including copies renamed to `ChatGPT.app`. Windows reads the current user's +`OpenAI.Codex` MSIX registration to find the app. Choose a route and settings location +to compare the current and proposed settings and their exact file paths. +Credential values stay hidden. The preview changes no files. **Apply routing…** +shows a confirmation before saving. Apply compares the reviewed file contents +with the current files and requires a new review if they changed. Progress +appears while the action runs. One dismissible notification shows the result, +with backup paths and diagnostics in **Details**. If the settings already match, +the button reads **Routing already applied**. An optional absolute file path +selects settings in another location. For Pi, choose `models.json`; the app also +uses `settings.json` beside it. A custom file cannot be combined with a saved +account. + +The app's **Connect tools** page defaults to `sy.config.toml` for Codex CLI and +`codex -p sy`. The source installer's `SY_PROFILE` choice does not change this +destination. Select a custom file to update another profile. With the normal +user settings location, the Codex app option changes shared defaults in +`config.toml` for both the app and CLI. A custom file or saved account changes +only that destination; the coding tool must load those settings separately. +Claude Code gets environment settings in `settings.json`. Pi gets a custom +provider in `models.json` and defaults in `settings.json`. Unrelated settings +remain. + +The app backs up each original file as `.switchyard-original`. If a +file did not exist, `.switchyard-original-missing` records its +absence. **Review restore…** lists the files and original backups. **Restore +settings** keeps a copy of each current file, then restores its backup or +removes the file if it was originally absent. The result lists the recovery +copies. A successful restore retires the original backup, so the next +installation backs up your current settings. If backup cleanup fails, retry +Restore before installing another route. + +Install and Restore check every selected file before replacing any file. +Malformed settings and symlink settings files are rejected before writes. Each +replacement is atomic. If a later replacement fails, the app tries to restore +earlier files. A crash can leave Pi's two files out of sync, and rollback can +fail. Another process running as the same user can also change a file after the +app checks it. A failed replacement keeps original backups for manual recovery. +If backup cleanup fails after restoration, the remaining receipts keep the +restored contents available for a retry. Recovery copies remain until you remove +them. + +Caller login routes require a matching coding tool and provider: Codex for +ChatGPT or Claude Code for Anthropic. Pi requires a route whose API credentials +the server owns. Configure those credentials on the server before installing +the route. Claude settings with a user API key or bearer token must be cleared +before installing a subscription route. Switching from an API route installed +by Switchyard removes its placeholder credentials only when the existing model +settings and local endpoint match that installation. If the local endpoint has +changed, restore the original settings first. Restart the coding tool after +changing its defaults. Project settings, shell variables, and command-line +options can override them; check Claude's `/status` for shell overrides. + +For older installs that replaced `config.toml`, `cargo desktop uninstall` restores +`config.toml.direct` and keeps a copy of the active config before replacing it. + +## Start sessions with separate logins and worktrees + +Codex CLI and Claude Code support named accounts. Expand **Sign in to another +subscription account…** and enter a name. **Open login in Terminal…** opens the +tool's own login command. Complete the login, refresh, and select the account. +Switchyard keeps account directories under `~/.switchyard/accounts`; the coding +tool owns their login files. The selected account determines where **Connect +tools** saves settings and which login a new session uses. Existing sessions +keep their login. Switchyard does not read, copy, or rotate OAuth tokens or +combine subscription quotas. + +In **Sessions**, choose a route and login, enter an absolute Git project path, +and click **Review session**, then **Open Terminal…**. Each session starts from +that checkout's HEAD on a new branch in `~/.switchyard/worktrees` and opens in +Terminal. Uncommitted files stay in the original checkout. Each launched session +has its own model settings; launching another session does not change user +defaults. Codex app uses its own workspace controls; choose Codex CLI for +worktree sessions. + +Codex and Claude launches send a session ID for usage correlation. Pi stores its +session under the private agent directory beside the worktree. Its requests +need an integration that sends session and turn IDs to appear as a correlated +session. Otherwise, Usage lists its calls under **All sessions**. + +Use `git worktree list` and `git worktree remove` to manage checkouts after +saving or discarding changes. Successful launches keep their branches and +private settings directories until you remove them. Named account directories +also remain until you remove them. If launch setup fails, the app tries to +remove the worktree, branch, and private settings it created; cleanup can fail. + +The app does not show provider reset windows or manage task dependencies, +session resume, remote workspaces, or review comments. Provider limits still apply. + +## Usage + +**Usage** lists the actual model, public route, timestamp, input, cached, and +output tokens, and recorded session and turn IDs for each completed model call. +Filter by session or search for a turn, model, or route. A user turn may contain +several calls. Classifier calls are marked as routing overhead. Missing IDs +appear as **Session not recorded** or **Turn not recorded** in the desktop app +and **Not recorded** in the terminal app. Switchyard does not infer them from +prompts. + +Recent history reads the last 8 MiB of the log plus one byte to check whether the +first record is complete. It keeps at most 5,000 complete records and excludes +partial records. Usage labels a limited view and counts unreadable records. +Overview starts with model totals. Use **Period** to select today, the past +seven local calendar days, or all retained history. **Compare** switches between +models, routes, and sessions; **Measure** switches between tokens and calls. +The chart shows the ten largest totals, and the table lists every matching +name with exact counts. Click a name to see its recent calls with the same +period filter. The daily chart shows usage by model for the past seven days. + +Model, route, and daily totals use the incremental full-log reader. Session +comparisons and per-turn details use the bounded recent history and label its +limits. Replacing or truncating the log resets full-log totals. The terminal +app's hourly sparkline still uses recent history. + +Charts use Apache ECharts 6.1.0, bundled in `ui/vendor` with its Apache license +and NOTICE. They load locally and require no CDN or frontend build tool. +The minified dependency adds about 1.1 MB before compression. Tooltips render +on the canvas, and tables provide readable counts and keyboard links. +The layout follows free references from [Carbon](https://www.carbondesignsystem.com/building-blocks/data-visualization/dashboards), +[Tabler](https://docs.tabler.io/ui/getting-started), and +[Primer](https://primer.style/product/components/page-layout/). + +Routing history stores no prompt or response text. +Observed tokens do not report provider quotas, reset windows, or authoritative +billing. Both apps refresh usage at the configured `refresh_seconds` interval. + +## Settings + +The app reads `~/.switchyard/desktop.toml` on refresh and before each operation: + +```toml +server_url = "http://127.0.0.1:4123" +routing_log = "~/.switchyard/routing.jsonl" +config_file = "~/.switchyard/composite.toml" +launchd_label = "com.nvidia.switchyard.server" +baseline_model = "gpt-5.6-sol" +refresh_seconds = 30 +``` + +Use **Settings → Open server config** to edit advanced routing options. +Model lists are cached beside the app settings in `model-lists.json`. +Usage remains readable when the route config is missing or malformed. + +## How savings are computed + +The server writes one JSONL record per call, with the model that answered and +its token counts. + +- **Actual** is every call priced at the model that served it, including + Switchyard's own classifier calls. +- **Baseline** is the caller-facing calls only, priced as if each had used + `baseline_model`. Classifier calls have no baseline counterpart, because + without Switchyard they would not happen. + +Savings are the difference, so routing overhead counts against the figure and a +bad day shows a negative number. Each period's cost and savings estimates stay +hidden until every model recorded in that period and the baseline model have +prices. Overview names missing model prices and explains estimates under **How +cost estimates work**. + +Codex's "Approve for me" reviews count too. The server config that the +installer writes sends them to `codex-auto-review` on the ChatGPT backend. With +a ChatGPT login, Codex sends these reviews to `codex-auto-review` even without +Switchyard, so the installer prices that model at the `baseline_model` rates, +including the cached input rate. +Each review then adds the same amount to the actual cost and to the baseline, +so reviews do not change the dollar amount saved. They do lower the percentage +a little, because the baseline grows. If you change `baseline_model`, update +this price to match. + +The app needs a price for the model ID that the routing log records for reviews, +which is the reviewer target's `id`. Without that price, the app hides the +savings estimate for every period that includes a review. The installer does not +overwrite an existing `composite.toml` or `desktop.toml`, so an existing install +needs the route and the price added by hand. + +If a reviewer uses another model ID, add a price for that ID. Any period with +an unpriced review hides its savings and shows the missing-price hint. + +These are list-price estimates. On a ChatGPT login there is no per-token bill, +so read them as "what this traffic would have cost at API rates". diff --git a/crates/switchyard-desktop/build.rs b/crates/switchyard-desktop/build.rs new file mode 100644 index 0000000000..ca2c62f579 --- /dev/null +++ b/crates/switchyard-desktop/build.rs @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +fn main() { + // Every host checks both platform exports so a stale Windows icon also fails a macOS build. + println!("cargo:rerun-if-changed=icons"); + if let Err(error) = switchyard_desktop_icons::export(std::path::Path::new("icons"), true) { + panic!("{error}"); + } + #[cfg(any(target_os = "macos", windows))] + tauri_build::build(); +} diff --git a/crates/switchyard-desktop/icons/README.md b/crates/switchyard-desktop/icons/README.md new file mode 100644 index 0000000000..4561c107bd --- /dev/null +++ b/crates/switchyard-desktop/icons/README.md @@ -0,0 +1,41 @@ +# Desktop app icon + +The app icon shows a raised titanium railway turnout with pale-blue supports on +a navy beveled tile. It represents Switchyard routing requests between models. + +`icon.png` is the 1024 × 1024 RGBA source with a transparent tile exterior. +`icon.icns` contains macOS sizes from 16 to 1024 pixels. `icon.ico` contains +Windows sizes of 16, 32, 64, 128, and 256 pixels. + +The desktop tray uses the separate `tray.png` source; `tray.rgba` contains its +decoded pixels, and `tray.svg` is the editable design reference. Windows uses +`tray-windows.rgba`, a 22 × 22 pixel color copy of the app icon. macOS tints the +template tray icon. + +## Generation + +Gemini 3 Pro Image generated the selected artwork through Inference Hub with +`gcp/google/gemini-3-pro-image`, `n: 1`, and `size: "1024x1024"`. +The selected image used this exact prompt: + +```text +Create a refined Switchyard macOS app icon based on this visual description of the selected reference: raised silver titanium paired railway rails, ONE right-hand turnout with one straight continuing main route and one gradually curved branch, dark navy beveled rounded-square tile, restrained cool-blue accents. Generate one new polished 1024x1024 icon refinement. The reference has finely brushed silver rail top faces, charcoal metal rail webs, broad pale-blue base plates around the switch, and a small cool-blue inset near the central frog, all mounted on a textured midnight tile with a thick rounded beveled frame. Improve its clipping and mixed perspective. Reduce and recenter the ENTIRE rail motif so EVERY part, including end faces, foundations and shadows, is safely inside the tile. Leave at least 140 pixels of empty navy tile between EVERY rail end and the nearest tile edge; no rail reaches a tile rim or image boundary. Rail motif bounding box approximately x220..804 and y210..804, tile occupies x70..954 and y70..954. ALL rail endpoints clearly visible and finished, not cropped. Use ONE coherent near-front orthographic camera for the whole object: tile surface, rail top faces, metal side faces, gauge spacing and shadows all follow the same projection. Straight main rail pair stays straight and parallel. Branch rail pair separates gradually beginning tangent, with matching constant gauge along the curve and continuous plausible switch/frog junction. No extra branches, crossings or disconnected rail fragments. Maintain realistic physical extrusion, titanium bevels, softly grounded contact shadows, and large readable shapes. NO text, logos, arrows, trains, scenery, decorative stars or circuit patterns. Simple professional app icon, plain white exterior, no checkerboard. Change only the subtle material, bevel or lighting treatment specified below while keeping the reference turnout concept and corrected generous margins. Variant: bright polished titanium upper bevels with satin silver top faces and graphite side faces; cool pale blue support inlay, frontal orthographic camera, restrained upper-left soft key light. Moderate visible metal thickness and very clean machining. Rail motif small enough for generous interior margins. +``` + +## Export + +1. Preserve the generated image before export. +2. Isolate the navy tile from its white exterior. Fill enclosed mask regions so + the rails and supports stay opaque, then soften the outer mask by 0.35 pixels. +3. Save the result as a 1024 × 1024 RGBA PNG, keeping the artwork inside the tile + unchanged. +4. Run `cargo desktop-icons` from the repository. The Rust command exports + `icon.icns`, `icon.ico`, and both tray pixel files. It uses Lanczos filtering + when resizing the app artwork. +5. Inspect the PNG on light, dark, and vivid backgrounds and compare the icon at + 16, 32, 64, and 128 pixels. + +Run `cargo desktop-icons --check` to verify exports without changing files. +Every desktop build runs this check on every platform. After changing either +PNG, regenerate its exports before building. Keep the PNG sources as RGBA +images at their existing sizes: 1024 × 1024 for the app and 22 × 22 for the tray. diff --git a/crates/switchyard-desktop/icons/icon.icns b/crates/switchyard-desktop/icons/icon.icns new file mode 100644 index 0000000000..aa3d06723a Binary files /dev/null and b/crates/switchyard-desktop/icons/icon.icns differ diff --git a/crates/switchyard-desktop/icons/icon.ico b/crates/switchyard-desktop/icons/icon.ico new file mode 100644 index 0000000000..3519cb9194 Binary files /dev/null and b/crates/switchyard-desktop/icons/icon.ico differ diff --git a/crates/switchyard-desktop/icons/icon.png b/crates/switchyard-desktop/icons/icon.png new file mode 100644 index 0000000000..78b2c7f1ee Binary files /dev/null and b/crates/switchyard-desktop/icons/icon.png differ diff --git a/crates/switchyard-desktop/icons/tray-windows.rgba b/crates/switchyard-desktop/icons/tray-windows.rgba new file mode 100644 index 0000000000..3a1026377f Binary files /dev/null and b/crates/switchyard-desktop/icons/tray-windows.rgba differ diff --git a/crates/switchyard-desktop/icons/tray.png b/crates/switchyard-desktop/icons/tray.png new file mode 100644 index 0000000000..dfeb25a27f Binary files /dev/null and b/crates/switchyard-desktop/icons/tray.png differ diff --git a/crates/switchyard-desktop/icons/tray.rgba b/crates/switchyard-desktop/icons/tray.rgba new file mode 100644 index 0000000000..f0a1865327 Binary files /dev/null and b/crates/switchyard-desktop/icons/tray.rgba differ diff --git a/crates/switchyard-desktop/icons/tray.svg b/crates/switchyard-desktop/icons/tray.svg new file mode 100644 index 0000000000..fe8cb99b2e --- /dev/null +++ b/crates/switchyard-desktop/icons/tray.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/switchyard-desktop/src/accounts.rs b/crates/switchyard-desktop/src/accounts.rs new file mode 100644 index 0000000000..467d51ffb0 --- /dev/null +++ b/crates/switchyard-desktop/src/accounts.rs @@ -0,0 +1,279 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Named login directories use the coding tool's own authentication commands. + +use crate::harness::{self, Harness}; +use std::path::{Path, PathBuf}; + +// PendingAccount removes only an empty directory if login setup fails. +// The coding tool may already have saved login files, which must stay. +struct PendingAccount(Option); + +impl Drop for PendingAccount { + fn drop(&mut self) { + if let Some(path) = &self.0 { + let _ = std::fs::remove_dir(path); + } + } +} + +fn root(tool: Harness) -> Result { + let name = match tool { + Harness::CodexCli => "codex", + Harness::Claude => "claude", + _ => return Err("Named accounts are available for Codex CLI and Claude Code.".into()), + }; + let home = crate::config::expand_home(Path::new("~")); + let mut path = home; + for component in [".switchyard", "accounts", name] { + path.push(component); + if std::fs::symlink_metadata(&path).is_ok_and(|m| m.file_type().is_symlink()) { + return Err(format!( + "{} is a symlink. Edit its target yourself.", + path.display() + )); + } + } + Ok(path) +} + +pub fn validate(tool: Harness, path: &Path) -> Result<(), String> { + let root = root(tool)?; + if path.parent() != Some(root.as_path()) + || !std::fs::symlink_metadata(path).is_ok_and(|m| m.is_dir() && !m.file_type().is_symlink()) + { + return Err("Choose a supported coding tool.".into()); + } + Ok(()) +} + +pub fn discover(tool: Harness) -> Result, String> { + if !matches!(tool, Harness::CodexCli | Harness::Claude) { + return Ok(Vec::new()); + } + let root = root(tool)?; + let entries = match std::fs::read_dir(&root) { + Ok(entries) => entries, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()), + Err(error) => { + return Err(format!( + "Could not list accounts in {}: {error}", + root.display() + )); + } + }; + let mut accounts = Vec::new(); + for entry in entries { + let entry = entry.map_err(|error| error.to_string())?; + if !entry + .file_name() + .to_string_lossy() + .starts_with(".archived-") + && entry + .file_type() + .map_err(|error| error.to_string())? + .is_dir() + && let Ok(name) = entry.file_name().into_string() + { + accounts.push((name, entry.path())); + } + } + accounts.sort_by(|a, b| a.0.cmp(&b.0)); + Ok(accounts) +} + +pub fn list(tool: Harness) -> Vec<(String, PathBuf)> { + discover(tool).unwrap_or_default() +} + +pub fn config_paths(tool: Harness, account: Option<&Path>) -> Vec { + let Some(account) = account else { + return harness::paths(tool); + }; + match tool { + Harness::CodexCli => vec![account.join("sy.config.toml")], + Harness::Claude => vec![account.join("settings.json")], + _ => harness::paths(tool), + } +} + +pub fn add(tool: Harness, name: &str) -> Result { + if name.is_empty() + || name.len() > 64 + || !name + .bytes() + .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_') + { + return Err("Account names need 1–64 letters, numbers, hyphens, or underscores.".into()); + } + let root = root(tool)?; + harness::binary(tool).ok_or("Install the selected coding tool first.")?; + std::fs::create_dir_all(&root).map_err(|e| e.to_string())?; + let path = root.join(name); + #[cfg(unix)] + { + use std::os::unix::fs::DirBuilderExt; + std::fs::DirBuilder::new().mode(0o700).create(&path).map_err(|e|format!("Create account: {e}. Pick an existing account after Refresh, or choose a new name."))?; + } + #[cfg(not(unix))] + std::fs::create_dir(&path).map_err(|e| e.to_string())?; + let mut pending = PendingAccount(Some(path.clone())); + login(tool, &path, name)?; + pending.0 = None; + Ok(format!( + "Opened the coding tool's login in Terminal for account {name}. Complete login, click Refresh, and select this account. Switchyard does not read the login tokens. The selected account applies to new sessions only. Account directory: {}", + path.display() + )) +} + +pub fn login(tool: Harness, path: &Path, name: &str) -> Result { + validate(tool, path)?; + let binary = harness::binary(tool).ok_or("Install the selected coding tool first.")?; + let variable = match tool { + Harness::CodexCli => "CODEX_HOME", + Harness::Claude => "CLAUDE_CONFIG_DIR", + _ => return Err("Choose a supported coding tool.".into()), + }; + #[cfg(windows)] + crate::windows::open_terminal(crate::windows::TerminalJob { + binary: binary.clone(), + args: match tool { + Harness::CodexCli => vec!["login".into()], + _ => vec!["auth".into(), "login".into()], + }, + directory: None, + environment: vec![(variable.into(), path.display().to_string())], + remove_environment: vec![], + })?; + #[cfg(not(windows))] + { + let login = match tool { + Harness::CodexCli => "login", + _ => "auth login", + }; + let script = format!( + "#!/bin/bash\nrm -- \"$0\"\nexport PATH=\"/opt/homebrew/bin:/usr/local/bin:$PATH\"\nexport {variable}={}\n{} {login}", + crate::sessions::quote(&path.display().to_string()), + crate::sessions::quote(&binary.display().to_string()) + ); + crate::server::open_terminal_script("switchyard-login-", &script)?; + } + Ok(format!( + "Opened login in Terminal for account {name}. Finish login in the coding tool. Login status remains unknown until the tool confirms it." + )) +} + +pub fn archive(tool: Harness, path: &Path) -> Result { + validate(tool, path)?; + let archived = path.with_file_name(format!(".archived-{}", uuid::Uuid::now_v7())); + std::fs::rename(path, &archived).map_err(|error| error.to_string())?; + Ok(format!( + "Archived the account folder at {}. Login files are preserved. Existing sessions may still use the old directory and need a new login.", + archived.display() + )) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn archived_accounts_leave_the_picker_and_keep_login_files() { + if std::env::var_os("SWITCHYARD_ACCOUNT_ARCHIVE_FIXTURE").is_some() { + let entries = discover(Harness::CodexCli).expect("accounts"); + assert_eq!(entries.len(), 1); + let path = &entries[0].1; + archive(Harness::CodexCli, path).expect("archive"); + assert!(!path.exists()); + assert!( + discover(Harness::CodexCli) + .expect("accounts after archive") + .is_empty() + ); + let hidden = std::fs::read_dir(root(Harness::CodexCli).expect("root")) + .expect("archived root") + .next() + .expect("archived entry") + .expect("entry") + .path(); + assert_eq!( + std::fs::read_to_string(hidden.join("auth.json")).expect("login"), + "private-fixture-login" + ); + return; + } + let fixture = tempfile::tempdir().expect("fixture"); + let path = fixture.path().join(".switchyard/accounts/codex/named"); + std::fs::create_dir_all(&path).expect("account"); + std::fs::write(path.join("auth.json"), "private-fixture-login").expect("login"); + let child = std::process::Command::new(std::env::current_exe().expect("runner")) + .args([ + "--exact", + "accounts::tests::archived_accounts_leave_the_picker_and_keep_login_files", + ]) + .env("HOME", fixture.path()) + .env("SWITCHYARD_ACCOUNT_ARCHIVE_FIXTURE", "1") + .output() + .expect("child"); + assert!( + child.status.success(), + "{}", + String::from_utf8_lossy(&child.stdout) + ); + } + + #[test] + fn unsafe_names_fail_before_any_side_effect() { + for name in ["", "..", "a/b", "$(touch bad)", "a\nb"] { + assert!(add(Harness::CodexCli, name).is_err()); + } + assert!(add(Harness::Pi, "valid").is_err()); + } + + #[test] + fn a_login_setup_failure_removes_only_its_empty_directory() { + let root = tempfile::tempdir().expect("directory"); + let path = root.path().join("account"); + std::fs::create_dir(&path).expect("account"); + drop(PendingAccount(Some(path.clone()))); + assert!(!path.exists()); + std::fs::create_dir(&path).expect("account"); + std::fs::write(path.join("auth.json"), "private").expect("auth"); + drop(PendingAccount(Some(path.clone()))); + assert!(path.join("auth.json").is_file()); + } + // The subprocess isolates HOME so parallel tests keep their own settings. + #[cfg(unix)] + #[test] + fn symlinked_account_roots_are_not_listed_or_used_for_login() { + if std::env::var_os("SWITCHYARD_ACCOUNT_FIXTURE").is_some() { + assert!(list(Harness::CodexCli).is_empty()); + assert!(discover(Harness::CodexCli).is_err()); + assert!(add(Harness::CodexCli, "new").is_err()); + let path = crate::config::expand_home(Path::new("~/.switchyard/accounts/codex/named")); + assert!(validate(Harness::CodexCli, &path).is_err()); + return; + } + let dir = tempfile::tempdir().expect("fixture"); + let home = dir.path().join("home"); + let target = dir.path().join("target"); + std::fs::create_dir(&home).expect("home"); + std::fs::create_dir_all(target.join("accounts/codex/named")).expect("accounts"); + std::os::unix::fs::symlink(&target, home.join(".switchyard")).expect("symlink"); + let output = std::process::Command::new(std::env::current_exe().expect("runner")) + .args([ + "--exact", + "accounts::tests::symlinked_account_roots_are_not_listed_or_used_for_login", + ]) + .env("SWITCHYARD_ACCOUNT_FIXTURE", "1") + .env("HOME", &home) + .output() + .expect("child"); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stdout) + ); + assert!(!target.join("accounts/codex/new").exists()); + } +} diff --git a/crates/switchyard-desktop/src/config.rs b/crates/switchyard-desktop/src/config.rs new file mode 100644 index 0000000000..01f6be1956 --- /dev/null +++ b/crates/switchyard-desktop/src/config.rs @@ -0,0 +1,229 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! This module reads app settings from `~/.switchyard/desktop.toml`. + +use std::path::{Path, PathBuf}; + +use serde::Deserialize; + +use crate::pricing::PriceTable; + +/// Config stores the settings the apps use to find the server and estimate costs. +/// Missing keys fall back to [`Config::default`]. +#[derive(Clone, Debug, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct Config { + /// This field sets the server base URL used for health checks. + pub server_url: String, + /// This field names the JSONL file where the server appends routing records. + pub routing_log: PathBuf, + /// This field names the server TOML file opened by "Open server config". + pub config_file: PathBuf, + /// This field names the LaunchAgent used to restart the server. + pub launchd_label: String, + /// This field sets the usage refresh interval in seconds. + pub refresh_seconds: u64, + /// This field names the model used to estimate costs without routing. + pub baseline_model: String, + /// This field stores per-model rates; missing rates hide dollar figures. + pub prices: PriceTable, +} + +impl Default for Config { + fn default() -> Self { + Self { + server_url: "http://127.0.0.1:4123".to_string(), + routing_log: PathBuf::from("~/.switchyard/routing.jsonl"), + config_file: PathBuf::from("~/.switchyard/composite.toml"), + launchd_label: "com.nvidia.switchyard.server".to_string(), + refresh_seconds: 30, + baseline_model: "gpt-5.6-sol".to_string(), + prices: PriceTable::new(), + } + } +} + +impl Config { + /// This function parses settings and uses defaults when the file is absent. + pub fn load(path: &Path) -> Result { + let mut config: Self = match std::fs::read_to_string(path) { + Ok(text) => toml::from_str(&text) + .map_err(|error| format!("parse {}: {error}", path.display()))?, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Self::default(), + Err(error) => return Err(format!("read {}: {error}", path.display())), + }; + crate::health::validate_url(&config.server_url)?; + for (model, price) in &config.prices { + price + .validate() + .map_err(|error| format!("Price for {model}: {error}"))?; + } + config.routing_log = expand_home(&config.routing_log); + config.config_file = expand_home(&config.config_file); + Ok(config) + } + + /// The app reads installation metadata so Finder and Windows shortcuts retain a custom SY_HOME. + /// An executable without that metadata uses `~/.switchyard/desktop.toml`. + pub fn default_path() -> Result { + let executable = std::env::current_exe().map_err(|e| e.to_string())?; + let metadata = switchyard_desktop_install::bundle_metadata(&executable); + if metadata.try_exists().map_err(|e| e.to_string())? { + return switchyard_desktop_install::Settings::load(&metadata) + .map(|s| s.desktop_settings()); + } + Ok(expand_home(Path::new("~/.switchyard/desktop.toml"))) + } +} + +pub fn save_preferences( + path: &Path, + baseline: &str, + refresh: u64, + prices: &PriceTable, +) -> Result<(), String> { + use std::io::Write; + if baseline.trim().is_empty() || baseline.contains(['\n', '\r']) { + return Err("Choose a nonempty baseline model ID without line breaks.".into()); + } + if refresh == 0 { + return Err("Refresh interval must be at least one second.".into()); + } + for (model, price) in prices { + price + .validate() + .map_err(|error| format!("Price for {model}: {error}"))?; + } + let original = match std::fs::read_to_string(path) { + Ok(text) => Some(text), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => None, + Err(error) => return Err(error.to_string()), + }; + let mut doc = original + .as_deref() + .unwrap_or("") + .parse::() + .map_err(|error| error.to_string())?; + doc["baseline_model"] = toml_edit::value(baseline.trim()); + doc["refresh_seconds"] = + toml_edit::value(i64::try_from(refresh).map_err(|error| error.to_string())?); + let price_text = toml::to_string(&serde_json::json!({"prices":prices})) + .map_err(|error| error.to_string())?; + let price_doc = price_text + .parse::() + .map_err(|error| error.to_string())?; + doc["prices"] = price_doc["prices"].clone(); + let text = doc.to_string(); + let config: Config = toml::from_str(&text).map_err(|error| error.to_string())?; + crate::health::validate_url(&config.server_url)?; + let destination = if original.is_some() { + std::fs::canonicalize(path).map_err(|error| error.to_string())? + } else { + path.to_path_buf() + }; + let parent = destination + .parent() + .ok_or("App settings need a parent folder.")?; + std::fs::create_dir_all(parent).map_err(|error| error.to_string())?; + let mut temp = tempfile::NamedTempFile::new_in(parent).map_err(|error| error.to_string())?; + temp.write_all(text.as_bytes()) + .and_then(|()| temp.as_file().sync_all()) + .map_err(|error| error.to_string())?; + if let Ok(metadata) = std::fs::metadata(path) { + temp.as_file() + .set_permissions(metadata.permissions()) + .map_err(|error| error.to_string())?; + } + let current = match std::fs::read_to_string(path) { + Ok(text) => Some(text), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => None, + Err(error) => return Err(error.to_string()), + }; + if current != original { + return Err("App settings changed while saving. Reload them and try again.".into()); + } + temp.persist(destination) + .map_err(|error| error.to_string())?; + Ok(()) +} + +pub fn validate_toml_file(path: &Path) -> Result<(), String> { + let text = std::fs::read_to_string(path) + .map_err(|error| format!("read {}: {error}", path.display()))?; + toml::from_str::(&text) + .map_err(|error| format!("parse {}: {error}", path.display()))?; + Ok(()) +} + +/// This function rewrites a leading `~`. Paths are written by hand in the settings file, +/// where `~` is the natural way to spell a home path. +pub fn expand_home(path: &Path) -> PathBuf { + let home = std::env::var_os("HOME") + .or_else(|| std::env::var_os("USERPROFILE")) + .map_or_else(|| PathBuf::from("."), PathBuf::from); + match path.strip_prefix("~") { + Ok(rest) => home.join(rest), + Err(_) => path.to_path_buf(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn preference_edits_keep_unrelated_settings_and_reject_invalid_values() { + let dir = tempfile::tempdir().expect("fixture"); + let path = dir.path().join("desktop.toml"); + let original = + "# user comment\nserver_url='http://[::1]:4123'\nrouting_log='/custom/log'\n"; + std::fs::write(&path, original).expect("settings"); + assert!(save_preferences(&path, "", 30, &PriceTable::new()).is_err()); + assert!(save_preferences(&path, "baseline", 0, &PriceTable::new()).is_err()); + assert_eq!(std::fs::read_to_string(&path).expect("settings"), original); + let prices = PriceTable::from([( + "baseline".into(), + crate::pricing::ModelPrice { + input_per_mtok: 1.0, + cached_input_per_mtok: None, + output_per_mtok: 2.0, + }, + )]); + save_preferences(&path, "baseline", 12, &prices).expect("save"); + let saved = std::fs::read_to_string(&path).expect("saved"); + assert!(saved.contains("# user comment")); + let config = Config::load(&path).expect("config"); + assert_eq!(config.server_url, "http://[::1]:4123"); + assert_eq!(config.routing_log, PathBuf::from("/custom/log")); + assert_eq!(config.baseline_model, "baseline"); + assert_eq!(config.refresh_seconds, 12); + assert_eq!(config.prices, prices); + } + + #[test] + fn invalid_price_input_forms_reject_settings() { + let dir = tempfile::tempdir().expect("fixture"); + let path = dir.path().join("desktop.toml"); + for field in ["input_per_mtok", "cached_input_per_mtok", "output_per_mtok"] { + for value in ["-1.0", "nan", "inf", "-inf"] { + let mut rates = vec![ + "input_per_mtok=0.0".to_string(), + "output_per_mtok=0.0".to_string(), + ]; + rates.retain(|line| !line.starts_with(field)); + rates.push(format!("{field}={value}")); + std::fs::write(&path, format!("[prices.model]\n{}", rates.join("\n"))) + .expect("settings"); + assert!(Config::load(&path).is_err(), "{field}={value}"); + } + } + std::fs::write(&path,"[prices.model]\ninput_per_mtok=0.0\noutput_per_mtok=1.0\ncached_input_per_mtok_typo=0.0").expect("settings"); + assert!(Config::load(&path).is_err()); + std::fs::write( + &path, + "[prices.model]\ninput_per_mtok=0.0\noutput_per_mtok=1.0\ncached_input_per_mtok=0.0", + ) + .expect("settings"); + assert!(Config::load(&path).is_ok()); + } +} diff --git a/crates/switchyard-desktop/src/controller.rs b/crates/switchyard-desktop/src/controller.rs new file mode 100644 index 0000000000..aed89a17a8 --- /dev/null +++ b/crates/switchyard-desktop/src/controller.rs @@ -0,0 +1,1852 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Both frontends call these operations so validation stays independent of widgets. + +use crate::{ + accounts, + config::Config, + harness::{self, Harness}, + history, models, rollup, server, + server_config::{ALGORITHMS, Choice, Route, ServerConfig}, + sessions, +}; +use serde::{Deserialize, Serialize}; +use serde_json::{Value, json}; +use std::path::PathBuf; + +#[derive(Clone, Debug, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)] +pub enum Action { + Editor { + generation: u64, + route: String, + algorithm: String, + }, + Apply { + generation: u64, + route: String, + algorithm: String, + choices: Vec, + revision: Option, + }, + Remove { + generation: u64, + route: String, + id: String, + revision: Option, + }, + PreviewRoute { + generation: u64, + route: String, + algorithm: String, + choices: Vec, + revision: Option, + }, + PreviewRouteChange { + generation: u64, + operation: String, + route: String, + name: String, + id: String, + algorithm: Option, + choices: Vec, + }, + RouteChange { + generation: u64, + operation: String, + route: String, + name: String, + id: String, + algorithm: Option, + choices: Vec, + preview_token: Option, + }, + Models { + client: String, + refresh: bool, + key: Option, + }, + PreviewInstall { + tool: Harness, + account: Option, + route: String, + id: String, + settings_file: Option, + }, + Install { + tool: Harness, + account: Option, + route: String, + id: String, + settings_file: Option, + preview_token: Option, + }, + PreviewRestore { + tool: Harness, + account: Option, + settings_file: Option, + }, + Restore { + tool: Harness, + account: Option, + settings_file: Option, + preview_token: Option, + }, + AddAccount { + tool: Harness, + name: String, + }, + Launch { + preview_token: Option, + tool: Harness, + account: Option, + route: String, + id: String, + project: String, + }, + PreviewLaunch { + tool: Harness, + account: Option, + route: String, + id: String, + project: String, + }, + LoginAccount { + tool: Harness, + name: String, + }, + PreviewAccountArchive { + tool: Harness, + name: String, + }, + ArchiveAccount { + tool: Harness, + name: String, + preview_token: Option, + }, + OpenAccount { + tool: Harness, + name: String, + }, + OpenLogs {}, + OpenSession { + id: String, + }, + PreviewSessionCleanup { + id: String, + }, + RemoveSession { + id: String, + preview_token: Option, + }, + Restart {}, + PreviewUpdate {}, + Update { + preview_token: Option, + }, + OpenUpdateLog {}, + OpenConfig {}, + OpenSettings {}, + RetrySettings {}, + CheckConfig {}, + SavePreferences { + baseline_model: String, + refresh_seconds: u64, + prices: crate::pricing::PriceTable, + }, +} + +#[derive(Serialize)] +pub struct Reply { + pub message: String, + pub data: Value, +} +impl Reply { + fn saved_edit(result: server::SaveResult) -> Self { + Self { + message: result.message, + data: json!({"warning": result.warning, "details": result.details}), + } + } + + fn message(message: String) -> Self { + Self { + message, + data: Value::Null, + } + } +} + +// Review keeps exact file bytes in the controller so later edits require a new review. +// Only the UUID token goes to the frontend; reviewed files can contain credentials. +struct Review { + token: String, + inputs: Value, + files: Vec, +} +impl Review { + fn capture(inputs: Value, paths: Vec) -> Result { + let files = paths + .into_iter() + .map(|path| { + let bytes = match std::fs::read(&path) { + Ok(bytes) => Some(std::sync::Arc::<[u8]>::from(bytes)), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => None, + Err(error) => return Err(format!("Read {}: {error}", path.display())), + }; + Ok((path, bytes)) + }) + .collect::, String>>()?; + Ok(Self { + token: uuid::Uuid::now_v7().to_string(), + inputs, + files, + }) + } + fn original(&self) -> Result<&str, String> { + std::str::from_utf8( + self.files + .first() + .and_then(|(_, bytes)| bytes.as_deref()) + .ok_or("The reviewed server config is missing.")?, + ) + .map_err(|error| error.to_string()) + } + // Decoded settings omit comments and unrelated fields, so this check compares raw bytes. + fn check(&self, token: Option<&str>, inputs: &Value) -> Result<(), String> { + if token != Some(self.token.as_str()) || inputs != &self.inputs { + return Err("Review these changes again before applying them.".into()); + } + for (path, bytes) in &self.files { + let current = match std::fs::read(path) { + Ok(bytes) => Some(std::sync::Arc::<[u8]>::from(bytes)), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => None, + Err(error) => return Err(error.to_string()), + }; + if ¤t != bytes { + return Err(format!( + "{} changed since your review. Review it again; your draft is preserved.", + path.display() + )); + } + } + Ok(()) + } +} + +// Each Controller retains one launch review until the next preview or a selected config change. +// Review keeps raw settings private; Launch uses these values and the reviewed Git commit. +struct ReviewedLaunch { + review: Review, + project: Value, + model: String, + server_url: String, + login: bool, + account: Option, +} + +pub struct Controller { + settings: PathBuf, + config: Config, + reader: rollup::Reader, + // The generation ties a draft to the server config file selected by the settings. + generation: u64, + route_reviews: std::collections::HashMap, + tool_review: Option, + session_cleanup: Option<(String, String, Value)>, + account_archive: Option<(Harness, String, String)>, + route_change: Option, + update_review: Option<(String, Value)>, + launch_review: Option, +} +impl Controller { + pub fn new(config: Config, settings: PathBuf) -> Self { + Self { + config, + settings, + reader: rollup::Reader::default(), + generation: 0, + route_reviews: Default::default(), + tool_review: None, + session_cleanup: None, + account_archive: None, + route_change: None, + update_review: None, + launch_review: None, + } + } + fn reload(&mut self) -> Result<(), String> { + let config = Config::load(&self.settings)?; + if config.config_file != self.config.config_file { + self.generation += 1; + self.route_reviews.clear(); + self.tool_review = None; + self.route_change = None; + self.launch_review = None; + } + self.config = config; + Ok(()) + } + fn check_generation(&self, generation: u64) -> Result<(), String> { + if generation != self.generation { + return Err("The route changed or was removed. Refresh and choose it again.".into()); + } + Ok(()) + } + fn routes(&self) -> Result { + let text = std::fs::read_to_string(&self.config.config_file).map_err(|e| e.to_string())?; + ServerConfig::parse(&text).map_err(|_| { + "The server config contains invalid TOML. Open it in Settings to fix it.".into() + }) + } + // Unchanged bytes keep the draft revision when the editor switches algorithms. + fn remember_route(&mut self, route: &str, mut review: Review) -> String { + if let Some(previous) = self.route_reviews.get(route) + && previous.files == review.files + { + review.token = previous.token.clone(); + } + let revision = review.token.clone(); + self.route_reviews.insert(route.to_string(), review); + revision + } + pub fn snapshot(&mut self) -> Result { + self.reload()?; + let mut errors = Vec::new(); + // One local date keeps aggregate totals and frontend filters aligned across midnight. + let today = chrono::Local::now().date_naive(); + let mut usage_available = true; + let usage = self + .reader + .read(&self.config.routing_log, today) + .unwrap_or_else(|error| { + usage_available = false; + errors.push(format!("Could not read usage: {error}")); + rollup::Usage::default() + }); + let estimates: std::collections::BTreeMap<_, _> = [ + ("today", &usage.today), ("week", &usage.week), ("all", &usage.all), + ].into_iter().map(|(period, totals)| { + let missing = crate::pricing::unpriced( + totals.routed.keys().chain(totals.classifier.keys()) + .map(String::as_str).chain(std::iter::once(self.config.baseline_model.as_str())), + &self.config.prices, + ); + (period, json!({"cost":if usage_available {crate::pricing::estimate(totals, &self.config.prices, &self.config.baseline_model)} else {None},"missing":missing})) + }).collect(); + let checked_at = chrono::Utc::now().to_rfc3339(); + let mut health = crate::health::details(&self.config.server_url); + health["checked_at"] = json!(checked_at); + health["url"] = json!(self.config.server_url); + let server_status = if health["state"] == "running" { + crate::health::ServerStatus::Running + } else { + crate::health::ServerStatus::Stopped + }; + let summary = crate::summary::build(server_status, &usage, &self.config); + let metrics = json!({"server_url":self.config.server_url,"running":server_status == crate::health::ServerStatus::Running, + "today":{"requests":usage.today.requests(),"tokens":usage.today.tokens()}, + "week":{"requests":usage.week.requests(),"tokens":usage.week.tokens()}}); + let mut routes = Vec::new(); + let mut clients = Vec::new(); + let route_review = Review::capture(json!({}), vec![self.config.config_file.clone()]); + let route_config = route_review + .as_ref() + .map_err(Clone::clone) + .and_then(|review| { + ServerConfig::parse(review.original()?).map_err(|_| { + "The server config contains invalid TOML. Open it in Settings to fix it." + .to_string() + }) + }); + match route_config { + Ok(config) => { + let listed = config.routes(); + self.route_reviews + .retain(|key, _| listed.iter().any(|route| &route.key == key)); + for route in listed { + let original = route_review.as_ref().map_err(Clone::clone)?; + let review = Review { + token: uuid::Uuid::now_v7().to_string(), + inputs: json!({"route":route.key}), + files: original.files.clone(), + }; + let revision = self.remember_route(&route.key, review); + let choices = config.choices(&route.key); + let models = choices + .iter() + .map(|c| format!("{} / {}", c.client, c.model)) + .collect::>() + .join(" → "); + routes.push(json!({"key": route.key, "id": route.id, "kind": route.kind,"revision":revision, + "label": format!("{} · {} · {}", route.id, route.kind, models), "choices": choices, + "tiers": config.algorithm(&route.key).map(|a| config.roles(&route.key, a).iter().map(|r| r.tier.map(|t| format!("{t:?}"))).collect::>()), + "compatibility": harness::HARNESSES.iter().map(|(tool,_)| (serde_json::to_value(tool).unwrap().as_str().unwrap().to_string(),login_mode(*tool,&config,&route).err())).collect::>(), + "dependents":known_dependents(&route.id), + "editable": config.algorithm(&route.key).is_some(), "generated": config.generated_by("routes", &route.key)})); + } + clients = config.clients().iter().map(|c| json!({"name":c.name,"host":c.host(),"models":config.models_on(&c.name),"unlisted":models::unlisted(c),"accepts_key":cfg!(any(target_os = "macos", windows)) && !models::unlisted(c) && (c.forward_auth || c.api_key_env.is_some()),"note":models::missing_env(c).map(models::missing_env_note)})).collect(); + } + Err(e) => errors.push(e), + } + let mut history_available = true; + let history = history::load(&self.config.routing_log).unwrap_or_else(|e| { + history_available = false; + errors.push(e); + history::History::default() + }); + let activity = history::hourly_calls(&history.entries, chrono::Utc::now()); + let tools: Vec<_> = harness::HARNESSES.iter().map(|(tool,label)| { + let discovered = accounts::discover(*tool); + let error = discovered.as_ref().err().cloned(); + let discovered = discovered.unwrap_or_default(); + let names: Vec<_> = discovered.iter().map(|(name,_)|name).collect(); + let details: Vec<_> = discovered.iter().map(|(name,path)|json!({"name":name,"path":path,"login_status":"unknown","status":harness::inspect(*tool,&accounts::config_paths(*tool,Some(path))).unwrap_or_else(|error|error)})).collect(); + json!({"tool":tool,"label":label,"available":harness::binary(*tool).is_some(),"files":harness::paths(*tool),"status":harness::inspect(*tool,&harness::paths(*tool)).unwrap_or_else(|error|error),"accounts":names,"account_details":details,"discovery_error":error}) + }).collect(); + let update_status = install_metadata(&self.settings) + .and_then(|path| switchyard_desktop_install::update_status(&path)) + .unwrap_or_else(|error| json!({"state":"unavailable","message":error})); + let session_inventory = sessions::inventory(); + let inventory_error = session_inventory.as_ref().err().cloned(); + let session_inventory = session_inventory.unwrap_or_default(); + Ok( + json!({"update_status":update_status,"session_inventory":session_inventory,"session_inventory_error":inventory_error,"settings":{"path":self.settings,"config_file":self.config.config_file,"routing_log":self.config.routing_log,"baseline_model":self.config.baseline_model,"refresh_seconds":self.config.refresh_seconds,"prices":self.config.prices}, + "data_state":{"usage":{"state":if usage_available {"available"} else {"unavailable"},"checked_at":checked_at,"path":self.config.routing_log},"history":{"state":if history_available {"available"} else {"unavailable"},"checked_at":checked_at,"path":self.config.routing_log},"health":health}, + "generation":self.generation,"summary":summary,"metrics":metrics,"routes":routes,"clients":clients,"tools":tools, + "algorithms":ALGORITHMS.iter().map(|a|json!({"kind":a.kind,"title":a.title,"summary":a.summary})).collect::>(), + "today":today,"analytics":usage,"estimates":estimates,"baseline_model":self.config.baseline_model, + "activity":activity,"sessions":history.sessions(),"entries":history.entries,"limited":history.limited,"skipped":history.skipped, + "errors":errors,"refresh_seconds":self.config.refresh_seconds.max(1)}), + ) + } + pub fn dispatch(&mut self, action: Action) -> Result { + if matches!(action, Action::OpenSettings {}) { + server::command("open", &[&self.settings.display().to_string()])?; + return Ok(Reply::message("Opened app settings.".into())); + } + self.reload()?; + match action { + Action::Editor { + generation, + route, + algorithm, + } => { + self.check_generation(generation)?; + let review = Review::capture( + json!({"route":route}), + vec![self.config.config_file.clone()], + )?; + let config = self.routes()?; + review.check(Some(&review.token), &review.inputs)?; + current_route(&config, &route, None)?; + config + .algorithm(&route) + .ok_or("This route must be edited in the server config file.")?; + let algorithm = algorithm_by_id(&algorithm)?; + let roles = config.roles(&route, algorithm); + let revision = self.remember_route(&route, review); + let data = json!({"revision":revision,"roles": roles.iter().map(|r|json!({"label":r.label,"hint":r.hint,"tier":r.tier.map(|t|format!("{t:?}"))})).collect::>(),"choices":config.choices(&route)}); + Ok(Reply { + message: algorithm.summary.into(), + data, + }) + } + Action::Apply { + generation, + route, + algorithm, + choices, + revision, + } => { + self.check_generation(generation)?; + let config = self.routes()?; + current_route(&config, &route, None)?; + config + .algorithm(&route) + .ok_or("This route must be edited in the server config file.")?; + let algorithm = algorithm_by_id(&algorithm)?; + self.route_reviews + .get(&route) + .ok_or("Open the route editor before saving.")? + .check(revision.as_deref(), &json!({"route":route}))?; + server::apply( + &self.config, + &route, + algorithm, + &choices, + self.route_reviews[&route].original()?, + ) + .map(Reply::saved_edit) + } + Action::Remove { + generation, + route, + id, + revision, + } => { + self.check_generation(generation)?; + current_route(&self.routes()?, &route, Some(&id))?; + self.route_reviews + .get(&route) + .ok_or("Open the route editor before removing it.")? + .check(revision.as_deref(), &json!({"route":route}))?; + server::remove( + &self.config, + &route, + &id, + self.route_reviews[&route].original()?, + ) + .map(Reply::saved_edit) + } + Action::PreviewRouteChange { + generation, + operation, + route, + name, + id, + algorithm, + choices, + } => { + self.check_generation(generation)?; + let inputs = json!({"operation":operation,"route":route,"name":name,"id":id,"algorithm":algorithm,"choices":choices}); + let review = Review::capture(inputs, vec![self.config.config_file.clone()])?; + let edited = self.routes()?.change_route( + &operation, + &route, + &name, + &id, + algorithm.as_deref().map(algorithm_by_id).transpose()?, + &choices, + )?; + review.check(Some(&review.token), &review.inputs)?; + let data = json!({"notes":edited.notes,"file":self.config.config_file,"preview_token":review.token}); + self.route_change = Some(review); + Ok(Reply { + message: String::new(), + data, + }) + } + Action::RouteChange { + generation, + operation, + route, + name, + id, + algorithm, + choices, + preview_token, + } => { + self.check_generation(generation)?; + let inputs = json!({"operation":operation,"route":route,"name":name,"id":id,"algorithm":algorithm,"choices":choices}); + let review = self + .route_change + .as_ref() + .ok_or("Review the route change first.")?; + review.check(preview_token.as_deref(), &inputs)?; + let edited = self.routes()?.change_route( + &operation, + &route, + &name, + &id, + algorithm.as_deref().map(algorithm_by_id).transpose()?, + &choices, + )?; + server::save_route_change(&self.config, review.original()?, edited) + .map(Reply::saved_edit) + } + Action::Models { + client, + refresh, + key, + } => { + let config = self.routes()?; + let client = config + .clients() + .into_iter() + .find(|c| c.name == client) + .ok_or("Unknown endpoint.")?; + if let Some(key) = &key { + if key.trim().is_empty() || models::has_line_break(key) { + return Err("Enter a nonempty key without line breaks.".into()); + } + if models::unlisted(&client) + || !(client.forward_auth || client.api_key_env.is_some()) + { + return Err("This endpoint does not accept a model-list key. Choose an endpoint configured for model-list authentication.".into()); + } + #[cfg(not(any(target_os = "macos", windows)))] + models::save_key(&client.base_url, key)?; + } + let cache = self.settings.with_file_name(models::CACHE_FILE); + let loaded = + models::load(&cache, &client, refresh || key.is_some(), key.as_deref()); + if let Some(key) = &key { + match &loaded.error { + None | Some(models::ListError::NotCached(_)) => { + models::save_key(&client.base_url, key)? + } + Some(error) => return Err(error.reason()), + } + } + // Fetch and cache-write failures need a warning even when a model list remains usable. + let warning = loaded.error.is_some(); + let fetched_at = loaded.list.as_ref().map(|list| list.fetched_at); + let source = loaded.list.as_ref().map_or("config", |_| loaded.source); + let message = loaded + .error + .map(|e| format!("{} {}", e.reason(), e.advice())) + .unwrap_or_else(|| "Model list loaded.".into()); + let models = loaded + .list + .map(|l| l.models) + .unwrap_or_else(|| config.models_on(&client.name)); + Ok(Reply { + message, + data: json!({"client":client.name,"count":models.len(),"models":models,"warning":warning,"source":source,"fetched_at":fetched_at}), + }) + } + Action::PreviewRoute { + generation, + route, + algorithm, + choices, + revision, + } => { + self.check_generation(generation)?; + self.route_reviews + .get(&route) + .ok_or("Open the route editor before reviewing it.")? + .check(revision.as_deref(), &json!({"route":route}))?; + let edited = self + .routes()? + .edit(&route, algorithm_by_id(&algorithm)?, &choices)?; + Ok(Reply { + message: String::new(), + data: json!({"notes":edited.notes,"revision":revision,"file":self.config.config_file}), + }) + } + Action::PreviewInstall { + tool, + account, + route, + id, + settings_file, + } => { + let files = install_paths(tool, account.as_deref(), settings_file.as_deref())?; + let inputs = json!({"tool":tool,"account":account,"route":route,"id":id,"settings_file":settings_file,"server_url":self.config.server_url}); + let mut paths = harness::review_files(&files); + paths.push(self.config.config_file.clone()); + let review = Review::capture(inputs, paths)?; + let config = self.routes()?; + let selected = current_route(&config, &route, Some(&id))?; + let mut data = harness::preview( + tool, + &files, + &self.config.server_url, + &selected.id, + login_mode(tool, &config, &selected)?, + )?; + review.check(Some(&review.token), &review.inputs)?; + data["preview_token"] = json!(review.token); + self.tool_review = Some(review); + Ok(Reply { + message: String::new(), + data, + }) + } + Action::Install { + tool, + account, + route, + id, + settings_file, + preview_token, + } => { + let files = install_paths(tool, account.as_deref(), settings_file.as_deref())?; + let inputs = json!({"tool":tool,"account":account,"route":route,"id":id,"settings_file":settings_file,"server_url":self.config.server_url}); + self.tool_review + .as_ref() + .ok_or("Preview these settings before applying them.")? + .check(preview_token.as_deref(), &inputs)?; + let config = self.routes()?; + let route = current_route(&config, &route, Some(&id))?; + harness::install_reviewed( + tool, + &files, + &self.config.server_url, + &route.id, + login_mode(tool, &config, &route)?, + self.tool_review + .as_ref() + .map(|review| review.files.as_slice()), + ) + .map(Reply::message) + } + Action::PreviewRestore { + tool, + account, + settings_file, + } => { + let files = install_paths(tool, account.as_deref(), settings_file.as_deref())?; + let review = Review::capture( + json!({"restore":true,"tool":tool,"account":account,"settings_file":settings_file}), + harness::review_files(&files), + )?; + let mut data = harness::preview_restore(tool, &files)?; + review.check(Some(&review.token), &review.inputs)?; + data["preview_token"] = json!(review.token); + self.tool_review = Some(review); + Ok(Reply { + message: String::new(), + data, + }) + } + Action::Restore { + tool, + account, + settings_file, + preview_token, + } => { + let files = install_paths(tool, account.as_deref(), settings_file.as_deref())?; + self.tool_review.as_ref().ok_or("Preview the restore before applying it.")?.check(preview_token.as_deref(),&json!({"restore":true,"tool":tool,"account":account,"settings_file":settings_file}))?; + match harness::restore_reviewed( + &files, + self.tool_review + .as_ref() + .map(|review| review.files.as_slice()), + ) { + Ok(message) => Ok(Reply::message(message)), + Err(error) if error.starts_with("Restored settings, but") => Ok(Reply { + message: error, + data: json!({"warning":true}), + }), + Err(error) => Err(error), + } + } + Action::AddAccount { tool, name } => { + accounts::add(tool, name.trim()).map(Reply::message) + } + Action::Launch { + tool, + account, + route, + id, + project, + preview_token, + } => { + let inputs = + json!({"tool":tool,"account":account,"route":route,"id":id,"project":project}); + let plan = self + .launch_review + .as_ref() + .ok_or("Review the session before opening Terminal.")?; + plan.review.check(preview_token.as_deref(), &inputs)?; + let current = sessions::preview(std::path::Path::new(&project))?; + if current != plan.project { + return Err( + "The project changed since review. Review the session again.".into(), + ); + } + sessions::launch( + tool, + std::path::Path::new( + plan.project["project"] + .as_str() + .ok_or("Missing reviewed project.")?, + ), + &plan.model, + &plan.server_url, + plan.login, + plan.account.as_deref(), + plan.project["revision"] + .as_str() + .ok_or("Missing reviewed commit.")?, + ) + .map(Reply::message) + } + Action::PreviewLaunch { + tool, + account, + route, + id, + project, + } => { + if tool == Harness::CodexApp { + return Err("Use Codex app’s workspace controls or choose Codex CLI.".into()); + } + if harness::binary(tool).is_none() { + return Err("Install the selected coding tool first.".into()); + } + let inputs = + json!({"tool":tool,"account":account,"route":route,"id":id,"project":project}); + let review = Review::capture( + inputs.clone(), + vec![self.config.config_file.clone(), self.settings.clone()], + )?; + let app_config = Config::load(&self.settings)?; + review.check(Some(&review.token), &inputs)?; + if app_config.config_file != self.config.config_file { + return Err("Review these changes again before applying them.".into()); + } + let config = ServerConfig::parse(review.original()?).map_err( + |_| "The server config contains invalid TOML. Open it in Settings to fix it.", + )?; + let route = current_route(&config, &route, Some(&id))?; + let login = login_mode(tool, &config, &route)?; + let account = account_path(tool, account.as_deref())?; + let project_review = sessions::preview(std::path::Path::new(&project))?; + let mut data = project_review.clone(); + data["tool"] = json!(tool); + data["route"] = json!(route.id); + data["account_directory"] = json!(account); + data["server_url"] = json!(app_config.server_url); + data["login"] = json!(login); + if tool == Harness::Pi { + data["settings_notice"] = json!( + "Pi uses isolated session settings. Personal settings and extensions are not copied." + ); + } + review.check(Some(&review.token), &inputs)?; + data["preview_token"] = json!(review.token); + self.launch_review = Some(ReviewedLaunch { + review, + project: project_review, + model: route.id, + server_url: app_config.server_url, + login, + account, + }); + Ok(Reply { + message: String::new(), + data, + }) + } + Action::LoginAccount { tool, name } => { + let path = account_path(tool, Some(&name))?.ok_or("Choose an account.")?; + accounts::login(tool, &path, &name).map(Reply::message) + } + Action::PreviewAccountArchive { tool, name } => { + let path = account_path(tool, Some(&name))?.ok_or("Choose an account.")?; + let token = uuid::Uuid::now_v7().to_string(); + self.account_archive = Some((tool, name.clone(), token.clone())); + Ok(Reply { + message: String::new(), + data: json!({"name":name,"path":path,"preview_token":token,"notice":"Archive moves this account out of the picker and preserves all login files. Existing sessions using its old directory may need a new login."}), + }) + } + Action::ArchiveAccount { + tool, + name, + preview_token, + } => { + let (expected_tool, expected_name, token) = self + .account_archive + .as_ref() + .ok_or("Review account archive first.")?; + if tool != *expected_tool + || name != *expected_name + || preview_token.as_deref() != Some(token.as_str()) + { + return Err("Review account archive again.".into()); + } + let path = account_path(tool, Some(&name))?.ok_or("Choose an account.")?; + accounts::archive(tool, &path).map(Reply::message) + } + Action::OpenAccount { tool, name } => { + let path = account_path(tool, Some(&name))?.ok_or("Choose an account.")?; + server::command("open", &[&path.display().to_string()])?; + Ok(Reply::message("Opened the account folder.".into())) + } + Action::OpenSession { id } => { + let receipt = sessions::managed_session(&id)?; + server::command( + "open", + &[receipt["worktree"] + .as_str() + .ok_or("Missing session directory.")?], + )?; + Ok(Reply::message("Opened the session folder.".into())) + } + Action::PreviewSessionCleanup { id } => { + let data = sessions::cleanup_preview(&id)?; + let token = uuid::Uuid::now_v7().to_string(); + self.session_cleanup = Some((id, token.clone(), data.clone())); + let mut data = data; + data["preview_token"] = json!(token); + Ok(Reply { + message: String::new(), + data, + }) + } + Action::RemoveSession { id, preview_token } => { + let (expected_id, token, data) = self + .session_cleanup + .as_ref() + .ok_or("Review session cleanup first.")?; + if &id != expected_id || preview_token.as_deref() != Some(token.as_str()) { + return Err("Review session cleanup again.".into()); + } + sessions::cleanup(&id, data).map(Reply::message) + } + Action::OpenLogs {} => { + let path = crate::config::expand_home(std::path::Path::new("~/.switchyard/logs")); + server::command("open", &[&path.display().to_string()])?; + Ok(Reply::message("Opened Switchyard logs.".into())) + } + Action::Restart {} => server::restart_checked(&self.config).map(Reply::message), + Action::CheckConfig {} => server::check_config(&self.config) + .map(|()| Reply::message("Server config check passed.".into())), + Action::SavePreferences { + baseline_model, + refresh_seconds, + prices, + } => { + crate::config::save_preferences( + &self.settings, + &baseline_model, + refresh_seconds, + &prices, + )?; + self.reload()?; + Ok(Reply::message(format!( + "Changed {}.", + self.settings.display() + ))) + } + Action::RetrySettings {} => Ok(Reply::message("App settings loaded.".into())), + Action::OpenSettings {} => unreachable!(), + Action::PreviewUpdate {} => { + let mut data = + switchyard_desktop_install::preview_update(&install_metadata(&self.settings)?)?; + let token = uuid::Uuid::now_v7().to_string(); + let mut reviewed = data.clone(); + reviewed + .as_object_mut() + .ok_or("Invalid update preview.")? + .remove("status"); + self.update_review = Some((token.clone(), reviewed)); + data["preview_token"] = json!(token); + Ok(Reply { + message: String::new(), + data, + }) + } + Action::Update { preview_token } => { + let (token, reviewed) = self + .update_review + .as_ref() + .ok_or("Review the source update first.")?; + let mut current = + switchyard_desktop_install::preview_update(&install_metadata(&self.settings)?)?; + current + .as_object_mut() + .ok_or("Invalid update preview.")? + .remove("status"); + if preview_token.as_deref() != Some(token.as_str()) || ¤t != reviewed { + return Err( + "The source checkout or installation changed. Review the update again." + .into(), + ); + } + update(&self.settings).map(Reply::message) + } + Action::OpenUpdateLog {} => { + let metadata = + switchyard_desktop_install::Settings::load(&install_metadata(&self.settings)?)?; + let log = metadata.sy_home.join("logs/update.log"); + server::command("open", &[&log.display().to_string()])?; + Ok(Reply::message("Opened the update log.".into())) + } + Action::OpenConfig {} => { + server::command("open", &[&self.config.config_file.display().to_string()])?; + Ok(Reply::message( + "Opened the server config in your default editor.".into(), + )) + } + } + } +} +fn known_dependents(id: &str) -> Vec { + let mut dependents = Vec::new(); + for (tool, label) in harness::HARNESSES { + let mut destinations = vec![(None, harness::paths(*tool))]; + destinations.extend( + accounts::list(*tool) + .into_iter() + .map(|(name, path)| (Some(name), accounts::config_paths(*tool, Some(&path)))), + ); + for (account, files) in destinations { + if harness::inspect(*tool, &files) + .is_ok_and(|status| status.lines().next() == Some(format!("Model: {id}").as_str())) + { + dependents.push(json!({"tool":tool,"label":label,"account":account,"files":files})); + } + } + } + dependents +} + +fn algorithm_by_id(kind: &str) -> Result<&'static crate::server_config::Algorithm, String> { + ALGORITHMS + .iter() + .find(|a| a.kind == kind) + .ok_or_else(|| "Unknown routing algorithm.".into()) +} +fn current_route(config: &ServerConfig, key: &str, id: Option<&str>) -> Result { + config + .routes() + .into_iter() + .find(|r| r.key == key && id.is_none_or(|id| id == r.id)) + .ok_or_else(|| "The route changed or was removed. Refresh and choose it again.".into()) +} +// A custom file selects one settings location; saved accounts select their own folders. +fn install_paths( + tool: Harness, + account: Option<&str>, + file: Option<&str>, +) -> Result, String> { + if let Some(file) = file { + if account.is_some() { + return Err("Choose a saved account or a custom settings file, not both.".into()); + } + let path = PathBuf::from(file); + if !path.is_absolute() || file.trim().is_empty() || file.contains(['\n', '\r']) { + return Err("Enter an absolute settings file path without line breaks.".into()); + } + if path.is_dir() { + return Err("Choose a settings file, not a folder.".into()); + } + if tool == Harness::Pi { + if path.file_name().is_none_or(|name| name != "models.json") { + return Err("Choose Pi’s models.json; settings.json is read beside it.".into()); + } + return Ok(vec![path.clone(), path.with_file_name("settings.json")]); + } + return Ok(vec![path]); + } + let account = account_path(tool, account)?; + Ok(accounts::config_paths(tool, account.as_deref())) +} + +fn account_path(tool: Harness, name: Option<&str>) -> Result, String> { + let Some(name) = name else { + return Ok(None); + }; + let path = accounts::list(tool) + .into_iter() + .find(|(n, _)| n == name) + .map(|(_, p)| p) + .ok_or("Unknown account. Refresh and choose an existing account.")?; + accounts::validate(tool, &path)?; + Ok(Some(path)) +} +fn login_mode( + tool: Harness, + config: &ServerConfig, + route: &crate::server_config::Route, +) -> Result { + let clients = config.clients(); + let choices = config.choices(&route.key); + if choices.is_empty() { + return Err( + "This route cannot be installed by the app. Choose a route with models.".into(), + ); + } + let mut login = false; + for choice in choices { + let client = clients + .iter() + .find(|c| c.name == choice.client) + .ok_or("The route has an unknown endpoint.")?; + if client.forward_auth { + let url = url::Url::parse(&client.base_url) + .map_err(|_| "The route has an invalid endpoint URL.")?; + let host = url + .host_str() + .ok_or("The route has an invalid endpoint URL.")?; + let supported = url.scheme() == "https" + && url.port().is_none_or(|port| port == 443) + && match tool { + Harness::CodexCli | Harness::CodexApp => host == "chatgpt.com", + Harness::Claude => host == "api.anthropic.com", + Harness::Pi => false, + }; + if !supported { + return Err(format!( + "This route forwards the caller's login to {host}. Pick a matching coding tool or a route with server API credentials." + )); + } + login = true; + } + } + Ok(login) +} + +fn install_metadata(settings: &std::path::Path) -> Result { + let executable = std::env::current_exe().map_err(|e| e.to_string())?; + let bundle = switchyard_desktop_install::bundle_metadata(&executable); + let metadata = if bundle.is_file() { + bundle + } else { + settings + .parent() + .ok_or("The settings file has no parent directory.")? + .join("install.toml") + }; + Ok(metadata) +} +fn update(settings: &std::path::Path) -> Result { + let metadata = install_metadata(settings)?; + let log = switchyard_desktop_install::start_update(&metadata)?; + Ok(format!( + "Started rebuilding and reinstalling from the saved source checkout. Progress and errors are recorded in {}. The app restarts after a successful install.", + log.display() + )) +} + +#[cfg(test)] +mod tests { + use super::*; + // The subprocess isolates coding-tool discovery and every launch destination. + #[cfg(unix)] + #[test] + fn session_launch_requires_an_unchanged_review_before_creating_files() { + use std::os::unix::fs::PermissionsExt; + use std::{fs, process::Command}; + if let Some(home) = std::env::var_os("SWITCHYARD_LAUNCH_REVIEW_FIXTURE") { + let home = PathBuf::from(home); + let project = home.join("project"); + let git = |args: &[&str]| { + let result = Command::new("/usr/bin/git") + .arg("-C") + .arg(&project) + .args(args) + .output() + .expect("git"); + assert!( + result.status.success(), + "{}", + String::from_utf8_lossy(&result.stderr) + ); + }; + fs::create_dir(&project).expect("project"); + git(&["init"]); + git(&[ + "-c", + "user.name=Fixture", + "-c", + "user.email=fixture@example.test", + "-c", + "commit.gpgsign=false", + "commit", + "--allow-empty", + "-m", + "initial", + ]); + let settings = home.join("desktop.toml"); + let server = home.join("server.toml"); + let text = "[llm_clients.c]\nbase_url='http://localhost:1234'\n[targets.m]\nid='actual'\nllm_client='c'\n[routes.r]\nid='public'\ntype='passthrough'\ntarget='m'\n"; + fs::write(&server, text).expect("server"); + fs::write( + &settings, + format!( + "config_file={server:?}\nrouting_log={:?}\n", + home.join("routing.jsonl") + ), + ) + .expect("settings"); + let mut controller = + Controller::new(Config::load(&settings).expect("settings"), settings.clone()); + let preview = |controller: &mut Controller| { + controller + .dispatch(Action::PreviewLaunch { + tool: Harness::CodexCli, + account: None, + route: "r".into(), + id: "public".into(), + project: project.display().to_string(), + }) + .expect("preview") + }; + let launch = |token: Option, path: String| Action::Launch { + tool: Harness::CodexCli, + account: None, + route: "r".into(), + id: "public".into(), + project: path, + preview_token: token, + }; + let reviewed = preview(&mut controller); + let token = reviewed.data["preview_token"] + .as_str() + .expect("token") + .to_string(); + for rejected in [ + launch(None, project.display().to_string()), + launch(Some("wrong".into()), project.display().to_string()), + launch(Some(token.clone()), home.display().to_string()), + ] { + assert!(controller.dispatch(rejected).is_err()); + } + fs::write(&server, format!("{text}# external edit\n")).expect("edit"); + assert!( + controller + .dispatch(launch(Some(token), project.display().to_string())) + .is_err() + ); + assert_eq!( + fs::read_to_string(&server).expect("preserved"), + format!("{text}# external edit\n") + ); + let reviewed = preview(&mut controller); + let token = reviewed.data["preview_token"] + .as_str() + .expect("token") + .to_string(); + let original = fs::read_to_string(&settings).expect("settings"); + fs::write(&settings, format!("{original}# external settings\n")).expect("edit"); + assert!( + controller + .dispatch(launch(Some(token), project.display().to_string())) + .is_err() + ); + let reviewed = preview(&mut controller); + let token = reviewed.data["preview_token"] + .as_str() + .expect("token") + .to_string(); + git(&[ + "-c", + "user.name=Fixture", + "-c", + "user.email=fixture@example.test", + "-c", + "commit.gpgsign=false", + "commit", + "--allow-empty", + "-m", + "changed", + ]); + assert!( + controller + .dispatch(launch(Some(token), project.display().to_string())) + .err() + .expect("changed HEAD") + .contains("project changed") + ); + assert!(!home.join(".switchyard/worktrees").exists()); + assert!(!home.join("terminal-called").exists()); + assert_eq!(fs::read_dir(&home).expect("files").count(), 4); + return; + } + let home = tempfile::tempdir().expect("fixture"); + let bin = home.path().join("bin"); + fs::create_dir(&bin).expect("bin"); + for name in ["codex", "open", "osascript"] { + let path = bin.join(name); + fs::write( + &path, + "#!/bin/sh\ntouch \"$HOME/terminal-called\"\nexit 0\n", + ) + .expect("stub"); + fs::set_permissions(path, fs::Permissions::from_mode(0o755)).expect("executable"); + } + let output=Command::new(std::env::current_exe().expect("test binary")) + .args(["--exact", "controller::tests::session_launch_requires_an_unchanged_review_before_creating_files"]) + .env("SWITCHYARD_LAUNCH_REVIEW_FIXTURE",home.path()).env("HOME",home.path()) + .env("PATH",format!("{}:/usr/bin:/bin",bin.display())).output().expect("child"); + assert!( + output.status.success(), + "{}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + } + + #[test] + // An algorithm switch must not give stale choices a revision for externally edited bytes. + fn algorithm_switches_keep_the_first_revision_and_external_edits_reject_it() { + let dir = tempfile::tempdir().expect("fixture"); + let settings = dir.path().join("desktop.toml"); + let config = dir.path().join("server.toml"); + let text = "[llm_clients.c]\nbase_url='http://localhost:1234'\n[targets.m]\nid='actual'\nllm_client='c'\n[routes.r]\nid='public'\ntype='passthrough'\ntarget='m'\n"; + std::fs::write(&config, text).expect("config"); + std::fs::write( + &settings, + format!( + "config_file={config:?}\nrouting_log={:?}", + dir.path().join("log") + ), + ) + .expect("settings"); + let mut controller = Controller::new(Config::load(&settings).expect("load"), settings); + let first = controller + .dispatch(Action::Editor { + generation: 0, + route: "r".into(), + algorithm: "passthrough".into(), + }) + .expect("editor"); + let second = controller + .dispatch(Action::Editor { + generation: 0, + route: "r".into(), + algorithm: "random".into(), + }) + .expect("switch roles"); + assert_eq!(first.data["revision"], second.data["revision"]); + assert_eq!( + controller.snapshot().expect("snapshot")["routes"][0]["revision"], + first.data["revision"] + ); + std::fs::write(&config, format!("{text}# external edit\n")).expect("external edit"); + let latest = controller + .dispatch(Action::Editor { + generation: 0, + route: "r".into(), + algorithm: "passthrough".into(), + }) + .expect("new editor"); + assert_ne!(latest.data["revision"], first.data["revision"]); + let rejected = controller.dispatch(Action::PreviewRoute { + generation: 0, + route: "r".into(), + algorithm: "passthrough".into(), + choices: vec![Choice { + client: "c".into(), + model: "replacement".into(), + }], + revision: first.data["revision"].as_str().map(str::to_string), + }); + assert!(rejected.is_err()); + assert_eq!( + std::fs::read_to_string(&config).expect("config"), + format!("{text}# external edit\n") + ); + assert_eq!(std::fs::read_dir(dir.path()).expect("files").count(), 2); + } + + #[test] + // The review includes backup files because changed backups also change recovery behavior. + fn changed_review_files_and_inputs_reject_without_side_effects() { + let dir = tempfile::tempdir().expect("fixture"); + let settings = dir.path().join("tool.json"); + let backup = dir.path().join("backup"); + std::fs::write(&settings, "private-original").expect("settings"); + let inputs = json!({"tool":"claude","route":"first"}); + let review = Review::capture(inputs.clone(), vec![settings.clone(), backup.clone()]) + .expect("review"); + assert!(review.check(Some(&review.token), &inputs).is_ok()); + for token in [None, Some("unknown")] { + assert!(review.check(token, &inputs).is_err()); + } + assert!( + review + .check( + Some(&review.token), + &json!({"tool":"claude","route":"second"}) + ) + .is_err() + ); + std::fs::write(&settings, "external-edit").expect("external edit"); + assert!(review.check(Some(&review.token), &inputs).is_err()); + std::fs::write(&settings, "private-original").expect("restore fixture"); + std::fs::write(&backup, "new-backup").expect("new backup"); + assert!(review.check(Some(&review.token), &inputs).is_err()); + assert_eq!( + std::fs::read_to_string(&settings).expect("settings"), + "private-original" + ); + assert_eq!(std::fs::read_dir(dir.path()).expect("files").count(), 2); + } + + #[test] + // The Apply fixture supplies a generation so only its extra choice field causes rejection. + fn malformed_actions_reject_before_dispatch() { + for input in [ + r#"{"kind":"restart","command":"rm"}"#, + r#"{"kind":"install","tool":"unknown","route":"r","id":"m","account":null}"#, + r#"{"kind":"apply","generation":0,"route":"r","algorithm":"passthrough","choices":[{"client":"c","model":"m","extra":1}]}"#, + r#"{"kind":"unknown"}"#, + ] { + assert!(serde_json::from_str::(input).is_err()); + } + } + #[test] + // A custom destination must use the same files for preview, installation, backup, and restore. + fn custom_settings_are_validated_and_restored() { + let dir = tempfile::tempdir().expect("directory"); + for (tool, _) in harness::HARNESSES { + let file = dir.path().join(if *tool == Harness::Pi { + "models.json" + } else { + "custom-settings" + }); + assert_eq!( + install_paths(*tool, None, Some(file.to_str().expect("path"))).expect("custom")[0], + file + ); + } + for (account, file) in [ + (None, "relative.json"), + (None, ""), + (Some("account"), "/tmp/settings.json"), + (None, "/tmp/settings\n.json"), + ] { + assert!(install_paths(Harness::Claude, account, Some(file)).is_err()); + } + assert!(install_paths(Harness::Pi, None, Some("/tmp/settings.json")).is_err()); + assert!(install_paths(Harness::Claude, None, dir.path().to_str()).is_err()); + assert_eq!(std::fs::read_dir(dir.path()).expect("files").count(), 0); + let settings = dir.path().join("app.toml"); + let config = dir.path().join("server.toml"); + std::fs::write(&config,"[llm_clients.c]\nbase_url='https://example.com/v1'\nformat='openai_chat'\n[targets.m]\nid='actual'\nllm_client='c'\n[routes.r]\nid='public'\ntype='passthrough'\ntarget='m'\n").expect("config"); + std::fs::write( + &settings, + format!( + "config_file={config:?}\nrouting_log={:?}", + dir.path().join("log") + ), + ) + .expect("settings"); + let custom = dir.path().join("custom.json"); + std::fs::write(&custom, "{\"user\":true}").expect("custom"); + let mut controller = Controller::new(Config::load(&settings).expect("settings"), settings); + let file = Some(custom.display().to_string()); + let preview = controller + .dispatch(Action::PreviewInstall { + tool: Harness::Claude, + account: None, + settings_file: file.clone(), + route: "r".into(), + id: "public".into(), + }) + .expect("preview"); + assert!( + preview.data["changes"] + .as_array() + .expect("diff") + .iter() + .any(|change| change["file"] == custom.display().to_string()) + ); + std::fs::write(&custom, "{\"external\":true}").expect("external edit"); + let rejected = controller.dispatch(Action::Install { + tool: Harness::Claude, + account: None, + settings_file: file.clone(), + route: "r".into(), + id: "public".into(), + preview_token: preview.data["preview_token"].as_str().map(str::to_string), + }); + assert!(rejected.is_err()); + assert_eq!( + std::fs::read_to_string(&custom).expect("custom"), + "{\"external\":true}" + ); + assert!( + !custom + .with_file_name("custom.json.switchyard-original") + .exists() + ); + std::fs::write(&custom, "{\"user\":true}").expect("restore fixture"); + controller + .dispatch(Action::Install { + tool: Harness::Claude, + account: None, + settings_file: file.clone(), + route: "r".into(), + id: "public".into(), + preview_token: preview.data["preview_token"].as_str().map(str::to_string), + }) + .expect("install"); + assert_eq!( + preview.data["proposed"], + harness::inspect(Harness::Claude, std::slice::from_ref(&custom)).expect("inspect") + ); + let restore_preview = controller + .dispatch(Action::PreviewRestore { + tool: Harness::Claude, + account: None, + settings_file: file.clone(), + }) + .expect("restore preview"); + controller + .dispatch(Action::Restore { + tool: Harness::Claude, + account: None, + settings_file: file, + preview_token: restore_preview.data["preview_token"] + .as_str() + .map(str::to_string), + }) + .expect("restore"); + assert_eq!( + std::fs::read_to_string(custom).expect("custom"), + "{\"user\":true}" + ); + } + + #[cfg(any(target_os = "macos", windows))] + #[test] + // A cached list must not let an untested key reach Keychain. + fn submitting_a_key_checks_upstream_even_when_refresh_is_false() { + use std::io::{Read, Write}; + let dir = tempfile::tempdir().expect("directory"); + let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("listener"); + listener.set_nonblocking(true).expect("nonblocking"); + let url = format!("http://{}/v1", listener.local_addr().expect("address")); + #[cfg(windows)] + assert_eq!( + switchyard_desktop_install::windows::saved_key(&url).expect("no saved key"), + None + ); + let worker = std::thread::spawn(move || { + for _ in 0..200 { + if let Ok((mut stream, _)) = listener.accept() { + stream.set_nonblocking(false).expect("blocking request"); + stream + .set_read_timeout(Some(std::time::Duration::from_secs(1))) + .expect("timeout"); + let mut request = Vec::new(); + let mut chunk = [0; 1024]; + for _ in 0..4 { + let n = stream.read(&mut chunk).expect("request"); + request.extend_from_slice(&chunk[..n]); + if n == 0 || request.windows(4).any(|bytes| bytes == b"\r\n\r\n") { + break; + } + } + stream.write_all(b"HTTP/1.1 401 Unauthorized\r\nContent-Length: 0\r\nConnection: close\r\n\r\n").expect("response"); + return String::from_utf8_lossy(&request).into_owned(); + } + std::thread::sleep(std::time::Duration::from_millis(10)); + } + String::new() + }); + let settings = dir.path().join("settings.toml"); + let routes = dir.path().join("routes.toml"); + std::fs::write(&routes, format!("[llm_clients.c]\nbase_url={url:?}\nformat='openai_responses'\nforward_auth=true\n[targets.m]\nid='actual'\nllm_client='c'\n[routes.r]\nid='public'\ntype='passthrough'\ntarget='m'\n")).expect("routes"); + std::fs::write( + &settings, + format!( + "config_file={routes:?}\nrouting_log={:?}", + dir.path().join("log") + ), + ) + .expect("settings"); + let cached = + json!({format!("{url}/models"): {"models":["cached"],"fetched_at":models::now()}}) + .to_string(); + let cache = dir.path().join(models::CACHE_FILE); + std::fs::write(&cache, &cached).expect("cache"); + let mut controller = Controller::new(Config::load(&settings).expect("settings"), settings); + let cached_reply = controller + .dispatch(Action::Models { + client: "c".into(), + refresh: false, + key: None, + }) + .expect("cached list"); + assert_eq!(cached_reply.data["warning"], false); + assert_eq!(cached_reply.data["source"], "cache"); + // A failed refresh must not report the cached list as a successful upstream fetch. + let failed_refresh = controller + .dispatch(Action::Models { + client: "c".into(), + refresh: true, + key: None, + }) + .expect("cached fallback"); + assert_eq!(failed_refresh.data["warning"], true); + assert_eq!(failed_refresh.data["source"], "cache"); + assert_eq!(failed_refresh.data["models"], json!(["cached"])); + let result = controller.dispatch(Action::Models { + client: "c".into(), + refresh: false, + key: Some("rejected-fixture-key".into()), + }); + let request = worker.join().expect("worker"); + assert!(result.err().expect("rejected key").contains("401")); + assert!(request.starts_with("GET /v1/models ")); + assert!(request.contains("Authorization: Bearer rejected-fixture-key")); + assert_eq!(std::fs::read_to_string(cache).expect("cache"), cached); + #[cfg(windows)] + assert_eq!( + switchyard_desktop_install::windows::saved_key(&url).expect("rejected key not saved"), + None + ); + std::fs::write(&routes, "[llm_clients.c]\nbase_url='http://127.0.0.1:9/v1'\nformat='openai_responses'\n[targets.m]\nid='actual'\nllm_client='c'\n[routes.r]\nid='public'\ntype='passthrough'\ntarget='m'\n").expect("routes"); + let result = controller.dispatch(Action::Models { + client: "c".into(), + refresh: false, + key: Some("unused-fixture-key".into()), + }); + assert!( + result + .err() + .expect("unused key") + .contains("does not accept a model-list key") + ); + } + #[test] + #[cfg(not(any(target_os = "macos", windows)))] + // Rejecting a typed key must leave the upstream endpoint, model cache, and route config unchanged. + fn key_persistence_rejects_without_network_or_file_writes() { + let dir = tempfile::tempdir().expect("directory"); + let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("listener"); + listener.set_nonblocking(true).expect("nonblocking"); + let url = format!("http://{}/v1", listener.local_addr().expect("address")); + let settings = dir.path().join("settings.toml"); + let routes = dir.path().join("routes.toml"); + let text = format!( + "[llm_clients.c]\nbase_url={url:?}\nformat='openai_responses'\nforward_auth=true\n[targets.m]\nid='actual'\nllm_client='c'\n[routes.r]\nid='public'\ntype='passthrough'\ntarget='m'\n" + ); + std::fs::write(&routes, &text).expect("routes"); + std::fs::write( + &settings, + format!( + "config_file={routes:?}\nrouting_log={:?}", + dir.path().join("log") + ), + ) + .expect("settings"); + let mut controller = Controller::new(Config::load(&settings).expect("settings"), settings); + let result = controller.dispatch(Action::Models { + client: "c".into(), + refresh: true, + key: Some("fixture-key".into()), + }); + assert_eq!( + result.err(), + Some("Saving model-list keys requires macOS Keychain or Windows Credential Manager. On this platform, set the environment variable named by api_key_env and make it available to the app.".into()) + ); + assert_eq!( + listener.accept().expect_err("no upstream request").kind(), + std::io::ErrorKind::WouldBlock + ); + assert_eq!(std::fs::read_to_string(routes).expect("routes"), text); + assert_eq!(std::fs::read_dir(dir.path()).expect("files").count(), 2); + } + #[test] + fn usage_snapshot_survives_missing_and_malformed_route_configs() { + let dir = tempfile::tempdir().expect("directory"); + let settings = dir.path().join("settings.toml"); + let routes = dir.path().join("routes.toml"); + let log = dir.path().join("log"); + std::fs::write( + &log, + "{\"ts\":\"2026-10-08T12:00:00Z\",\"model\":\"actual\",\"session_id\":\"session\"}\n", + ) + .expect("log"); + std::fs::write( + &settings, + format!("config_file={routes:?}\nrouting_log={log:?}"), + ) + .expect("settings"); + let mut controller = Controller::new(Config::load(&settings).expect("settings"), settings); + for malformed in [false, true] { + if malformed { + std::fs::write(&routes, "secret = 'UNTERMINATED_PRIVATE_VALUE").expect("routes"); + } + let snapshot = controller.snapshot().expect("snapshot"); + assert_eq!(snapshot["sessions"], json!(["session"])); + assert_eq!(snapshot["entries"][0]["model"], "actual"); + assert_eq!( + snapshot["analytics"]["all"]["routed"]["actual"]["requests"], + 1 + ); + assert!(snapshot["estimates"]["all"]["cost"].is_null()); + assert!(!snapshot["errors"].as_array().expect("errors").is_empty()); + assert!(!snapshot.to_string().contains("PRIVATE_VALUE")); + } + } + #[test] + // Matching route names in two files must not let an old draft edit the newly selected file. + fn stale_editor_rejects_after_settings_change_without_writes() { + let dir = tempfile::tempdir().expect("directory"); + let settings = dir.path().join("settings.toml"); + let first = dir.path().join("first.toml"); + let second = dir.path().join("second.toml"); + let text = "[llm_clients.c]\nbase_url='https://example.com/v1'\nformat='openai_responses'\n[targets.m]\nid='actual'\nllm_client='c'\n[routes.r]\nid='public'\ntype='passthrough'\ntarget='m'\n"; + std::fs::write(&first, text).expect("first"); + std::fs::write(&second, text).expect("second"); + let write_settings = |path: &std::path::Path| { + std::fs::write( + &settings, + format!( + "config_file={path:?}\nrouting_log={:?}", + dir.path().join("log") + ), + ) + .expect("settings"); + }; + write_settings(&first); + let mut controller = + Controller::new(Config::load(&settings).expect("settings"), settings.clone()); + let snapshot = controller.snapshot().expect("snapshot"); + assert_eq!(snapshot["routes"][0]["tiers"], json!(["Capable"])); + let generation = snapshot["generation"].as_u64().expect("generation"); + write_settings(&second); + for action in [ + Action::Editor { + generation, + route: "r".into(), + algorithm: "passthrough".into(), + }, + Action::Apply { + generation, + route: "r".into(), + algorithm: "passthrough".into(), + choices: vec![Choice { + client: "c".into(), + model: "changed".into(), + }], + revision: None, + }, + ] { + assert!(controller.dispatch(action).is_err()); + } + assert_eq!(std::fs::read_to_string(&second).expect("second"), text); + assert_eq!(std::fs::read_dir(dir.path()).expect("files").count(), 3); + assert_eq!( + controller.snapshot().expect("snapshot")["generation"], + generation + 1 + ); + } + #[test] + fn invalid_route_choices_reject_before_saving_or_restarting() { + let dir = tempfile::tempdir().expect("directory"); + let settings = dir.path().join("settings.toml"); + let config = dir.path().join("server.toml"); + let original = "[llm_clients.c]\nbase_url='http://localhost:1234'\n[targets.m]\nid='actual'\nllm_client='c'\n[routes.r]\nid='public'\ntype='passthrough'\ntarget='m'\n"; + std::fs::write(&config, original).expect("config"); + std::fs::write(&settings, format!("config_file={config:?}")).expect("settings"); + let mut controller = Controller::new(Config::load(&settings).expect("settings"), settings); + let editor = controller + .dispatch(Action::Editor { + generation: 0, + route: "r".into(), + algorithm: "passthrough".into(), + }) + .expect("editor"); + let revision = editor.data["revision"].as_str().map(str::to_string); + for (choices, expected) in [ + (vec![], "passthrough needs 1 models, not 0."), + ( + vec![Choice { + client: "c".into(), + model: " ".into(), + }], + "Pick a model for Model.", + ), + ( + vec![Choice { + client: "unknown".into(), + model: "model".into(), + }], + "The config has no LLM client named \"unknown\".", + ), + ] { + let error = controller + .dispatch(Action::Apply { + generation: 0, + route: "r".into(), + algorithm: "passthrough".into(), + choices, + revision: revision.clone(), + }) + .err() + .expect("rejection"); + assert_eq!(error, expected); + assert_eq!(std::fs::read_to_string(&config).expect("config"), original); + assert_eq!(std::fs::read_dir(dir.path()).expect("files").count(), 2); + } + } + + #[test] + fn unsupported_routes_are_read_only_before_any_write() { + let dir = tempfile::tempdir().expect("directory"); + let config = dir.path().join("server.toml"); + let settings = dir.path().join("app.toml"); + std::fs::write( + &settings, + format!("config_file={config:?}\nserver_url='http://127.0.0.1:0'"), + ) + .expect("settings"); + let mut controller = Controller::new(Config::load(&settings).expect("settings"), settings); + // These fixtures use valid server algorithms that the app cannot edit. + for fields in [ + "type='noop'", + "type='prefill_router'\ntargets=['answer']\ncheckpoint='router.pt'", + "type='llm_classifier'\nmode='custom'\nmodels={judge=['judge'],any=['answer']}\ndefault_target='any'\nprompt='Select a target.'\nresponse_schema='{\"type\":\"object\"}'\npolicy={type='target_selector',selector='/target'}", + ] { + let _: switchyard_runner::AlgorithmSpec = + toml::from_str(fields).expect("server route schema"); + let original = format!("[routes.r]\nid='public'\n{fields}\n"); + std::fs::write(&config, &original).expect("config"); + assert_eq!( + controller.snapshot().expect("snapshot")["routes"][0]["editable"], + false + ); + for kind in ["editor", "apply"] { + let mut raw = + json!({"kind":kind,"generation":0,"route":"r","algorithm":"passthrough"}); + if kind == "apply" { + raw["choices"] = json!([]); + } + let action = serde_json::from_value(raw).expect("action"); + assert_eq!( + controller.dispatch(action).err().expect("read only"), + "This route must be edited in the server config file." + ); + } + assert_eq!(std::fs::read_to_string(&config).expect("config"), original); + assert_eq!(std::fs::read_dir(dir.path()).expect("files").count(), 2); + } + } + + #[test] + fn rejected_install_has_no_writes() { + let dir = tempfile::tempdir().expect("directory"); + let settings = dir.path().join("settings.toml"); + let config = dir.path().join("server.toml"); + let text = "[llm_clients.c]\nbase_url='http://chatgpt.com/backend-api/codex'\nforward_auth=true\nformat='openai_responses'\n[targets.m]\nid='actual'\nllm_client='c'\n[routes.r]\nid='public'\ntype='passthrough'\ntarget='m'\n"; + std::fs::write(&config, text).expect("config"); + std::fs::write( + &settings, + format!( + "config_file={:?}\nrouting_log={:?}", + config, + dir.path().join("log") + ), + ) + .expect("settings"); + let mut controller = Controller::new(Config::load(&settings).expect("settings"), settings); + for id in ["old", "public"] { + assert!( + controller + .dispatch(Action::Install { + tool: Harness::CodexCli, + account: None, + settings_file: None, + route: "r".into(), + id: id.into(), + preview_token: None, + }) + .is_err() + ); + assert_eq!(std::fs::read_to_string(&config).expect("config"), text); + assert_eq!(std::fs::read_dir(dir.path()).expect("files").count(), 2); + } + let snapshot = controller.snapshot().expect("snapshot"); + assert!( + snapshot["routes"][0]["label"] + .as_str() + .expect("label") + .contains("passthrough · c / actual") + ); + assert!(!snapshot.to_string().contains("backend-api")); + } +} diff --git a/crates/switchyard-desktop/src/gui.rs b/crates/switchyard-desktop/src/gui.rs new file mode 100644 index 0000000000..d1518a90ad --- /dev/null +++ b/crates/switchyard-desktop/src/gui.rs @@ -0,0 +1,349 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Tauri manages the window and tray, and runs blocking operations on worker threads. + +use crate::controller::{Action, Controller, Reply}; +use std::sync::{ + Arc, Mutex, + atomic::{AtomicU8, Ordering}, +}; +use tauri::{ + Manager, State, + menu::{Menu, MenuItem, PredefinedMenuItem}, + tray::TrayIconBuilder, +}; + +struct Shared { + controller: Arc>, + // The state is idle (0), busy (1), closing (2), or busy with Quit queued (3). + // The idle state accepts new operations. + operation: Arc, +} +struct TrayPreview { + menu: Menu, + models: Mutex>, + health: MenuItem, +} +impl TrayPreview { + fn update(&self, app: &tauri::AppHandle, snapshot: &serde_json::Value) -> Result<(), String> { + let health = &snapshot["data_state"]["health"]; + self.health + .set_text(format!( + "Server: {} · checked {}", + health["state"].as_str().unwrap_or("unknown"), + health["checked_at"].as_str().unwrap_or("not yet") + )) + .map_err(|e| e.to_string())?; + let totals: crate::rollup::Totals = + serde_json::from_value(snapshot["analytics"]["all"].clone()) + .map_err(|error| error.to_string())?; + for item in self.menu.items().map_err(|error| error.to_string())? { + if item.id().as_ref().starts_with("model:") { + self.menu.remove(&item).map_err(|error| error.to_string())?; + } + } + let mut counts = totals.routed.clone(); + for (model, overhead) in &totals.classifier { + let row = counts.entry(model.clone()).or_default(); + row.input += overhead.input; + row.cached_input += overhead.cached_input; + row.output += overhead.output; + } + let mut models: Vec<_> = counts.into_iter().collect(); + models.sort_by(|a, b| b.1.total().cmp(&a.1.total()).then_with(|| a.0.cmp(&b.0))); + *self.models.lock().map_err(|e| e.to_string())? = + models.into_iter().take(3).map(|(name, _)| name).collect(); + let rows = if snapshot["data_state"]["usage"]["state"] == "unavailable" { + vec!["Usage unavailable; open Overview…".to_string()] + } else { + crate::summary::tray_models(&totals) + }; + for (index, label) in rows.into_iter().enumerate() { + let item = MenuItem::with_id(app, format!("model:{index}"), label, true, None::<&str>) + .map_err(|error| error.to_string())?; + self.menu + .insert(&item, 5 + index) + .map_err(|error| error.to_string())?; + } + Ok(()) + } +} +// Operation keeps the app busy from queueing through worker completion. +// Drop returns a busy app to idle or closes it when Quit is queued. +struct Operation(Arc, Option); +impl Drop for Operation { + fn drop(&mut self) { + if self + .0 + .compare_exchange(1, 0, Ordering::AcqRel, Ordering::Acquire) + == Err(3) + { + self.0.store(2, Ordering::Release); + if let Some(app) = &self.1 { + app.exit(0); + } + } + } +} +impl Shared { + fn begin(&self) -> Result { + self.operation + .compare_exchange(0, 1, Ordering::AcqRel, Ordering::Acquire) + .map_err(|_| "An operation is running. Try again when it finishes.".to_string())?; + Ok(Operation(self.operation.clone(), None)) + } + fn quit(&self) -> bool { + if self + .operation + .compare_exchange(1, 3, Ordering::AcqRel, Ordering::Acquire) + .is_ok() + { + return false; + } + self.operation + .compare_exchange(0, 2, Ordering::AcqRel, Ordering::Acquire) + .is_ok() + || self.operation.load(Ordering::Acquire) == 2 + } +} + +#[tauri::command] +async fn snapshot( + app: tauri::AppHandle, + state: State<'_, Shared>, +) -> Result { + let mut operation = state.begin()?; + operation.1 = Some(app.clone()); + let shared = state.controller.clone(); + let snapshot = tauri::async_runtime::spawn_blocking(move || { + let _operation = operation; + shared + .try_lock() + .map_err(|_| "An operation is running. Try again when it finishes.".to_string())? + .snapshot() + }) + .await + .map_err(|e| e.to_string())??; + app.state::().update(&app, &snapshot)?; + Ok(snapshot) +} +#[tauri::command] +async fn action( + app: tauri::AppHandle, + state: State<'_, Shared>, + action: Action, +) -> Result { + let mut operation = state.begin()?; + operation.1 = Some(app.clone()); + let shared = state.controller.clone(); + tauri::async_runtime::spawn_blocking(move || { + let _operation = operation; + shared + .try_lock() + .map_err(|_| "An operation is running. Try again when it finishes.".to_string())? + .dispatch(action) + }) + .await + .map_err(|e| e.to_string())? +} +#[tauri::command] +async fn choose_path(directory: bool) -> Result, String> { + // The blocking worker waits for the picker; rfd opens macOS panels on the main thread. + tauri::async_runtime::spawn_blocking(move || { + let dialog = rfd::FileDialog::new().set_title(if directory { + "Choose a Git project folder" + } else { + "Choose coding-tool settings" + }); + let path = if directory { + dialog.pick_folder() + } else { + dialog.pick_file() + }; + #[cfg(windows)] + { + Ok(path.map(|path| path.display().to_string())) + } + #[cfg(target_os = "macos")] + { + path.map(|path| { + path.into_os_string().into_string().map_err(|_| { + "The selected path cannot be represented as UTF-8. Enter a different path in the field." + .to_string() + }) + }) + .transpose() + } + }) + .await + .map_err(|error| error.to_string())? +} + +fn open_page(app: &tauri::AppHandle, page: &str) { + if let Some(window) = app.get_webview_window("main") { + // Only the fixed page names from the tray menu reach this script. + let script = format!( + "window.dispatchEvent(new CustomEvent('switchyard-page', {{detail:{page:?}}}))" + ); + if let Err(error) = window.eval(&script) { + eprintln!("switchyard-desktop: could not select the page: {error}"); + } + } + show(app); +} +fn activation(app: &tauri::AppHandle, visible: bool) -> tauri::Result<()> { + #[cfg(target_os = "macos")] + { + app.set_activation_policy(if visible { + tauri::ActivationPolicy::Regular + } else { + tauri::ActivationPolicy::Accessory + }) + } + #[cfg(not(target_os = "macos"))] + { + let _ = (app, visible); + Ok(()) + } +} +fn show(app: &tauri::AppHandle) { + if let Some(window) = app.get_webview_window("main") + && let Err(e) = activation(app, true) + .and_then(|()| window.show()) + .and_then(|()| window.set_focus()) + { + eprintln!("switchyard-desktop: could not open the window: {e}"); + } +} +pub fn run(controller: Controller) -> Result<(), String> { + let app = tauri::Builder::default() + .manage(Shared { + controller: Arc::new(Mutex::new(controller)), + operation: Arc::new(AtomicU8::new(0)), + }) + .invoke_handler(tauri::generate_handler![snapshot, action, choose_path]) + .setup(|app| { + let open = MenuItem::with_id(app, "open", "Open Switchyard", true, None::<&str>)?; + let install = MenuItem::with_id(app, "install", "Connect tools…", true, None::<&str>)?; + let health = MenuItem::with_id(app,"health","Server: checking…",false,None::<&str>)?; + let period = MenuItem::with_id( + app, + "period", + "Models · all retained history", + false, + None::<&str>, + )?; + let usage = MenuItem::with_id(app, "usage", "View model usage…", true, None::<&str>)?; + let settings = MenuItem::with_id(app, "settings", "Settings…", true, None::<&str>)?; + let quit = MenuItem::with_id(app, "quit", "Quit Switchyard", true, None::<&str>)?; + let separator = PredefinedMenuItem::separator(app)?; + let bottom_separator = PredefinedMenuItem::separator(app)?; + let menu = Menu::with_items( + app, + &[ + &open, + &install, + &separator, + &health, + &period, + &usage, + &bottom_separator, + &settings, + &quit, + ], + )?; + app.manage(TrayPreview { menu: menu.clone(), models: Mutex::new(Vec::new()), health }); + if let Ok(snapshot) = app + .state::() + .controller + .lock() + .map_err(|e| e.to_string())? + .snapshot() + { + app.state::().update(app.handle(), &snapshot)?; + } + #[cfg(target_os = "macos")] + let tray_pixels = include_bytes!("../icons/tray.rgba"); + #[cfg(windows)] + let tray_pixels = include_bytes!("../icons/tray-windows.rgba"); + TrayIconBuilder::new() + .icon(tauri::image::Image::new_owned( + tray_pixels.to_vec(), + 22, + 22, + )) + .icon_as_template(cfg!(target_os = "macos")) + .tooltip("Switchyard — routes and usage") + .menu(&menu) + .on_menu_event(|app, event| match event.id.as_ref() { + "open" => open_page(app, "overview"), + "install" => open_page(app, "install"), + "usage" => open_page(app, "overview"), + id if id.starts_with("model:") => { + let model = id.strip_prefix("model:").and_then(|index|index.parse::().ok()).and_then(|index|app.state::().models.lock().ok()?.get(index).cloned()); + if let Some(window)=app.get_webview_window("main") { + let detail=serde_json::json!({"page":"overview","model":model}); + let _=window.eval(format!("window.dispatchEvent(new CustomEvent('switchyard-page',{{detail:{detail}}}))").as_str()); + } + show(app); + }, + "settings" => open_page(app, "settings"), + "quit" => { + if app.state::().quit() { app.exit(0); } + else if let Some(window)=app.get_webview_window("main") { + let _=window.eval("window.dispatchEvent(new CustomEvent('switchyard-notice',{detail:'Switchyard will quit when the current operation finishes.'}))"); + show(app); + } + }, + _ => {} + }) + .build(app)?; + Ok(()) + }) + .on_window_event(|window, event| { + if let tauri::WindowEvent::CloseRequested { api, .. } = event { + api.prevent_close(); + // Accessory removes the running Dock entry while keeping the tray menu. + if let Err(e) = window.hide().and_then(|()| { + activation(window.app_handle(), false) + }) { + eprintln!("switchyard-desktop: could not hide the window: {e}"); + } + } + }) + .build(tauri::generate_context!()) + .map_err(|e| e.to_string())?; + app.run(|app, event| match event { + #[cfg(target_os = "macos")] + tauri::RunEvent::Reopen { .. } => show(app), + tauri::RunEvent::ExitRequested { api, .. } if !app.state::().quit() => { + api.prevent_exit() + } + _ => {} + }); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + // A queued operation must prevent Quit before its worker starts. + fn quit_blocks_pending_operations_and_stays_closed() { + let shared = Shared { + controller: Arc::new(Mutex::new(Controller::new( + crate::config::Config::default(), + std::path::PathBuf::new(), + ))), + operation: Arc::new(AtomicU8::new(0)), + }; + let operation = shared.begin().expect("operation"); + assert!(shared.begin().is_err()); + assert!(!shared.quit()); + drop(operation); + assert!(shared.quit()); + assert!(shared.quit()); + assert!(shared.begin().is_err()); + } +} diff --git a/crates/switchyard-desktop/src/harness.rs b/crates/switchyard-desktop/src/harness.rs new file mode 100644 index 0000000000..7b2872b684 --- /dev/null +++ b/crates/switchyard-desktop/src/harness.rs @@ -0,0 +1,1335 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! This module installs and restores user settings for the supported coding tools. + +use serde_json::{Value, json}; +use std::io::Write; +use std::path::{Path, PathBuf}; +use toml_edit::{DocumentMut, value}; + +pub type ReviewedFile = (PathBuf, Option>); + +#[derive(Clone, Copy, Debug, PartialEq, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "snake_case")] +pub enum Harness { + CodexCli, + CodexApp, + Claude, + Pi, +} + +pub const HARNESSES: &[(Harness, &str)] = &[ + (Harness::CodexCli, "Codex CLI profile"), + (Harness::CodexApp, "Codex app and CLI defaults"), + (Harness::Claude, "Claude Code"), + (Harness::Pi, "Pi"), +]; + +pub fn paths(tool: Harness) -> Vec { + let home = crate::config::expand_home(Path::new("~")); + let codex = std::env::var_os("CODEX_HOME") + .map(PathBuf::from) + .unwrap_or_else(|| home.join(".codex")); + let claude = std::env::var_os("CLAUDE_CONFIG_DIR") + .map(PathBuf::from) + .unwrap_or_else(|| home.join(".claude")); + let pi = std::env::var_os("PI_CODING_AGENT_DIR") + .map(PathBuf::from) + .unwrap_or_else(|| home.join(".pi/agent")); + match tool { + Harness::CodexCli => vec![codex.join("sy.config.toml")], + Harness::CodexApp => vec![codex.join("config.toml")], + Harness::Claude => vec![claude.join("settings.json")], + Harness::Pi => vec![pi.join("models.json"), pi.join("settings.json")], + } +} + +pub fn inspect(tool: Harness, files: &[PathBuf]) -> Result { + let texts = files + .iter() + .map(|path| read(path)) + .collect::, _>>()?; + inspect_settings(tool, files, &texts) +} + +fn inspect_settings(tool: Harness, files: &[PathBuf], texts: &[String]) -> Result { + let text = &texts[0]; + let model; + let endpoint; + match tool { + Harness::CodexCli | Harness::CodexApp => { + let doc = text + .parse::() + .map_err(|e| e.message().to_owned())?; + model = doc + .get("model") + .and_then(|v| v.as_str()) + .unwrap_or("Default") + .to_string(); + let provider = doc + .get("model_provider") + .and_then(|v| v.as_str()) + .unwrap_or("openai"); + endpoint = doc + .get("model_providers") + .and_then(|v| v.get(provider)) + .and_then(|v| v.get("base_url")) + .and_then(|v| v.as_str()) + .unwrap_or("Provider default") + .to_string(); + } + Harness::Claude => { + let doc = object(text)?; + model = doc + .pointer("/env/ANTHROPIC_MODEL") + .and_then(Value::as_str) + .or_else(|| doc.get("model").and_then(Value::as_str)) + .unwrap_or("Default") + .to_string(); + endpoint = doc + .pointer("/env/ANTHROPIC_BASE_URL") + .and_then(Value::as_str) + .unwrap_or("Anthropic default") + .to_string(); + } + Harness::Pi => { + let doc = object(&texts[1])?; + model = doc + .get("defaultModel") + .and_then(Value::as_str) + .unwrap_or("Default") + .to_string(); + let provider = doc + .get("defaultProvider") + .and_then(Value::as_str) + .unwrap_or("Default"); + let models = object(text)?; + endpoint = models + .get("providers") + .and_then(|v| v.get(provider)) + .and_then(|v| v.get("baseUrl")) + .and_then(Value::as_str) + .unwrap_or("Provider default") + .to_string(); + } + } + // The display keeps the URL scheme, host, and port to omit credentials and queries. + let endpoint = url::Url::parse(&endpoint) + .ok() + .map(|u| { + format!( + "{}://{}{}", + u.scheme(), + u.host_str().unwrap_or("?"), + u.port().map(|p| format!(":{p}")).unwrap_or_default() + ) + }) + .unwrap_or_else(|| "Provider default".into()); + Ok(format!( + "Model: {model}\nEndpoint: {endpoint}\nSettings: {}", + files + .iter() + .map(|file| file.display().to_string()) + .collect::>() + .join("\n ") + )) +} + +fn read_file(path: &Path) -> Result, String> { + match std::fs::symlink_metadata(path) { + Ok(meta) if meta.file_type().is_symlink() => { + return Err(format!( + "{} is a symlink. Edit its target yourself.", + path.display() + )); + } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None), + Err(e) => return Err(e.to_string()), + _ => {} + } + std::fs::read_to_string(path) + .map(Some) + .map_err(|e| format!("Read {}: {e}", path.display())) +} + +fn read(path: &Path) -> Result { + Ok(read_file(path)?.unwrap_or_default()) +} + +fn object(text: &str) -> Result { + let doc: Value = if text.is_empty() { + json!({}) + } else { + serde_json::from_str(text).map_err(|e| e.to_string())? + }; + if !doc.is_object() { + return Err("Settings must be a JSON object.".into()); + } + Ok(doc) +} + +fn map<'a>(doc: &'a mut Value, key: &str) -> Result<&'a mut Value, String> { + if doc.get(key).is_none() { + doc[key] = json!({}); + } + if !doc[key].is_object() { + return Err(format!("{key} must be a JSON object.")); + } + Ok(&mut doc[key]) +} + +pub fn validate(tool: Harness, root: &str, model: &str, login: bool) -> Result { + let url = url::Url::parse(root).map_err(|e| e.to_string())?; + if url.scheme() != "http" + || !matches!(url.host_str(), Some("localhost" | "127.0.0.1" | "[::1]")) + || !url.username().is_empty() + || url.password().is_some() + || url.query().is_some() + || url.fragment().is_some() + || url.path() != "/" + { + return Err( + "Install requires a local HTTP server URL, such as http://127.0.0.1:4123.".into(), + ); + } + if model.trim().is_empty() { + return Err("Pick a route first.".into()); + } + if tool == Harness::Pi && login { + return Err("Pi's custom provider needs a route with server API credentials. Use Codex or Claude for subscription login forwarding.".into()); + } + // validate returns the parsed URL without a trailing slash so callers can append API paths. + Ok(url.to_string().trim_end_matches('/').to_owned()) +} + +fn codex_settings(text: &str, root: &str, model: &str, login: bool) -> Result { + let mut doc = text + .parse::() + .map_err(|e| e.message().to_owned())?; + if doc + .get("model_providers") + .is_some_and(|item| item.as_table_like().is_none()) + { + return Err("model_providers must be a TOML table.".into()); + } + if doc + .get("model_providers") + .and_then(|v| v.get("sy")) + .is_some_and(|item| item.as_table_like().is_none()) + { + return Err("sy must be a TOML provider table.".into()); + } + doc["model"] = value(model); + doc["model_provider"] = value("sy"); + doc["model_providers"]["sy"]["name"] = value("Switchyard"); + doc["model_providers"]["sy"]["base_url"] = value(format!("{root}/v1")); + doc["model_providers"]["sy"]["wire_api"] = value("responses"); + doc["model_providers"]["sy"]["requires_openai_auth"] = value(login); + // A stale env_key would make Codex require a local API key even when the + // server owns the credentials or forwards the coding tool’s subscription login. + doc["model_providers"]["sy"] + .as_table_like_mut() + .ok_or("sy must be a provider table")? + .remove("env_key"); + Ok(doc.to_string()) +} + +fn claude_settings(text: &str, root: &str, model: &str, login: bool) -> Result { + let mut doc = object(text)?; + let env = map(&mut doc, "env")?; + let role_models = [ + "ANTHROPIC_DEFAULT_OPUS_MODEL", + "ANTHROPIC_DEFAULT_SONNET_MODEL", + "ANTHROPIC_DEFAULT_HAIKU_MODEL", + ]; + // The complete installation signature distinguishes our placeholder from a user credential. + let prior_switchyard = env["ANTHROPIC_BASE_URL"].as_str() == Some(root) + && env["ANTHROPIC_AUTH_TOKEN"].as_str() == Some("switchyard-local") + && env["ANTHROPIC_API_KEY"].as_str() == Some("") + && env["ANTHROPIC_MODEL"] + .as_str() + .is_some_and(|model| !model.is_empty()) + && role_models + .iter() + .all(|key| env[*key] == env["ANTHROPIC_MODEL"]); + env["ANTHROPIC_BASE_URL"] = json!(root); + env["ANTHROPIC_MODEL"] = json!(model); + // All model roles use the selected public route, including subagents. + for key in role_models { + env[key] = json!(model); + } + if login { + if prior_switchyard { + let values = env.as_object_mut().ok_or("env must be a JSON object.")?; + values.remove("ANTHROPIC_AUTH_TOKEN"); + values.remove("ANTHROPIC_API_KEY"); + } + for key in ["ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_API_KEY"] { + if env.get(key).is_some_and(|value| value.as_str() != Some("")) { + return Err(format!( + "{key} overrides subscription login. Remove it from Claude settings before installing this route." + )); + } + } + } else { + env["ANTHROPIC_AUTH_TOKEN"] = json!("switchyard-local"); + env["ANTHROPIC_API_KEY"] = json!(""); + } + Ok(serde_json::to_string_pretty(&doc).map_err(|e| e.to_string())? + "\n") +} + +// The preview uses the installation transformations without creating files or backups. +pub fn preview( + tool: Harness, + files: &[PathBuf], + root: &str, + model: &str, + login: bool, +) -> Result { + let root = validate(tool, root, model, login)?; + for path in files { + if read_file(&restore_pending_path(path))?.is_some() { + return Err("A previous restore still needs backup cleanup. Retry Restore before connecting another route.".into()); + } + } + let originals = files + .iter() + .map(|path| read_file(path)) + .collect::, _>>()?; + let incoming = configured(tool, &originals, &root, model, login)?; + let current = originals + .iter() + .map(|text| text.clone().unwrap_or_default()) + .collect::>(); + Ok(json!({ + "changes": settings_changes(tool, files, ¤t, &incoming)?, + "current": inspect_settings(tool, files, ¤t)?, + "proposed": inspect_settings(tool, files, &incoming)?, + "authentication": if login { "This route uses the coding tool’s subscription login." } else { "This route uses the API credentials configured on the Switchyard server." }, + "files": files.iter().map(|path| path.display().to_string()).collect::>(), + "file_actions": files.iter().zip(originals.iter().zip(&incoming)).map(|(path,(before,after))|json!({"file":path,"action":if before.as_deref()==Some(after.as_str()) {"preserve"} else if before.is_none() {"create"} else {"update"}})).collect::>() + })) +} + +// PreviewModel and its nested types omit unknown fields that could contain credentials. +#[derive(serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +struct PreviewModel { + id: Option, + name: Option, + reasoning: Option, + input: Option>, + context_window: Option, + max_tokens: Option, + cost: Option, +} +#[derive(serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "lowercase")] +enum PreviewInput { + Text, + Image, +} +#[derive(serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +struct PreviewCosts { + input: Option, + output: Option, + cache_read: Option, + cache_write: Option, +} + +// settings_changes compares supported routing fields and hides credential values before display. +fn settings_changes( + tool: Harness, + files: &[PathBuf], + before: &[String], + after: &[String], +) -> Result, String> { + let mut changes = Vec::new(); + for (index, file) in files.iter().enumerate() { + let decode = |text: &str| -> Result { + if matches!(tool, Harness::CodexCli | Harness::CodexApp) { + let doc: toml::Value = toml::from_str(text).map_err(|e| e.to_string())?; + serde_json::to_value(doc).map_err(|e| e.to_string()) + } else { + object(text) + } + }; + let old = decode(&before[index])?; + let new = decode(&after[index])?; + let keys: &[&str] = match tool { + Harness::CodexCli | Harness::CodexApp => &[ + "/model", + "/model_provider", + "/model_providers/sy/name", + "/model_providers/sy/env_key", + "/model_providers/sy/base_url", + "/model_providers/sy/requires_openai_auth", + "/model_providers/sy/wire_api", + ], + Harness::Claude => &[ + "/model", + "/env/ANTHROPIC_MODEL", + "/env/ANTHROPIC_DEFAULT_OPUS_MODEL", + "/env/ANTHROPIC_DEFAULT_SONNET_MODEL", + "/env/ANTHROPIC_DEFAULT_HAIKU_MODEL", + "/env/ANTHROPIC_BASE_URL", + "/env/ANTHROPIC_AUTH_TOKEN", + "/env/ANTHROPIC_API_KEY", + ], + Harness::Pi if index == 0 => &[ + "/providers/switchyard/baseUrl", + "/providers/switchyard/api", + "/providers/switchyard/apiKey", + "/providers/switchyard/headers", + "/providers/switchyard/models", + ], + Harness::Pi => &["/defaultModel", "/defaultProvider"], + }; + for key in keys { + let left = old.pointer(key); + let right = new.pointer(key); + if left == right { + continue; + } + let display = |value: Option<&Value>| -> String { + let Some(value) = value else { + return "(unset)".into(); + }; + match *key { + "/env/ANTHROPIC_AUTH_TOKEN" + | "/env/ANTHROPIC_API_KEY" + | "/providers/switchyard/apiKey" => if value.as_str() == Some("") { + "(empty)" + } else { + "(set; hidden)" + } + .into(), + "/providers/switchyard/headers" => value + .as_object() + .map(|headers| { + headers + .keys() + .map(|name| format!("{name}: (set; hidden)")) + .collect::>() + .join(", ") + }) + .unwrap_or_else(|| "(configured headers; values hidden)".into()), + "/providers/switchyard/models" => value + .as_array() + .map(|models| { + models + .iter() + .map(|model| { + serde_json::from_value::(model.clone()) + .and_then(serde_json::to_value) + .unwrap_or_else(|_| { + json!("(unsupported model metadata; hidden)") + }) + }) + .collect::>() + }) + .map(|models| Value::Array(models).to_string()) + .unwrap_or_default(), + "/env/ANTHROPIC_BASE_URL" + | "/model_providers/sy/base_url" + | "/providers/switchyard/baseUrl" => value + .as_str() + .and_then(|v| url::Url::parse(v).ok()) + .map(|mut url| { + let _ = url.set_username(""); + let _ = url.set_password(None); + url.set_query(None); + url.set_fragment(None); + url.to_string() + }) + .unwrap_or_else(|| "(configured endpoint)".into()), + _ => match value { + Value::String(value) => value.clone(), + Value::Bool(value) => value.to_string(), + _ => "(set; hidden)".into(), + }, + } + }; + changes.push(json!({"file":file.display().to_string(),"key":key.trim_start_matches('/').replace('/',"."),"before":display(left),"after":display(right)})); + } + } + Ok(changes) +} + +// Both preview and installation use these transformations so the proposed settings match the saved settings. +fn configured( + tool: Harness, + originals: &[Option], + root: &str, + model: &str, + login: bool, +) -> Result, String> { + let mut incoming = Vec::new(); + match tool { + Harness::CodexCli | Harness::CodexApp => incoming.push(codex_settings( + originals[0].as_deref().unwrap_or_default(), + root, + model, + login, + )?), + Harness::Claude => incoming.push(claude_settings( + originals[0].as_deref().unwrap_or_default(), + root, + model, + login, + )?), + Harness::Pi => { + let mut doc = object(originals[0].as_deref().unwrap_or_default())?; + let providers = map(&mut doc, "providers")?; + providers["switchyard"] = json!({"baseUrl":format!("{root}/v1"), "api":"openai-completions", "apiKey":"switchyard-local", "headers":{"x-switchyard-origin":"pi"}, "models":[{"id":model,"name":model,"reasoning":true,"input":["text"],"contextWindow":128000,"maxTokens":16384,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0}}]}); + incoming.push(serde_json::to_string_pretty(&doc).map_err(|e| e.to_string())? + "\n"); + let mut settings = object(originals[1].as_deref().unwrap_or_default())?; + settings["defaultProvider"] = json!("switchyard"); + settings["defaultModel"] = json!(model); + incoming + .push(serde_json::to_string_pretty(&settings).map_err(|e| e.to_string())? + "\n"); + } + } + Ok(incoming) +} + +pub fn install( + tool: Harness, + files: &[PathBuf], + root: &str, + model: &str, + login: bool, +) -> Result { + install_reviewed(tool, files, root, model, login, None) +} + +pub fn install_reviewed( + tool: Harness, + files: &[PathBuf], + root: &str, + model: &str, + login: bool, + expected: Option<&[ReviewedFile]>, +) -> Result { + let root = validate(tool, root, model, login)?; + for path in files { + if read_file(&restore_pending_path(path))?.is_some() { + return Err("A previous restore did not finish retiring its backups. Retry Restore before installing another route.".into()); + } + } + let originals = files + .iter() + .map(|p| read_file(p)) + .collect::, _>>()?; + check_review(expected)?; + let incoming = configured(tool, &originals, &root, model, login)?; + let incoming: Vec<_> = incoming.into_iter().map(Some).collect(); + for (path, original) in files.iter().zip(&originals) { + if let Some(expected) = expected + .and_then(|files| files.iter().find(|(file, _)| file == path)) + .map(|(_, bytes)| bytes) + && original.as_ref().map(|text| text.as_bytes()) != expected.as_deref() + { + return Err("Tool settings changed after the preview. Review them again.".into()); + } + } + for (path, before) in files.iter().zip(&originals) { + let backup = backup_path(path); + let absent = absent_path(path); + if read_file(&backup)?.is_some() || read_file(&absent)?.is_some() { + continue; + } + let (destination, text) = match before { + Some(text) => (&backup, text.as_str()), + None => (&absent, ""), + }; + prepare(destination, text)? + .persist_noclobber(destination) + .map_err(|e| e.to_string())?; + } + replace(files, &originals, &incoming)?; + let next = match tool { + Harness::CodexCli => { + let profile = files[0] + .file_name() + .and_then(|name| name.to_str()) + .and_then(|name| name.strip_suffix(".config.toml")); + profile + .map(|name| format!(" Start Codex CLI with codex -p {name}.")) + .unwrap_or_else(|| " Restart Codex CLI.".into()) + } + Harness::Pi => " Reload Pi models with /model.".into(), + Harness::CodexApp => " Restart Codex app.".into(), + Harness::Claude => " Restart Claude Code.".into(), + }; + Ok(format!( + "Connected route {model}. Changed {}.{next}", + files + .iter() + .map(|path| path.display().to_string()) + .collect::>() + .join(", ") + )) +} + +fn backup_path(path: &Path) -> PathBuf { + path.with_file_name(format!( + "{}.switchyard-original", + path.file_name().unwrap_or_default().to_string_lossy() + )) +} + +// The marker distinguishes an originally missing file from an empty file. +// Restore removes installed settings only when the original file was missing. +fn absent_path(path: &Path) -> PathBuf { + backup_path(path).with_extension("switchyard-original-missing") +} + +fn restore_pending_path(path: &Path) -> PathBuf { + backup_path(path).with_extension("switchyard-restore-pending") +} + +pub fn review_files(files: &[PathBuf]) -> Vec { + files + .iter() + .flat_map(|path| { + [ + path.clone(), + backup_path(path), + absent_path(path), + restore_pending_path(path), + ] + }) + .collect() +} + +pub fn preview_restore(tool: Harness, files: &[PathBuf]) -> Result { + let resuming = files.iter().any(|path| restore_pending_path(path).exists()); + let mut plan = Vec::new(); + let mut before = Vec::new(); + let mut after = Vec::new(); + for path in files { + let current = read_file(path)?; + let pending = read_file(&restore_pending_path(path))?; + let original = match pending { + Some(text) => { + serde_json::from_str::>(&text).map_err(|error| error.to_string())? + } + None => match read_file(&backup_path(path))? { + Some(text) => Some(text), + None if read_file(&absent_path(path))?.is_some() => None, + None if resuming => current.clone(), + None => return Err("No original backup is available for this destination.".into()), + }, + }; + let action = if current == original { + "unchanged" + } else { + match (¤t, &original) { + (_, None) => "remove", + (None, _) => "create", + _ => "replace", + } + }; + before.push(current.unwrap_or_default()); + after.push(original.unwrap_or_default()); + plan.push(json!({"file":path,"action":action,"backup":backup_path(path)})); + } + Ok( + json!({"files":plan,"changes":settings_changes(tool,files,&before,&after).ok(),"warning":"Restore replaces each listed file with its original version. Later edits are kept in a recovery copy before replacement."}), + ) +} + +// Restore receipts keep the original contents or absence available until backup cleanup finishes. +#[cfg(test)] +pub fn restore(files: &[PathBuf]) -> Result { + restore_reviewed(files, None) +} + +pub fn restore_reviewed( + files: &[PathBuf], + expected: Option<&[ReviewedFile]>, +) -> Result { + let pending = files + .iter() + .map(|path| read_file(&restore_pending_path(path))) + .collect::, _>>()?; + let resuming = pending.iter().any(Option::is_some); + let mut restored = Vec::new(); + for (path, pending) in files.iter().zip(pending) { + let original = read_file(&backup_path(path))?; + let absent = read_file(&absent_path(path))?.is_some(); + let original = match pending { + Some(text) => serde_json::from_str::>(&text) + .map_err(|e| format!("Read pending restore: {e}"))?, + None if original.is_none() && !absent => { + if !resuming { + return Err("No original backup is available for this installation.".into()); + } + // A cleared receipt means this file finished restoring, so a retry preserves later edits. + read_file(path)? + } + None => original, + }; + restored.push(original); + } + let current = files + .iter() + .map(|p| read_file(p)) + .collect::, _>>()?; + check_review(expected)?; + for (path, original) in files.iter().zip(¤t) { + if let Some(expected) = expected + .and_then(|files| files.iter().find(|(file, _)| file == path)) + .map(|(_, bytes)| bytes) + && original.as_ref().map(|text| text.as_bytes()) != expected.as_deref() + { + return Err( + "Tool settings changed after the restore preview. Review them again.".into(), + ); + } + } + let mut kept_paths = Vec::new(); + for (path, before) in files.iter().zip(¤t) { + let Some(before) = before else { + continue; + }; + let mut recovery = tempfile::Builder::new() + .prefix("switchyard-before-restore-") + .tempfile_in(path.parent().ok_or("Settings need a parent directory")?) + .map_err(|e| e.to_string())?; + recovery + .write_all(before.as_bytes()) + .map_err(|e| e.to_string())?; + let (_, kept) = recovery.keep().map_err(|e| e.to_string())?; + kept_paths.push(kept.display().to_string()); + } + // Each private receipt stores the original contents or absence and blocks installation during cleanup. + for (path, original) in files.iter().zip(&restored) { + let pending = restore_pending_path(path); + if read_file(&pending)?.is_none() { + let text = serde_json::to_string(original).map_err(|e| e.to_string())?; + prepare(&pending, &text)? + .persist_noclobber(&pending) + .map_err(|e| e.to_string())?; + } + } + let recovery = format!("Previous settings: {}", kept_paths.join(", ")); + replace(files, ¤t, &restored).map_err(|error| format!("{error}. {recovery}"))?; + for path in files { + for retired in [backup_path(path), absent_path(path)] { + match std::fs::remove_file(&retired) { + Ok(()) => {} + Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} + Err(error) => { + return Err(format!( + "Restored settings, but could not retire {}: {error}. Retry Restore before installing another route. {recovery}", + retired.display() + )); + } + } + } + } + for path in files { + let pending = restore_pending_path(path); + std::fs::remove_file(&pending).map_err(|error| { + format!( + "Restored settings, but could not clear {}: {error}. {recovery}", + pending.display() + ) + })?; + } + Ok(format!( + "Restored original settings. Restart the coding tool. {recovery}" + )) +} + +fn check_review(expected: Option<&[ReviewedFile]>) -> Result<(), String> { + if let Some(files) = expected { + for (path, expected) in files { + let current = match std::fs::read(path) { + Ok(bytes) => Some(std::sync::Arc::<[u8]>::from(bytes)), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => None, + Err(error) => return Err(error.to_string()), + }; + if ¤t != expected { + return Err(format!( + "{} changed after the preview. Review it again.", + path.display() + )); + } + } + } + Ok(()) +} + +fn prepare(path: &Path, text: &str) -> Result { + let parent = path.parent().ok_or("Settings need a parent directory")?; + std::fs::create_dir_all(parent).map_err(|e| e.to_string())?; + let mut temp = tempfile::NamedTempFile::new_in(parent).map_err(|e| e.to_string())?; + temp.write_all(text.as_bytes()) + .and_then(|_| temp.as_file().sync_all()) + .map_err(|e| e.to_string())?; + Ok(temp) +} + +// Each file replacement is atomic, but the set of replacements is not. +// A later failure triggers an attempt to restore files already replaced. +fn replace( + files: &[PathBuf], + before: &[Option], + after: &[Option], +) -> Result<(), String> { + let staged = files + .iter() + .zip(after) + .map(|(path, text)| text.as_deref().map(|text| prepare(path, text)).transpose()) + .collect::, _>>()?; + for (path, expected) in files.iter().zip(before) { + if read_file(path)? != *expected { + return Err(format!( + "{} changed while installing. Refresh and try again.", + path.display() + )); + } + } + for (index, (path, temp)) in files.iter().zip(staged).enumerate() { + let result = match temp { + Some(temp) => temp.persist(path).map(|_| ()).map_err(|e| e.to_string()), + None if before[index].is_some() => { + std::fs::remove_file(path).map_err(|e| e.to_string()) + } + None => Ok(()), + }; + if let Err(error) = result { + for (path, original) in files[..index].iter().zip(before) { + match original { + Some(text) => { + prepare(path, text)? + .persist(path) + .map_err(|e| e.to_string())?; + } + None => std::fs::remove_file(path).map_err(|e| e.to_string())?, + } + } + return Err(error); + } + } + Ok(()) +} + +pub fn binary(tool: Harness) -> Option { + if tool == Harness::CodexApp { + #[cfg(windows)] + return crate::windows::codex_app(); + #[cfg(not(windows))] + { + #[cfg(target_os = "macos")] + if let Some(path) = objc2_app_kit::NSWorkspace::sharedWorkspace() + .URLForApplicationWithBundleIdentifier(&objc2_foundation::NSString::from_str( + "com.openai.codex", + )) + .and_then(|url| url.path()) + .map(|path| PathBuf::from(path.to_string())) + .filter(|path| path.is_dir()) + { + return Some(path); + } + return [ + PathBuf::from("/Applications/Codex.app"), + crate::config::expand_home(Path::new("~/Applications/Codex.app")), + ] + .into_iter() + .find(|p| p.exists()); + } + } + let name = match tool { + Harness::CodexCli => "codex", + Harness::Claude => "claude", + Harness::Pi => "pi", + Harness::CodexApp => return None, + }; + let mut dirs: Vec<_> = std::env::var_os("PATH") + .map(|s| std::env::split_paths(&s).collect()) + .unwrap_or_default(); + dirs.extend([ + PathBuf::from("/opt/homebrew/bin"), + PathBuf::from("/usr/local/bin"), + crate::config::expand_home(Path::new("~/.local/bin")), + crate::config::expand_home(Path::new("~/.npm-global/bin")), + ]); + #[cfg(windows)] + { + dirs.extend([ + crate::config::expand_home(Path::new("~/AppData/Roaming/npm")), + crate::config::expand_home(Path::new("~/.cargo/bin")), + crate::config::expand_home(Path::new("~/.local/bin")), + ]); + dirs.into_iter() + .flat_map(|p| [p.join(format!("{name}.exe")), p.join(format!("{name}.cmd"))]) + .find(|p| p.is_file()) + } + #[cfg(not(windows))] + dirs.into_iter().map(|p| p.join(name)).find(|p| p.is_file()) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + // Nested unknown fields and malformed metadata must not expose credentials in the preview. + fn pi_preview_includes_model_limits_and_redacts_headers() { + let dir = tempfile::tempdir().expect("directory"); + let files = vec![ + dir.path().join("models.json"), + dir.path().join("settings.json"), + ]; + std::fs::write(&files[0], r#"{"providers":{"switchyard":{"baseUrl":"http://localhost:4123/v1","api":"openai-completions","apiKey":"SECRET","headers":{"Authorization":"SECRET"},"models":[{"id":"route","name":"Route","reasoning":true,"input":["text","image"],"contextWindow":1000,"maxTokens":20,"cost":{"input":12,"output":3,"cacheRead":1,"cacheWrite":2,"unknown":{"key":"SECRET"}}},{"id":"bad-name","name":{"key":"SECRET"}},{"id":"bad-cost","cost":{"input":{"key":"SECRET"}}},{"id":"bad-input","input":["text",{"key":"SECRET"}]}]}}}"#).expect("models"); + let result = + preview(Harness::Pi, &files, "http://localhost:4123", "route", false).expect("preview"); + assert!(!result.to_string().contains("SECRET")); + let changes = result["changes"].as_array().expect("changes"); + let models = changes + .iter() + .find(|change| change["key"] == "providers.switchyard.models") + .expect("models diff"); + assert!(models["before"].as_str().expect("before").contains("1000")); + assert!(models["after"].as_str().expect("after").contains("128000")); + let projected: Value = + serde_json::from_str(models["before"].as_str().expect("before")).expect("models"); + assert_eq!( + projected[0]["cost"], + json!({"input":12.0,"output":3.0,"cacheRead":1.0,"cacheWrite":2.0}) + ); + assert_eq!(projected[0]["input"], json!(["text", "image"])); + assert_eq!(projected[0]["name"], "Route"); + for model in projected.as_array().expect("models").iter().skip(1) { + assert_eq!(model, "(unsupported model metadata; hidden)"); + } + + assert!( + changes + .iter() + .any(|change| change["key"] == "providers.switchyard.headers") + ); + } + + #[test] + // Preview must match the installed settings without creating files or backups. + fn preview_matches_installation_without_writes() { + for (tool, _) in HARNESSES { + let dir = tempfile::tempdir().expect("directory"); + let files = if *tool == Harness::Pi { + vec![ + dir.path().join("models.json"), + dir.path().join("settings.json"), + ] + } else { + vec![dir.path().join("config")] + }; + let original = if matches!(tool, Harness::CodexCli | Harness::CodexApp) { + "# user comment\nmodel='before'\nsandbox_mode='workspace-write'\n" + } else { + "{\"custom\":true}" + }; + std::fs::write(&files[0], original).expect("settings"); + let result = preview(*tool, &files, "http://localhost:4123", "new-route", false) + .expect("preview"); + assert!( + result["changes"] + .as_array() + .expect("diff") + .iter() + .any(|change| change["after"] + .as_str() + .is_some_and(|value| value.contains("new-route"))) + ); + assert_eq!(read(&files[0]).expect("unchanged"), original); + assert_eq!(std::fs::read_dir(dir.path()).expect("files").count(), 1); + install(*tool, &files, "http://localhost:4123", "new-route", false).expect("install"); + assert_eq!( + result["proposed"], + inspect(*tool, &files).expect("installed") + ); + install( + *tool, + &files, + "http://localhost:4123", + "refreshed-route", + false, + ) + .expect("refresh"); + let installed = read(&files[0]).expect("settings"); + assert!(installed.contains("refreshed-route")); + assert!( + installed.contains(if matches!(tool, Harness::CodexCli | Harness::CodexApp) { + "sandbox_mode" + } else { + "custom" + }) + ); + assert_eq!(read(&backup_path(&files[0])).expect("backup"), original); + restore(&files).expect("restore"); + assert_eq!(read(&files[0]).expect("original"), original); + if *tool == Harness::Pi { + assert!(!files[1].exists()); + } + std::fs::write(&files[0], original.replace("before", "new-user-model")) + .expect("user edit"); + install(*tool, &files, "http://localhost:4123", "again", false).expect("reinstall"); + restore(&files).expect("restore again"); + assert_eq!( + read(&files[0]).expect("new original"), + original.replace("before", "new-user-model") + ); + assert!( + result["authentication"] + .as_str() + .expect("authentication") + .contains("API credentials") + ); + } + let dir = tempfile::tempdir().expect("directory"); + let files = vec![dir.path().join("settings.json")]; + let original = + r#"{"env":{"ANTHROPIC_API_KEY":"SECRET","ANTHROPIC_MODEL":{"key":"SECRET"}}}"#; + std::fs::write(&files[0], original).expect("settings"); + assert!( + preview( + Harness::Claude, + &files, + "http://localhost:4123", + "route", + true + ) + .is_err() + ); + assert_eq!(read(&files[0]).expect("unchanged"), original); + assert_eq!(std::fs::read_dir(dir.path()).expect("files").count(), 1); + let safe = preview( + Harness::Claude, + &files, + "http://localhost:4123", + "route", + false, + ) + .expect("API preview"); + assert!(!safe.to_string().contains("SECRET")); + assert!( + safe["changes"] + .as_array() + .expect("diff") + .iter() + .any(|change| change["key"] == "env.ANTHROPIC_API_KEY" + && change["before"] == "(set; hidden)") + ); + std::fs::write(&files[0], "{}").expect("settings"); + let result = preview( + Harness::Claude, + &files, + "http://localhost:4123", + "route", + true, + ) + .expect("login preview"); + assert!( + result["authentication"] + .as_str() + .expect("authentication") + .contains("subscription login") + ); + } + + #[test] + fn a_pending_restore_blocks_install_and_retires_stale_backups_on_retry() { + let dir = tempfile::tempdir().expect("directory"); + let path = dir.path().join("settings.json"); + let files = std::slice::from_ref(&path); + std::fs::write(&path, "{}").expect("settings"); + install( + Harness::Claude, + files, + "http://localhost:4123", + "api", + false, + ) + .expect("install"); + let pending = restore_pending_path(&path); + std::fs::write( + &pending, + serde_json::to_string(&Some("{}".to_string())).expect("receipt"), + ) + .expect("pending retirement"); + let installed = read(&path).expect("installed"); + assert!( + install( + Harness::Claude, + files, + "http://localhost:4123", + "another", + false + ) + .is_err() + ); + assert_eq!(read(&path).expect("unchanged"), installed); + // The backup was already retired, so retry uses the saved restoration receipt. + std::fs::remove_file(backup_path(&path)).expect("retired backup"); + restore(files).expect("retry restore"); + assert_eq!(read(&path).expect("restored"), "{}"); + assert!(!pending.exists()); + } + + // Pi cleanup can stop between files; retry must keep edits to a file that already finished. + #[test] + fn pi_restore_retry_preserves_a_file_whose_receipt_was_already_cleared() { + let dir = tempfile::tempdir().expect("directory"); + let files = vec![ + dir.path().join("models.json"), + dir.path().join("settings.json"), + ]; + for path in &files { + std::fs::write(path, "{}").expect("original"); + } + install(Harness::Pi, &files, "http://localhost:4123", "route", false).expect("install"); + for path in &files { + std::fs::remove_file(backup_path(path)).expect("retired backup"); + std::fs::write(path, "{}").expect("restored settings"); + } + std::fs::write(&files[0], "{\"user_edit\":true}").expect("edit after restore"); + std::fs::write(restore_pending_path(&files[1]), r#""{}""#).expect("remaining receipt"); + restore(&files).expect("retry cleanup"); + assert_eq!(read(&files[0]).expect("first file"), "{\"user_edit\":true}"); + assert_eq!(read(&files[1]).expect("second file"), "{}"); + assert!( + files + .iter() + .all(|path| !restore_pending_path(path).exists()) + ); + } + + #[test] + fn subscription_install_removes_only_switchyard_credentials() { + let dir = tempfile::tempdir().expect("directory"); + let path = dir.path().join("settings.json"); + let files = std::slice::from_ref(&path); + std::fs::write(&path, "{}").expect("settings"); + install( + Harness::Claude, + files, + "http://localhost:4123", + "api-route", + false, + ) + .expect("API install"); + install( + Harness::Claude, + files, + "http://localhost:4123", + "subscription", + true, + ) + .expect("subscription install"); + let doc = object(&read(&path).expect("settings")).expect("JSON"); + assert!(doc["env"].get("ANTHROPIC_AUTH_TOKEN").is_none()); + assert!(doc["env"].get("ANTHROPIC_API_KEY").is_none()); + assert_eq!(doc["env"]["ANTHROPIC_MODEL"], "subscription"); + for key in ["ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_API_KEY"] { + for value in [ + json!("user-secret"), + json!("switchyard-local"), + json!(null), + json!(false), + json!(123), + json!([]), + json!({}), + ] { + let original = json!({"env":{key:value}}).to_string(); + std::fs::write(&path, &original).expect("settings"); + let before = std::fs::read_dir(dir.path()).expect("files").count(); + assert!( + install( + Harness::Claude, + files, + "http://localhost:4123", + "subscription", + true + ) + .is_err() + ); + assert_eq!(read(&path).expect("unchanged"), original); + assert_eq!( + std::fs::read_dir(dir.path()).expect("files").count(), + before + ); + } + } + } + + #[test] + fn invalid_inputs_change_no_files() { + let dir = tempfile::tempdir().expect("directory"); + let path = dir.path().join("settings.json"); + for invalid in ["[]", "null", "{", "{\"env\":false}"] { + std::fs::write(&path, invalid).expect("settings"); + assert!( + install( + Harness::Claude, + std::slice::from_ref(&path), + "http://localhost:4123", + "route", + false + ) + .is_err() + ); + assert_eq!(read(&path).expect("read"), invalid); + assert!(!backup_path(&path).exists()); + } + std::fs::write(&path, "{}").expect("settings"); + for root in [ + "https://example.com", + "http://localhost.evil", + "http://user:secret@localhost", + "http://127.0.0.1/path", + "http://localhost?q=a", + ] { + assert!( + install( + Harness::Claude, + std::slice::from_ref(&path), + root, + "route", + false + ) + .is_err() + ); + assert_eq!(read(&path).expect("read"), "{}"); + } + assert!(!backup_path(&path).exists()); + } + #[test] + fn pi_preflights_both_files() { + let dir = tempfile::tempdir().expect("directory"); + let files = vec![ + dir.path().join("models.json"), + dir.path().join("settings.json"), + ]; + std::fs::write(&files[0], "{}").expect("models"); + std::fs::write(&files[1], "[]").expect("settings"); + assert!(install(Harness::Pi, &files, "http://localhost:4123", "route", false).is_err()); + assert_eq!(read(&files[0]).expect("read"), "{}"); + assert!(!backup_path(&files[0]).exists()); + } + #[test] + fn malformed_codex_provider_tables_do_not_panic_or_write() { + let dir = tempfile::tempdir().expect("directory"); + let path = dir.path().join("config.toml"); + for text in [ + "model_providers = 1", + "model_providers = []", + "[model_providers]\nsy = false", + "[[model_providers.sy]]\nname = 'old'", + ] { + std::fs::write(&path, text).expect("settings"); + assert!( + install( + Harness::CodexCli, + std::slice::from_ref(&path), + "http://localhost:4123", + "route", + false + ) + .is_err() + ); + assert_eq!(read(&path).expect("read"), text); + assert!(!backup_path(&path).exists()); + } + std::fs::write( + &path, + "model_providers = { sy = { name = 'old', env_key = 'OLD_KEY' } }\n", + ) + .expect("settings"); + install( + Harness::CodexCli, + std::slice::from_ref(&path), + "http://localhost:4123", + "route", + false, + ) + .expect("inline table"); + let doc = read(&path) + .expect("read") + .parse::() + .expect("parse"); + assert_eq!( + doc["model_providers"]["sy"]["base_url"].as_str(), + Some("http://localhost:4123/v1") + ); + assert!(doc["model_providers"]["sy"].get("env_key").is_none()); + } + + #[test] + fn inspection_omits_url_secrets_and_invalid_toml_source() { + let dir = tempfile::tempdir().expect("directory"); + let path = dir.path().join("config.toml"); + std::fs::write(&path, "model_provider = 'p'\n[model_providers.p]\nbase_url = 'https://user:SECRET@example.com:8443/v1?key=SECRET'\n").expect("settings"); + let info = inspect(Harness::CodexCli, std::slice::from_ref(&path)).expect("inspect"); + assert!(info.contains("https://example.com:8443")); + assert!(!info.contains("SECRET")); + std::fs::write(&path, "api_key = \"SECRET\n").expect("invalid"); + let error = inspect(Harness::CodexCli, std::slice::from_ref(&path)).expect_err("invalid"); + assert!(!error.contains("SECRET")); + } + + #[test] + fn replacement_prepares_every_file_and_rejects_changed_inputs() { + let dir = tempfile::tempdir().expect("directory"); + let first = dir.path().join("first"); + let blocked = dir.path().join("blocked"); + std::fs::write(&first, "original").expect("first"); + std::fs::write(&blocked, "not a directory").expect("blocked"); + let files = vec![first.clone(), blocked.join("second")]; + assert!( + replace( + &files, + &[Some("original".into()), None], + &[Some("new".into()), Some("new".into())] + ) + .is_err() + ); + assert_eq!(read(&first).expect("read"), "original"); + assert!( + replace( + std::slice::from_ref(&first), + &[Some("stale".into())], + &[Some("new".into())] + ) + .is_err() + ); + assert_eq!(read(&first).expect("read"), "original"); + } + + #[cfg(unix)] + #[test] + fn restore_preflights_symlinks_before_changing_any_settings() { + let dir = tempfile::tempdir().expect("directory"); + let files = vec![ + dir.path().join("models.json"), + dir.path().join("settings.json"), + ]; + for path in &files { + std::fs::write(path, "{}").expect("original"); + } + install(Harness::Pi, &files, "http://localhost:4123", "route", false).expect("install"); + let before = read(&files[0]).expect("read"); + std::fs::remove_file(&files[1]).expect("remove"); + let target = dir.path().join("target"); + std::fs::write(&target, "private").expect("target"); + std::os::unix::fs::symlink(&target, &files[1]).expect("symlink"); + assert!(restore(&files).is_err()); + assert!(files.iter().all(|path| backup_path(path).is_file())); + assert_eq!(read(&files[0]).expect("read"), before); + assert_eq!(std::fs::read_to_string(target).expect("target"), "private"); + } +} diff --git a/crates/switchyard-desktop/src/health.rs b/crates/switchyard-desktop/src/health.rs new file mode 100644 index 0000000000..5b5c32a58a --- /dev/null +++ b/crates/switchyard-desktop/src/health.rs @@ -0,0 +1,116 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! This module checks the local server's `/health` endpoint. +//! +//! The request is written by hand over TCP. The server is always on loopback +//! over plain HTTP, so a full HTTP client would buy nothing here. + +use std::io::{BufRead, BufReader, Write}; +use std::net::{TcpStream, ToSocketAddrs}; +use std::time::Duration; + +/// TIMEOUT limits how long a health check can delay a usage refresh. +const TIMEOUT: Duration = Duration::from_millis(500); + +/// ServerStatus records whether the server answered its health check. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ServerStatus { + Running, + Stopped, +} + +/// This function probes `GET /health`, treating any failure as a stopped server. +pub fn probe(server_url: &str) -> ServerStatus { + match request(server_url) { + Ok(true) => ServerStatus::Running, + _ => ServerStatus::Stopped, + } +} + +fn request(server_url: &str) -> std::io::Result { + let url = validate_url(server_url).map_err(std::io::Error::other)?; + let host = url + .host_str() + .ok_or_else(|| std::io::Error::other("Missing server host"))?; + let authority = match url.host() { + Some(url::Host::Ipv6(address)) => format!("[{address}]:{}", url.port().unwrap_or(4123)), + _ => format!("{host}:{}", url.port().unwrap_or(4123)), + }; + let address = authority + .to_socket_addrs()? + .next() + .ok_or_else(|| std::io::Error::other(format!("no address for {authority}")))?; + let mut stream = TcpStream::connect_timeout(&address, TIMEOUT)?; + stream.set_read_timeout(Some(TIMEOUT))?; + stream.set_write_timeout(Some(TIMEOUT))?; + stream.write_all(format!("GET /health HTTP/1.0\r\nHost: {authority}\r\n\r\n").as_bytes())?; + let mut status_line = String::new(); + BufReader::new(stream).read_line(&mut status_line)?; + Ok(status_line.split_whitespace().nth(1) == Some("200")) +} + +pub fn validate_url(server_url: &str) -> Result { + if server_url.trim() != server_url + || server_url.chars().any(char::is_control) + || !server_url + .split_once("://") + .is_some_and(|(scheme, _)| scheme.eq_ignore_ascii_case("http")) + { + return Err( + "Server URL must start with http:// and contain no whitespace or control characters." + .into(), + ); + } + let url = + url::Url::parse(server_url).map_err(|error| format!("Invalid server URL: {error}"))?; + if url.scheme() != "http" + || url.host().is_none() + || !url.username().is_empty() + || url.password().is_some() + || url.query().is_some() + || url.fragment().is_some() + || !matches!(url.path(), "" | "/") + { + return Err("Server URL must be an HTTP base URL with a host and optional port, without credentials, a path, query, or fragment.".into()); + } + Ok(url) +} + +pub fn details(server_url: &str) -> serde_json::Value { + match request(server_url) { + Ok(true) => serde_json::json!({"state":"running","reason":null}), + Ok(false) => { + serde_json::json!({"state":"unreachable","reason":"The health endpoint did not return HTTP 200."}) + } + Err(error) => serde_json::json!({"state":"unreachable","reason":error.to_string()}), + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn health_urls_match_the_supported_probe_forms() { + for value in [ + "http://127.0.0.1:4123", + "http://localhost", + "http://[::1]:4123/", + ] { + assert!(validate_url(value).is_ok(), "{value}"); + } + for value in [ + "https://localhost", + "localhost:4123", + "http://user@localhost", + "http://localhost/v1", + "http://localhost?x=1", + "http://localhost#x", + " http://localhost", + "http://local\nhost", + "http:/localhost", + ] { + assert!(validate_url(value).is_err(), "{value}"); + } + } +} diff --git a/crates/switchyard-desktop/src/history.rs b/crates/switchyard-desktop/src/history.rs new file mode 100644 index 0000000000..9786fbcc73 --- /dev/null +++ b/crates/switchyard-desktop/src/history.rs @@ -0,0 +1,179 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! This module reads recent completed model calls and filters them by recorded IDs. + +use serde::Deserialize; +use std::collections::VecDeque; +use std::io::{Read, Seek, SeekFrom}; +use std::path::Path; + +const MAX_BYTES: u64 = 8 * 1024 * 1024; +const MAX_RECORDS: usize = 5000; + +#[derive(Debug, Deserialize, serde::Serialize)] +pub struct Entry { + pub ts: String, + pub model: String, + #[serde(default)] + pub session_id: Option, + #[serde(default)] + pub turn_id: Option, + #[serde(default)] + pub route_id: String, + #[serde(default)] + pub tier: String, + #[serde(default)] + pub prompt_tokens: u64, + #[serde(default)] + pub cached_tokens: u64, + #[serde(default)] + pub completion_tokens: u64, +} + +#[derive(Default)] +pub struct History { + pub entries: Vec, + pub limited: bool, + pub skipped: usize, +} + +// hourly_calls uses bounded recent history, so its buckets may omit calls from a busy day. +pub fn hourly_calls(entries: &[Entry], now: chrono::DateTime) -> [u64; 24] { + let mut buckets = [0; 24]; + for entry in entries { + if let Ok(time) = chrono::DateTime::parse_from_rfc3339(&entry.ts) { + let age = now.signed_duration_since(time); + if (chrono::Duration::zero()..chrono::Duration::hours(24)).contains(&age) { + buckets[23 - age.num_seconds() as usize / 3600] += 1; + } + } + } + buckets +} + +pub fn load(path: &Path) -> Result { + let mut file = match std::fs::File::open(path) { + Ok(f) => f, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(History::default()), + Err(e) => return Err(e.to_string()), + }; + let size = file.metadata().map_err(|e| e.to_string())?.len(); + let offset = size.saturating_sub(MAX_BYTES); + // The preceding byte distinguishes a partial record from a complete first + // record when the byte limit starts immediately after a newline. + let mut previous = *b"\n"; + if offset > 0 { + file.seek(SeekFrom::Start(offset - 1)) + .map_err(|e| e.to_string())?; + file.read_exact(&mut previous).map_err(|e| e.to_string())?; + } + file.seek(SeekFrom::Start(offset)) + .map_err(|e| e.to_string())?; + let mut bytes = Vec::new(); + file.take(MAX_BYTES) + .read_to_end(&mut bytes) + .map_err(|e| e.to_string())?; + let mut history = parse(&bytes, offset > 0 && previous[0] != b'\n'); + history.limited |= offset > 0; + Ok(history) +} + +fn parse(bytes: &[u8], truncated: bool) -> History { + let mut result = History { + limited: truncated, + ..History::default() + }; + let mut lines = bytes.split_inclusive(|b| *b == b'\n'); + if truncated { + lines.next(); + } + let mut entries = VecDeque::new(); + for line in lines { + if !line.ends_with(b"\n") { + break; + } + match serde_json::from_slice::(line) { + Ok(entry) + if !entry.model.trim().is_empty() + && chrono::DateTime::parse_from_rfc3339(&entry.ts).is_ok() => + { + if entries.len() == MAX_RECORDS { + entries.pop_front(); + result.limited = true; + } + entries.push_back(entry); + } + _ => result.skipped += 1, + } + } + result.entries = entries.into(); + result +} + +impl History { + pub fn sessions(&self) -> Vec { + self.entries + .iter() + .filter_map(|e| e.session_id.as_deref()) + .filter(|s| !s.trim().is_empty()) + .map(str::to_string) + .collect::>() + .into_iter() + .collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn groups_recorded_ids_and_never_displays_content() { + let bytes = b"{\"ts\":\"2026-10-08T12:00:00Z\",\"model\":\"actual\",\"session_id\":\"s\",\"turn_id\":\"t\",\"prompt_tokens\":12,\"completion_tokens\":3,\"messages\":\"SECRET\"}\n{\"ts\":\"2026-10-08T12:00:01Z\",\"model\":\"judge\",\"tier\":\"classifier\"}\ninvalid\n{\"model\":\"unfinished\"}"; + let history = parse(bytes, false); + assert_eq!(history.sessions(), vec!["s"]); + assert_eq!(history.entries.len(), 2); + assert_eq!(history.skipped, 1); + assert_eq!(history.entries[0].turn_id.as_deref(), Some("t")); + assert_eq!(history.entries[0].prompt_tokens, 12); + assert!( + !serde_json::to_string(&history.entries) + .expect("snapshot") + .contains("SECRET") + ); + assert!(history.entries[1].session_id.is_none()); + } + + #[test] + fn retains_recent_records_and_complete_byte_limited_records() { + let directory = tempfile::tempdir().expect("directory"); + let path = directory.path().join("log"); + let line = b"{\"ts\":\"2026-10-08T12:00:00Z\",\"model\":\"recent\"}\n"; + let mut contents = b"{\"ts\":\"2026-10-08T11:00:00Z\",\"model\":\"old\"}\n".to_vec(); + contents.extend(line.repeat(MAX_RECORDS)); + std::fs::write(&path, contents).expect("record-limited log"); + let history = load(&path).expect("record-limited history"); + assert_eq!(history.entries.len(), MAX_RECORDS); + assert!(history.entries.iter().all(|entry| entry.model == "recent")); + assert!(history.limited); + + for prefix in [ + b"discarded prefix\n".as_slice(), + b"partial prefix".as_slice(), + ] { + let mut contents = prefix.to_vec(); + contents.extend_from_slice(line); + contents.resize(prefix.len() + MAX_BYTES as usize, b' '); + std::fs::write(&path, contents).expect("byte-limited log"); + let history = load(&path).expect("byte-limited history"); + assert!(history.limited); + if prefix.ends_with(b"\n") { + assert_eq!(history.entries.len(), 1); + assert_eq!(history.entries[0].model, "recent"); + } else { + assert!(history.entries.is_empty()); + } + } + } +} diff --git a/crates/switchyard-desktop/src/main.rs b/crates/switchyard-desktop/src/main.rs new file mode 100644 index 0000000000..a49794ede9 --- /dev/null +++ b/crates/switchyard-desktop/src/main.rs @@ -0,0 +1,158 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! The desktop and terminal apps manage a local Switchyard server. +//! +//! Both apps show recorded traffic and compare list-price estimates with the +//! configured baseline model. Their Routes views edit the server config and +//! restart the server. + +#![cfg_attr(windows, windows_subsystem = "windows")] + +mod accounts; +mod config; +mod controller; +#[cfg(any(target_os = "macos", windows))] +mod gui; +mod harness; +mod health; +mod history; +mod models; +mod pricing; +mod rollup; +mod server; +mod server_config; +mod sessions; +mod summary; +mod tui; +#[cfg(windows)] +mod windows; + +use std::path::PathBuf; +use std::process::ExitCode; + +use config::Config; + +const USAGE: &str = "\ +Usage: switchyard-desktop [--tui | --print | --validate-toml] [FILE] + +This app manages routes, coding-tool settings, and usage. + + FILE The app reads settings from FILE [default: ~/.switchyard/desktop.toml]. + --tui Run the terminal app. + --print Print the current summary and exit. + --validate-toml Parse FILE as TOML and exit."; + +fn main() -> ExitCode { + #[cfg(windows)] + if let Some(result) = windows::terminal_job() { + return match result { + Ok(()) => ExitCode::SUCCESS, + Err(error) => { + eprintln!("switchyard-desktop: {error}"); + ExitCode::FAILURE + } + }; + } + + #[cfg(windows)] + windows::attach_console(); + let mut settings = None; + let mut print_only = false; + let mut validate_toml = false; + let mut terminal = false; + for arg in std::env::args().skip(1) { + match arg.as_str() { + "-h" | "--help" => { + println!("{USAGE}"); + return ExitCode::SUCCESS; + } + "--print" => print_only = true, + "--tui" => terminal = true, + "--validate-toml" => validate_toml = true, + _ if arg.starts_with('-') => { + eprintln!("switchyard-desktop: unknown option {arg}\n\n{USAGE}"); + return ExitCode::FAILURE; + } + _ => settings = Some(PathBuf::from(arg)), + } + } + + if terminal && (print_only || validate_toml) { + eprintln!("{USAGE}"); + return ExitCode::FAILURE; + } + + if validate_toml { + let Some(path) = settings else { + eprintln!("switchyard-desktop: --validate-toml requires a file path"); + return ExitCode::FAILURE; + }; + if let Err(error) = config::validate_toml_file(&path) { + eprintln!("switchyard-desktop: {error}"); + return ExitCode::FAILURE; + } + return ExitCode::SUCCESS; + } + + let settings = match settings.map(Ok).unwrap_or_else(Config::default_path) { + Ok(path) => path, + Err(error) => { + eprintln!("switchyard-desktop: {error}"); + return ExitCode::FAILURE; + } + }; + let config = match Config::load(&settings) { + Ok(config) => config, + Err(error) => { + eprintln!("switchyard-desktop: {error}"); + if print_only || (!terminal && !cfg!(any(target_os = "macos", windows))) { + return ExitCode::FAILURE; + } + Config::default() + } + }; + // The default mode prints a summary on platforms without a desktop app. + // The --tui option starts the terminal app on those platforms. + if print_only || (!terminal && !cfg!(any(target_os = "macos", windows))) { + if !print_only { + eprintln!( + "Run switchyard-desktop --tui {} to open the terminal app.", + sessions::quote(&settings.display().to_string()) + ); + } + let usage = match rollup::Reader::default() + .read(&config.routing_log, chrono::Local::now().date_naive()) + { + Ok(usage) => usage, + Err(error) => { + eprintln!( + "Usage unavailable. Could not read {}: {error}", + config.routing_log.display() + ); + return ExitCode::FAILURE; + } + }; + for row in summary::build(health::probe(&config.server_url), &usage, &config) { + println!("{row}"); + } + return ExitCode::SUCCESS; + } + + let controller = controller::Controller::new(config, settings); + if terminal { + #[cfg(windows)] + windows::allocate_console(); + if let Err(error) = tui::run(controller) { + eprintln!("switchyard-desktop: {error}"); + return ExitCode::FAILURE; + } + } else { + #[cfg(any(target_os = "macos", windows))] + if let Err(error) = gui::run(controller) { + eprintln!("switchyard-desktop: {error}"); + return ExitCode::FAILURE; + } + } + ExitCode::SUCCESS +} diff --git a/crates/switchyard-desktop/src/models.rs b/crates/switchyard-desktop/src/models.rs new file mode 100644 index 0000000000..6bc18f84fa --- /dev/null +++ b/crates/switchyard-desktop/src/models.rs @@ -0,0 +1,735 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! This module lists the models an LLM client offers from its `GET /models` endpoint, +//! and keeps each fetched list in a cache file. +//! +//! The request goes through the system `curl`, the same way the app runs +//! `launchctl`, so the app needs no HTTP or TLS stack of its own. The key is +//! written to curl's stdin, never to its command line, where other processes +//! could read it. +//! +//! The cache file holds model IDs and fetch times, never a key. The app uses +//! a list from the cache file until the user refreshes it, so it fetches a +//! list on its own only when the file does not have it. + +use std::collections::BTreeMap; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; +use std::sync::{Mutex, PoisonError}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde::{Deserialize, Serialize}; + +use crate::config::expand_home; +use crate::server_config::Client; + +/// CACHE_FILE names the cache file. It sits next to the settings file that the +/// app was started with. +pub const CACHE_FILE: &str = "model-lists.json"; + +/// KEYCHAIN_SERVICE identifies keys saved from Routes. Each +/// item's account is the client's `base_url`, so clients that share a +/// gateway share its key. +#[cfg(target_os = "macos")] +const KEYCHAIN_SERVICE: &str = "Switchyard model list"; + +/// TIMEOUT_SECONDS limits how long curl may spend listing models. +const TIMEOUT_SECONDS: &str = "20"; + +/// CACHE_WRITE stays locked while a thread rewrites the cache file, so threads that +/// finish at the same time do not drop each other's lists. +static CACHE_WRITE: Mutex<()> = Mutex::new(()); + +/// ModelList stores one fetched model list in the cache file. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct ModelList { + /// This field stores sorted model IDs. + pub models: Vec, + /// This field records the fetch time in seconds since 1970-01-01 UTC. + pub fetched_at: u64, +} + +/// Loaded stores the model list and fetch result for one URL. +#[derive(Clone, Debug, PartialEq)] +pub struct Loaded { + /// This field identifies an endpoint list as `endpoint` or a Switchyard cache list as `cache`. + /// A Codex cache list has the source `local`; `error` separately reports fetch or cache-write failures. + pub source: &'static str, + /// This field names the URL that serves the list. + pub url: String, + /// This field stores the newest list fetched or read from the cache file. + pub list: Option, + /// This field records a fetch or cache-write error, if either occurred. + pub error: Option, +} + +/// ListError describes why a client's models could not be listed. +#[derive(Clone, Debug, PartialEq)] +pub enum ListError { + /// The client needs a key, and none is set or saved. + NoKey, + /// The client reads its key from this `api_key_env` variable, which this + /// process does not have, and no key is saved. + NoEnv(String), + /// The platform's credential store did not return the saved key. + Keychain(String), + /// The models endpoint answered 401 or 403. + Rejected(u16), + /// The request failed, or the answer was not a model list. + Failed(String), + /// The list was fetched, but the cache file could not be written. + NotCached(String), +} + +impl ListError { + /// This function says what went wrong, in one sentence. + pub fn reason(&self) -> String { + match self { + Self::NoKey => { + "This endpoint needs an API key to list models, and the app does not have one." + .to_string() + } + Self::NoEnv(variable) => missing_env_note(variable), + Self::Keychain(error) => { + format!("Could not read the saved key from secure credential storage: {error}.") + } + Self::Rejected(status) => { + format!("The models endpoint rejected the key (HTTP {status}).") + } + Self::Failed(error) => format!("Could not list models: {error}."), + Self::NotCached(error) => format!("Could not save the list: {error}."), + } + } + + /// This function says what the user can do while the window has no list to show. The + /// window accepts a typed model ID, so most answers offer that. A missing + /// variable also stops Apply, so that answer offers only the key field. + pub fn advice(&self) -> &'static str { + match self { + Self::NoEnv(_) => "Or paste the key below to list models.", + _ if self.needs_key() => { + "Enter a valid API key and choose Save key and load models, or type a model ID." + } + _ => "Click Refresh models to try again, or type a model ID.", + } + } + + /// This function returns whether saving a key could fix this error. + pub fn needs_key(&self) -> bool { + matches!( + self, + Self::NoKey | Self::NoEnv(_) | Self::Keychain(_) | Self::Rejected(_) + ) + } +} + +/// Apply can use saved server credentials when variables are absent; model discovery uses separate keys. +pub fn missing_env_note(variable: &str) -> String { + format!( + "The app's environment has no {variable}. If server validation needs this credential, run cargo desktop install from a terminal where the variable is set to save it on macOS or Windows. On Linux, provide it to the app and server service." + ) +} + +/// This function returns the client's `api_key_env` variable when this process does not +/// have it set. +pub fn missing_env(client: &Client) -> Option<&str> { + client + .api_key_env + .as_deref() + .filter(|_| env_key(client).is_none()) +} + +/// This function loads one endpoint's models and retains the cached list when refresh fails. +/// The selected endpoint alone receives the entered key. +pub fn load(cache: &Path, client: &Client, refresh: bool, typed_key: Option<&str>) -> Loaded { + let url = list_url(client); + let old = read_cache(cache).remove(&url); + if unlisted(client) { + // ChatGPT has no list to fetch, so nothing goes in the cache file. + return Loaded { + source: "local", + url, + list: codex_models(), + error: None, + }; + } + if old.is_some() && !refresh { + return Loaded { + source: "cache", + url, + list: old, + error: None, + }; + } + match key(client, typed_key).and_then(|key| list(client, key.as_deref())) { + Ok(models) => { + let new = ModelList { + models, + fetched_at: now(), + }; + let error = write_cache(cache, &url, &new) + .err() + .map(ListError::NotCached); + Loaded { + source: "endpoint", + url, + list: Some(new), + error, + } + } + Err(error) => Loaded { + source: "cache", + url, + list: old, + error: Some(error), + }, + } +} + +/// This function returns the URL that serves the client's model list. It is also the +/// list's key in the cache file and in [`Loaded::url`]. +pub fn list_url(client: &Client) -> String { + models_url(&client.format, &client.base_url) +} + +/// This function returns whether the client's endpoint has no model list that this app can +/// ask for. The ChatGPT Codex endpoint answers only a ChatGPT login, never an +/// API key, and the app does not hold the login. +pub fn unlisted(client: &Client) -> bool { + client.host() == "chatgpt.com" +} + +/// This function reads the model list that Codex saved in `$CODEX_HOME/models_cache.json`, +/// or in `~/.codex/models_cache.json` when `CODEX_HOME` is unset. Codex +/// fetched that list with the user's ChatGPT login. For a ChatGPT client this +/// file is the only list of its models that the app can read. +fn codex_models() -> Option { + let home = std::env::var_os("CODEX_HOME") + .map(PathBuf::from) + .unwrap_or_else(|| expand_home(Path::new("~/.codex"))); + read_codex_models(&home.join("models_cache.json")) +} + +/// This function parses a Codex model list file. Returns every model's `slug`, sorted and +/// without duplicates, and the time Codex fetched the file. +fn read_codex_models(path: &Path) -> Option { + #[derive(Deserialize)] + struct Saved { + fetched_at: String, + models: Vec, + } + #[derive(Deserialize)] + struct Entry { + slug: String, + } + + let saved: Saved = serde_json::from_str(&std::fs::read_to_string(path).ok()?).ok()?; + let fetched_at = chrono::DateTime::parse_from_rfc3339(&saved.fetched_at).ok()?; + let mut models: Vec = saved.models.into_iter().map(|entry| entry.slug).collect(); + models.sort(); + models.dedup(); + Some(ModelList { + models, + fetched_at: u64::try_from(fetched_at.timestamp()).ok()?, + }) +} + +/// This function lists the client's model IDs, sorted. When the client needs a key and +/// `key` is `None`, returns [`ListError::NoEnv`] or [`ListError::NoKey`] +/// without sending a request. +fn list(client: &Client, key: Option<&str>) -> Result, ListError> { + match (key, &client.api_key_env) { + (None, Some(variable)) => Err(ListError::NoEnv(variable.clone())), + (None, None) if client.forward_auth => Err(ListError::NoKey), + _ => fetch(&list_url(client), &client.format, key), + } +} + +/// This function returns the models endpoint for a client. It builds the URL the way the +/// server builds its request URLs. OpenAI clients drop a trailing +/// `/chat/completions` or `/responses`. Anthropic clients drop a trailing +/// `/v1/messages` to `/v1`, and add `/v1` to any other path. +/// Anthropic returns 20 models unless the URL sets `limit`, so the URL adds +/// `limit=1000`, the most that Anthropic allows. +fn models_url(format: &str, base_url: &str) -> String { + let (base, query) = match base_url.split_once('?') { + Some((base, query)) => (base, format!("?{query}")), + None => (base_url, String::new()), + }; + let base = base.trim_end_matches('/'); + let path = if format == "anthropic_messages" { + let root = base + .strip_suffix("/messages") + .filter(|root| root.ends_with("/v1")) + .unwrap_or(base); + if root.ends_with("/v1") { + format!("{root}/models") + } else { + format!("{root}/v1/models") + } + } else { + let root = base + .strip_suffix("/chat/completions") + .or_else(|| base.strip_suffix("/responses")) + .unwrap_or(base); + format!("{root}/models") + }; + let query = if format == "anthropic_messages" && query.is_empty() { + "?limit=1000".to_string() + } else if format == "anthropic_messages" { + format!("{query}&limit=1000") + } else { + query + }; + format!("{path}{query}") +} + +/// This function returns whether the server sends this client a key. If it does, listing +/// the client's models needs a key too. +fn sends_key(client: &Client) -> bool { + client.forward_auth || client.api_key_env.is_some() +} + +/// This function returns the key to list the client's models with, or `None` when the +/// client needs no key or none is available. +/// +/// The entered key takes precedence over the environment and saved credentials. +fn key(client: &Client, typed_key: Option<&str>) -> Result, ListError> { + if !sends_key(client) { + return Ok(None); + } + if let Some(key) = typed_key.map(str::to_string).or_else(|| env_key(client)) { + return Ok(Some(key)); + } + saved_key(&client.base_url).map_err(ListError::Keychain) +} + +fn env_key(client: &Client) -> Option { + client + .api_key_env + .as_deref() + .and_then(|variable| std::env::var(variable).ok()) + .filter(|key| !key.trim().is_empty()) +} + +/// This function returns the key saved in the Keychain for `base_url`, or `None` when no +/// key is saved. +#[cfg(target_os = "macos")] +fn saved_key(base_url: &str) -> Result, String> { + /// NOT_FOUND matches the Keychain's `errSecItemNotFound` status. + const NOT_FOUND: i32 = -25300; + match security_framework::passwords::get_generic_password(KEYCHAIN_SERVICE, base_url) { + Ok(bytes) => String::from_utf8(bytes) + .map(|key| Some(key).filter(|key| !key.trim().is_empty())) + .map_err(|_| "the saved key is not text".to_string()), + Err(error) if error.code() == NOT_FOUND => Ok(None), + Err(error) => Err(keychain_error(&error)), + } +} + +#[cfg(not(any(target_os = "macos", windows)))] +fn saved_key(_base_url: &str) -> Result, String> { + Ok(None) +} + +/// This function saves a key in the login Keychain for every client with this `base_url`. +#[cfg(target_os = "macos")] +pub fn save_key(base_url: &str, key: &str) -> Result<(), String> { + security_framework::passwords::set_generic_password( + KEYCHAIN_SERVICE, + base_url, + key.trim().as_bytes(), + ) + .map_err(|error| keychain_error(&error)) +} + +#[cfg(not(any(target_os = "macos", windows)))] +/// This function rejects saved keys on platforms without Keychain or Credential Manager support. +pub fn save_key(_base_url: &str, _key: &str) -> Result<(), String> { + Err("Saving model-list keys requires macOS Keychain or Windows Credential Manager. On this platform, set the environment variable named by api_key_env and make it available to the app.".into()) +} + +#[cfg(windows)] +/// This function reads a saved key for this exact base_url from Windows Credential Manager. +fn saved_key(base_url: &str) -> Result, String> { + switchyard_desktop_install::windows::saved_key(base_url) +} +#[cfg(windows)] +/// This function saves the key for this exact base_url in Windows Credential Manager. +pub fn save_key(base_url: &str, key: &str) -> Result<(), String> { + switchyard_desktop_install::windows::save_key(base_url, key) +} + +/// This function returns the Keychain's message without its final period, so the window +/// can end its own sentence after it. +#[cfg(target_os = "macos")] +fn keychain_error(error: &security_framework::base::Error) -> String { + error.to_string().trim_end_matches('.').to_string() +} + +/// This function returns whether a key has a line break, which would end the header that +/// carries the key and start another one. +pub fn has_line_break(key: &str) -> bool { + key.contains(['\r', '\n']) +} + +/// This function reads the cache file. A missing or unreadable file reads as empty, so the +/// app fetches its lists again and writes a new file. +fn read_cache(path: &Path) -> BTreeMap { + std::fs::read_to_string(path) + .ok() + .and_then(|text| serde_json::from_str(&text).ok()) + .unwrap_or_default() +} + +/// This function adds the list at `url` to the cache file. The list replaces the one at +/// the same URL unless that one is newer: two loads can overlap, and the +/// load that fetched a list first can write it last. The text goes to a +/// temporary file that is renamed over the cache file, so a reader never +/// sees half a file. +fn write_cache(path: &Path, url: &str, list: &ModelList) -> Result<(), String> { + let _writing = CACHE_WRITE.lock().unwrap_or_else(PoisonError::into_inner); + let mut all = read_cache(path); + if all + .get(url) + .is_some_and(|cached| cached.fetched_at > list.fetched_at) + { + return Ok(()); + } + all.insert(url.to_string(), list.clone()); + let text = serde_json::to_string_pretty(&all) + .map_err(|error| format!("write {}: {error}", path.display()))?; + let dir = path + .parent() + .filter(|dir| !dir.as_os_str().is_empty()) + .unwrap_or(Path::new(".")); + let mut file = tempfile::NamedTempFile::new_in(dir) + .map_err(|error| format!("create a file in {}: {error}", dir.display()))?; + file.write_all(text.as_bytes()) + .and_then(|()| file.as_file().sync_all()) + .map_err(|error| format!("write {}: {error}", path.display()))?; + file.persist(path) + .map_err(|error| format!("replace {}: {}", path.display(), error.error))?; + Ok(()) +} + +/// This function returns the current time in seconds since 1970-01-01 UTC. +pub fn now() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_or(0, |since| since.as_secs()) +} + +fn fetch(url: &str, format: &str, key: Option<&str>) -> Result, ListError> { + let failed = |error: String| ListError::Failed(error); + if key.is_some_and(has_line_break) { + return Err(failed( + "the key has a line break, so the app did not send it".to_string(), + )); + } + let mut command = Command::new("curl"); + #[cfg(windows)] + switchyard_desktop_install::windows::hide_console(&mut command); + let mut child = command + // curl honors `-q` only as its first argument. `-q` makes curl ignore + // ~/.curlrc, where a `verbose` line would print the key into the + // error text that the window shows. + .args([ + "-q", + "--silent", + "--show-error", + "--max-time", + TIMEOUT_SECONDS, + ]) + // curl reads request headers from stdin, one per line. + .args(["--header", "@-"]) + // `--url` keeps a URL that starts with `-` from being read as an + // option. + .args(["--write-out", "\n%{http_code}", "--url", url]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(|error| failed(format!("run curl: {error}")))?; + if let Some(mut stdin) = child.stdin.take() { + let headers = match (key, format) { + (Some(key), "anthropic_messages") => { + format!("x-api-key: {key}\nanthropic-version: 2023-06-01\n") + } + (Some(key), _) => format!("Authorization: Bearer {key}\n"), + (None, _) => String::new(), + }; + let sent = stdin.write_all(headers.as_bytes()); + drop(stdin); + if let Err(error) = sent { + let _ = child.kill(); + let _ = child.wait(); + return Err(failed(format!("send headers to curl: {error}"))); + } + } + let output = child + .wait_with_output() + .map_err(|error| failed(format!("run curl: {error}")))?; + if !output.status.success() { + return Err(failed( + String::from_utf8_lossy(&output.stderr).trim().to_string(), + )); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + let (body, status) = stdout.rsplit_once('\n').unwrap_or(("", &stdout)); + match status.trim().parse::() { + Ok(200) => parse_ids(body).map_err(failed), + Ok(status @ (401 | 403)) => Err(ListError::Rejected(status)), + Ok(status) => Err(failed(format!("{url} answered HTTP {status}"))), + Err(_) => Err(failed(format!("{url} sent no HTTP status"))), + } +} + +/// This function parses the model IDs. OpenAI and Anthropic both answer with +/// `{"data": [{"id": ...}, ...]}`. +fn parse_ids(body: &str) -> Result, String> { + #[derive(Deserialize)] + struct Listing { + data: Vec, + } + #[derive(Deserialize)] + struct Entry { + id: String, + } + + let listing: Listing = serde_json::from_str(body) + .map_err(|error| format!("the model list is not the expected JSON: {error}"))?; + let mut ids: Vec = listing.data.into_iter().map(|entry| entry.id).collect(); + ids.sort(); + ids.dedup(); + Ok(ids) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::{BufRead, BufReader}; + use std::net::{SocketAddr, TcpListener, TcpStream}; + use std::sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }; + + /// Stub returns the configured response and records each request header. + /// Drop wakes its listener and joins the worker so a failed assertion cannot leave it running. + struct Stub { + /// This field sets the client base URL used to list models from this server. + url: String, + requests: Arc>>>, + address: SocketAddr, + stopping: Arc, + worker: Option>, + } + + impl Stub { + fn start(response: String) -> Self { + let listener = TcpListener::bind("127.0.0.1:0").expect("bind"); + let address = listener.local_addr().expect("addr"); + let url = format!("http://{address}/v1"); + let requests = Arc::new(Mutex::new(Vec::new())); + let stopping = Arc::new(AtomicBool::new(false)); + let stop = Arc::clone(&stopping); + let record = Arc::clone(&requests); + let worker = std::thread::spawn(move || { + for mut stream in listener.incoming().map_while(Result::ok) { + if stop.load(Ordering::Acquire) { + break; + } + let mut head = Vec::new(); + let mut reader = BufReader::new(stream.try_clone().expect("clone stream")); + let mut line = String::new(); + while reader.read_line(&mut line).is_ok_and(|read| read > 0) && line != "\r\n" { + head.push(line.trim_end().to_string()); + line.clear(); + } + record.lock().expect("lock").push(head); + let _ = stream.write_all(response.as_bytes()); + } + }); + Self { + url, + requests, + address, + stopping, + worker: Some(worker), + } + } + + fn requests(&self) -> Vec> { + self.requests.lock().expect("lock").clone() + } + } + + impl Drop for Stub { + fn drop(&mut self) { + self.stopping.store(true, Ordering::Release); + let _ = TcpStream::connect(self.address); + if let Some(worker) = self.worker.take() { + let _ = worker.join(); + } + } + } + + fn listing(models: &[&str]) -> String { + let data: Vec<_> = models + .iter() + .map(|id| serde_json::json!({ "id": id })) + .collect(); + format!( + "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n{}", + serde_json::json!({ "data": data }) + ) + } + + fn client(name: &str, format: &str, base_url: &str) -> Client { + Client { + name: name.to_string(), + format: format.to_string(), + base_url: base_url.to_string(), + api_key_env: None, + forward_auth: true, + } + } + + // The subprocess isolates PATH; its curl fixture exits before reading the oversized header. + #[cfg(unix)] + #[test] + fn failed_header_write_reaps_the_curl_process() { + use std::os::unix::fs::PermissionsExt; + if let Ok(record) = std::env::var("SWITCHYARD_CURL_FIXTURE") { + let error = fetch( + "http://localhost/models", + "openai_chat", + Some(&"k".repeat(1024 * 1024)), + ) + .expect_err("header write fails"); + assert!( + matches!(error, ListError::Failed(message) if message.starts_with("send headers to curl:")) + ); + let pid = std::fs::read_to_string(record).expect("curl PID"); + assert!( + !Command::new("/bin/kill") + .args(["-0", pid.trim()]) + .status() + .expect("process status") + .success() + ); + return; + } + let dir = tempfile::tempdir().expect("fixture"); + let record = dir.path().join("pid"); + let curl = dir.path().join("curl"); + std::fs::write( + &curl, + "#!/bin/sh\nprintf '%s' \"$$\" > \"$SWITCHYARD_CURL_FIXTURE\"\nexit 1\n", + ) + .expect("curl fixture"); + std::fs::set_permissions(&curl, std::fs::Permissions::from_mode(0o700)) + .expect("permissions"); + let output = Command::new(std::env::current_exe().expect("runner")) + .args([ + "--exact", + "models::tests::failed_header_write_reaps_the_curl_process", + ]) + .env("SWITCHYARD_CURL_FIXTURE", &record) + .env("PATH", dir.path()) + .output() + .expect("child"); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stdout) + ); + } + + #[test] + fn entered_key_is_sent_only_to_its_selected_base_url() { + let selected = Stub::start(listing(&["selected"])); + let unrelated = Stub::start(listing(&["unrelated"])); + let unauthenticated = Stub::start(listing(&["public"])); + let dir = tempfile::tempdir().expect("fixture"); + let clients = [ + client("selected", "openai_chat", &selected.url), + Client { + api_key_env: Some("SWITCHYARD_MENUBAR_TEST_UNSET_KEY".into()), + ..client("unrelated", "openai_chat", &unrelated.url) + }, + Client { + forward_auth: false, + ..client("public", "openai_chat", &unauthenticated.url) + }, + ]; + let loaded: Vec<_> = clients + .iter() + .enumerate() + .map(|(index, client)| { + load( + &dir.path().join(CACHE_FILE), + client, + true, + (index == 0).then_some("selected-secret"), + ) + }) + .collect(); + assert_eq!(loaded[0].error, None); + assert_eq!(loaded[0].source, "endpoint"); + assert!( + !std::fs::read_to_string(dir.path().join(CACHE_FILE)) + .expect("cache") + .contains("selected-secret") + ); + assert_eq!(selected.requests().len(), 1); + assert!(selected.requests()[0].contains(&"Authorization: Bearer selected-secret".into())); + assert_eq!( + loaded[1].error, + Some(ListError::NoEnv("SWITCHYARD_MENUBAR_TEST_UNSET_KEY".into())) + ); + assert!(unrelated.requests().is_empty()); + assert_eq!(loaded[2].error, None); + assert_eq!(loaded[2].source, "endpoint"); + assert_eq!(unauthenticated.requests().len(), 1); + assert!( + unauthenticated.requests()[0] + .iter() + .all(|header| !header.starts_with("Authorization:")) + ); + } + + #[test] + fn reports_a_rejected_key_and_a_missing_key() { + let stub = + Stub::start("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n{}".to_string()); + let gateway = client("gateway", "openai_chat", &stub.url); + let from_env = Client { + api_key_env: Some("SWITCHYARD_MENUBAR_TEST_UNSET_KEY".to_string()), + forward_auth: false, + ..gateway.clone() + }; + + assert_eq!(list(&gateway, Some("wrong")), Err(ListError::Rejected(401))); + assert_eq!(list(&gateway, None), Err(ListError::NoKey)); + assert_eq!( + list(&from_env, None), + Err(ListError::NoEnv( + "SWITCHYARD_MENUBAR_TEST_UNSET_KEY".to_string() + )) + ); + assert!(matches!( + list(&gateway, Some("key\r\nX-Injected: yes")), + Err(ListError::Failed(_)) + )); + assert_eq!( + stub.requests().len(), + 1, + "a missing key or a key with a line break sends no request" + ); + } +} diff --git a/crates/switchyard-desktop/src/pricing.rs b/crates/switchyard-desktop/src/pricing.rs new file mode 100644 index 0000000000..3d762b007f --- /dev/null +++ b/crates/switchyard-desktop/src/pricing.rs @@ -0,0 +1,204 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! This module compares recorded calls at configured rates with the baseline model estimate. + +use std::collections::BTreeMap; + +use serde::Deserialize; + +use crate::rollup::{ModelTokens, Totals}; + +const PER_MILLION: f64 = 1_000_000.0; + +/// ModelPrice stores rates per million tokens for one model. +#[derive(Clone, Copy, Debug, Deserialize, serde::Serialize, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct ModelPrice { + pub input_per_mtok: f64, + /// This field sets the cache-read rate and defaults to the full input rate. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cached_input_per_mtok: Option, + /// This field sets the rate for generated tokens, including reasoning. + pub output_per_mtok: f64, +} + +impl ModelPrice { + pub fn validate(&self) -> Result<(), String> { + for rate in [ + Some(self.input_per_mtok), + self.cached_input_per_mtok, + Some(self.output_per_mtok), + ] + .into_iter() + .flatten() + { + if !rate.is_finite() || rate < 0.0 { + return Err( + "Model prices must be finite, nonnegative dollars per million tokens.".into(), + ); + } + } + Ok(()) + } + + fn cost(&self, tokens: &ModelTokens) -> f64 { + let cached_rate = self.cached_input_per_mtok.unwrap_or(self.input_per_mtok); + (tokens.input as f64 * self.input_per_mtok + + tokens.cached_input as f64 * cached_rate + + tokens.output as f64 * self.output_per_mtok) + / PER_MILLION + } +} + +/// PriceTable stores rates by the model ID recorded in the routing log. +pub type PriceTable = BTreeMap; + +/// Savings stores estimated costs with and without routing. +#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize)] +pub struct Savings { + /// This field stores the estimated cost of recorded calls, including classifier calls. + pub actual: f64, + /// This field estimates caller-facing calls at the baseline model rates. + pub baseline: f64, +} + +impl Savings { + /// This function returns estimated dollars saved; routing overhead can make the value negative. + pub fn saved(&self) -> f64 { + self.baseline - self.actual + } + + /// This function returns the percentage saved, or `None` when the baseline cost is zero. + pub fn percent(&self) -> Option { + (self.baseline > 0.0).then(|| self.saved() / self.baseline * 100.0) + } +} + +/// This function prices a period against the baseline model. +/// +/// This function returns `None` when any model seen has no entry in the table, since a +/// partial bill would understate cost and overstate savings. +pub fn estimate(totals: &Totals, prices: &PriceTable, baseline_model: &str) -> Option { + if prices.values().any(|price| price.validate().is_err()) { + return None; + } + let baseline_price = prices.get(baseline_model)?; + + let mut actual = 0.0; + let mut baseline = 0.0; + for (model, tokens) in &totals.routed { + actual += prices.get(model)?.cost(tokens); + baseline += baseline_price.cost(tokens); + } + // Classifier calls are Switchyard's own cost. They have no baseline + // counterpart, so they only ever reduce the savings figure. + for (model, tokens) in &totals.classifier { + actual += prices.get(model)?.cost(tokens); + } + + Some(Savings { actual, baseline }) +} + +/// This function returns the models in `models` that have no entry in `prices`, sorted and +/// each once. Such a model makes [`estimate`] return `None`. +pub fn unpriced<'a>( + models: impl IntoIterator, + prices: &PriceTable, +) -> Vec<&'a str> { + let mut missing: Vec<&str> = models + .into_iter() + .filter(|model| !prices.contains_key(*model)) + .collect(); + missing.sort_unstable(); + missing.dedup(); + missing +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rates_reject_negative_nonfinite_and_unknown_fields() { + for rate in [-1.0, f64::INFINITY, f64::NAN] { + for field in 0..3 { + let mut price = ModelPrice { + input_per_mtok: 0.0, + cached_input_per_mtok: Some(0.0), + output_per_mtok: 0.0, + }; + match field { + 0 => price.input_per_mtok = rate, + 1 => price.cached_input_per_mtok = Some(rate), + _ => price.output_per_mtok = rate, + } + assert!(price.validate().is_err()); + } + } + assert!( + toml::from_str::( + "input_per_mtok=0.0\noutput_per_mtok=1.0\ncached_input_per_mtok_typo=0.0" + ) + .is_err() + ); + } + + #[test] + fn estimates_cost_without_hiding_overhead_or_missing_prices() { + let price = ModelPrice { + input_per_mtok: 1.0, + cached_input_per_mtok: Some(0.1), + output_per_mtok: 2.0, + }; + let prices = PriceTable::from([ + ("baseline".into(), price), + ( + "cheap".into(), + ModelPrice { + input_per_mtok: 0.2, + cached_input_per_mtok: None, + output_per_mtok: 0.4, + }, + ), + ]); + let mut totals = Totals::default(); + let empty = estimate(&totals, &prices, "baseline").expect("empty"); + assert_eq!(empty.saved(), 0.0); + assert!(empty.percent().is_none()); + totals.routed.insert( + "cheap".into(), + ModelTokens { + requests: 1, + input: 1_000_000, + cached_input: 1_000_000, + output: 100_000, + }, + ); + let routed = estimate(&totals, &prices, "baseline").expect("priced"); + assert!((routed.actual - 0.44).abs() < 1e-9); + assert!((routed.baseline - 1.3).abs() < 1e-9); + assert!((routed.percent().expect("percent") - (1.3 - 0.44) / 1.3 * 100.0).abs() < 1e-9); + totals.classifier.insert( + "baseline".into(), + ModelTokens { + requests: 1, + input: 2_000_000, + ..ModelTokens::default() + }, + ); + let overhead = estimate(&totals, &prices, "baseline").expect("overhead"); + assert_eq!(overhead.baseline, routed.baseline); + assert!((overhead.actual - 2.44).abs() < 1e-9); + assert!(overhead.saved() < 0.0); + totals + .routed + .insert("missing".into(), ModelTokens::default()); + assert!(estimate(&totals, &prices, "baseline").is_none()); + assert!(estimate(&Totals::default(), &prices, "missing").is_none()); + assert_eq!( + unpriced(["cheap", "missing", "other", "missing"], &prices), + ["missing", "other"] + ); + } +} diff --git a/crates/switchyard-menubar/src/rollup.rs b/crates/switchyard-desktop/src/rollup.rs similarity index 55% rename from crates/switchyard-menubar/src/rollup.rs rename to crates/switchyard-desktop/src/rollup.rs index c05a34780f..1afe095920 100644 --- a/crates/switchyard-menubar/src/rollup.rs +++ b/crates/switchyard-desktop/src/rollup.rs @@ -1,34 +1,34 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -//! Token totals for today and the past week, read from the server's routing log. +//! This module totals recorded model calls from the retained routing log. use std::collections::BTreeMap; use std::fs::{File, Metadata}; use std::io::{BufRead, BufReader, Seek, SeekFrom}; use std::path::Path; +use crate::history::Entry; use chrono::{DateTime, Local, NaiveDate}; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; -/// Tier value the server writes for classifier and judge calls. +/// CLASSIFIER_TIER matches the tier recorded for classifier and judge calls. const CLASSIFIER_TIER: &str = "classifier"; -/// Billable token counts for one model. -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +/// ModelTokens stores recorded token counts for one model. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] pub struct ModelTokens { pub requests: u64, - /// Input tokens billed at the full rate. Cache writes bill at that rate - /// too, so they are counted here rather than tracked separately. + /// This field counts input tokens priced at the full rate, including cache writes. pub input: u64, - /// Input tokens served from the provider cache. + /// This field counts input tokens served from the provider cache. pub cached_input: u64, - /// Generated tokens, including reasoning. + /// This field counts generated tokens, including reasoning. pub output: u64, } impl ModelTokens { - /// Every token the provider counted, cached reads included. + /// This function returns the recorded token total, including cached reads. pub fn total(&self) -> u64 { self.input + self.cached_input + self.output } @@ -41,20 +41,22 @@ impl ModelTokens { } } -/// Tokens for a period, split by who asked for them. +/// Totals separates caller-facing calls from classifier calls for a period. /// /// The split matters for savings: without Switchyard the `routed` calls would /// still have happened, but the `classifier` calls would not exist at all. -#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct Totals { - /// Calls that served the caller, keyed by the model that answered. + /// This field groups caller-facing calls by the model that answered. pub routed: BTreeMap, - /// Switchyard's own classifier and judge calls, keyed by model. + /// This field groups classifier and judge calls by model. pub classifier: BTreeMap, + /// Each route includes its answer calls and routing overhead. + pub routes: BTreeMap, } impl Totals { - fn add(&mut self, record: &Record) { + fn add(&mut self, record: &Entry) { let bucket = if record.tier == CLASSIFIER_TIER { &mut self.classifier } else { @@ -63,27 +65,31 @@ impl Totals { bucket .entry(record.model.clone()) .or_default() - .add(&record.tokens()); + .add(&ModelTokens::from(record)); + self.routes + .entry(record.route_id.clone()) + .or_default() + .add(&ModelTokens::from(record)); } fn merge(&mut self, other: &Self) { - for (model, tokens) in &other.routed { - self.routed.entry(model.clone()).or_default().add(tokens); - } - for (model, tokens) in &other.classifier { - self.classifier - .entry(model.clone()) - .or_default() - .add(tokens); + for (own, incoming) in [ + (&mut self.routed, &other.routed), + (&mut self.classifier, &other.classifier), + (&mut self.routes, &other.routes), + ] { + for (model, tokens) in incoming { + own.entry(model.clone()).or_default().add(tokens); + } } } - /// Calls made on the caller's behalf. + /// This function returns the number of caller-facing calls. pub fn requests(&self) -> u64 { self.routed.values().map(|tokens| tokens.requests).sum() } - /// Every token spent, Switchyard's own routing overhead included. + /// This function totals recorded tokens, including routing overhead. pub fn tokens(&self) -> u64 { self.routed .values() @@ -92,53 +98,43 @@ impl Totals { .sum() } - /// True when nothing was recorded. + /// This function returns true when neither group contains recorded calls. pub fn is_empty(&self) -> bool { self.routed.is_empty() && self.classifier.is_empty() } } -/// What the menu shows. -#[derive(Clone, Debug, Default)] +/// Usage stores totals from the retained log and seven local calendar days. +#[derive(Clone, Debug, Default, Serialize)] pub struct Usage { pub today: Totals, pub week: Totals, + pub all: Totals, + pub days: BTreeMap, + pub skipped: u64, } -/// Fields the rollup needs from one routing log line. -#[derive(Deserialize)] -struct Record { - ts: String, - model: String, - #[serde(default)] - tier: String, - #[serde(default)] - prompt_tokens: u64, - #[serde(default)] - cached_tokens: u64, - #[serde(default)] - completion_tokens: u64, -} - -impl Record { - fn tokens(&self) -> ModelTokens { - ModelTokens { +impl From<&Entry> for ModelTokens { + fn from(record: &Entry) -> Self { + Self { requests: 1, - // `prompt_tokens` is the whole input, cache reads included. - input: self.prompt_tokens.saturating_sub(self.cached_tokens), - cached_input: self.cached_tokens, - // Completion tokens already include reasoning tokens. - output: self.completion_tokens, + // The recorded input includes cached reads; output includes reasoning. + input: record.prompt_tokens.saturating_sub(record.cached_tokens), + cached_input: record.cached_tokens, + output: record.completion_tokens, } } } -/// Keeps daily totals and reads only newly completed log lines. +/// Reader keeps model and route totals without retaining individual calls. +/// Each read consumes only newly completed log lines. #[derive(Default)] pub struct Reader { metadata: Option, offset: u64, days: BTreeMap, + all: Totals, + skipped: u64, } impl Reader { @@ -161,6 +157,8 @@ impl Reader { { self.offset = 0; self.days.clear(); + self.all = Totals::default(); + self.skipped = 0; } self.metadata = Some(metadata); file.seek(SeekFrom::Start(self.offset))?; @@ -177,24 +175,37 @@ impl Reader { break; } self.offset += bytes as u64; - let Ok(record) = serde_json::from_slice::(&line) else { + // Entry applies the same token and optional-ID rules as recent history. + let Ok(record) = serde_json::from_slice::(&line) else { + self.skipped += 1; continue; }; + if record.model.trim().is_empty() { + self.skipped += 1; + continue; + } let Ok(ts) = DateTime::parse_from_rfc3339(&record.ts) else { + self.skipped += 1; continue; }; let day = ts.with_timezone(&Local).date_naive(); + self.all.add(&record); if day >= week_start { self.days.entry(day).or_default().add(&record); } } - let mut usage = Usage::default(); + let mut usage = Usage { + all: self.all.clone(), + skipped: self.skipped, + ..Usage::default() + }; for (day, totals) in &self.days { if *day == today { usage.today = totals.clone(); } if *day <= today { usage.week.merge(totals); + usage.days.insert(*day, totals.clone()); } } Ok(usage) @@ -219,15 +230,11 @@ fn is_same_file(left: &Metadata, right: &Metadata) -> bool { mod tests { use super::*; - fn read(path: &Path, today: NaiveDate) -> std::io::Result { - Reader::default().read(path, today) - } - fn date(text: &str) -> NaiveDate { text.parse().expect("valid date") } - /// Builds a log line stamped at a local time, mirroring how the server + /// This function builds a log line stamped at a local time, mirroring how the server /// writes timestamps for events that happened in the local day. fn line(local_ts: &str, model: &str, tier: &str, prompt: u64, completion: u64) -> String { let offset = Local::now().offset().to_string(); @@ -244,105 +251,73 @@ mod tests { } #[test] - fn splits_routed_and_classifier_tokens() { - let (_dir, path) = log(&format!( - "{}\n{}\n", - line("2026-09-28T10:00:00.000", "luna", "", 1_000, 200), - line("2026-09-28T10:00:01.000", "terra", "classifier", 300, 10), - )); - - let usage = read(&path, date("2026-09-28")).expect("read"); - + fn aggregate_skipped_records_count_once_and_reset_with_the_log() { + use std::io::Write; + let dir = tempfile::tempdir().expect("fixture"); + let path = dir.path().join("routing.jsonl"); + let valid = line("2026-10-09T12:00:00", "actual", "answer", 1, 2); + let text = format!( + "{valid}\nnot-json\n{{\"ts\":\"bad\",\"model\":\"actual\"}}\n{{\"ts\":\"2026-10-09T12:00:00Z\",\"model\":\"\"}}\nunfinished" + ); + std::fs::write(&path, text).expect("log"); + let mut reader = Reader::default(); + assert_eq!( + reader + .read(&path, date("2026-10-09")) + .expect("read") + .skipped, + 3 + ); + assert_eq!( + reader + .read(&path, date("2026-10-09")) + .expect("refresh") + .skipped, + 3 + ); + std::fs::OpenOptions::new() + .append(true) + .open(&path) + .expect("append") + .write_all(b"\n") + .expect("complete record"); + assert_eq!( + reader + .read(&path, date("2026-10-09")) + .expect("read append") + .skipped, + 4 + ); + std::fs::write(&path, format!("{valid}\n")).expect("truncate"); assert_eq!( - usage.today.requests(), - 1, - "classifier calls are not requests" + reader + .read(&path, date("2026-10-09")) + .expect("read reset") + .skipped, + 0 ); - assert_eq!(usage.today.routed["luna"].input, 1_000); - assert_eq!(usage.today.classifier["terra"].input, 300); - assert_eq!(usage.today.tokens(), 1_000 + 200 + 300 + 10); } #[test] fn separates_cache_reads_from_full_price_input() { - let record: Record = serde_json::from_str( + let record: Entry = serde_json::from_str( r#"{"ts":"2026-09-28T10:00:00.000Z","model":"m","prompt_tokens":1000,"cached_tokens":600,"completion_tokens":50,"reasoning_tokens":25}"#, ) .expect("parse record"); - let tokens = record.tokens(); + let tokens = ModelTokens::from(&record); assert_eq!(tokens.input, 400); assert_eq!(tokens.cached_input, 600); assert_eq!(tokens.output, 50); assert_eq!(tokens.total(), 1_050); } - #[test] - fn counts_completion_tokens_once_and_prices_them() { - let record: Record = serde_json::from_str( - r#"{"ts":"2026-09-28T10:00:00.000Z","model":"m","prompt_tokens":12,"cached_tokens":0,"completion_tokens":5,"reasoning_tokens":3}"#, - ) - .expect("parse record"); - - let tokens = record.tokens(); - - assert_eq!(tokens.input, 12); - assert_eq!(tokens.output, 5); - assert_eq!(tokens.total(), 17); - let mut totals = Totals::default(); - totals.add(&record); - let prices = crate::pricing::PriceTable::from([( - "m".to_string(), - crate::pricing::ModelPrice { - input_per_mtok: 1.0, - cached_input_per_mtok: None, - output_per_mtok: 2.0, - }, - )]); - let savings = crate::pricing::estimate(&totals, &prices, "m").expect("priced"); - assert!((savings.actual - 0.000022).abs() < 1e-12); - } - - #[test] - fn the_week_covers_seven_days() { - let mut contents = String::new(); - for day in 18..=28 { - contents.push_str(&line( - &format!("2026-09-{day:02}T10:00:00.000"), - "luna", - "", - 100, - 10, - )); - contents.push('\n'); - } - let (_dir, path) = log(&contents); - - let usage = read(&path, date("2026-09-28")).expect("read"); - - assert_eq!(usage.today.requests(), 1); - assert_eq!(usage.week.requests(), 7); - } - - #[test] - fn skips_lines_it_cannot_parse() { - // The trailing line has no newline yet, as when the server is mid-write. - let (_dir, path) = log(&format!( - "not json\n{}\n{{\"ts\":\"2026-09-2", - line("2026-09-28T10:00:00.000", "luna", "", 100, 10), - )); - - let usage = read(&path, date("2026-09-28")).expect("read"); - - assert_eq!(usage.today.requests(), 1); - } - #[test] fn reads_appends_once_and_retries_an_unfinished_line() { use std::io::Write; let first = line("2026-09-28T10:00:00.000", "luna", "", 100, 10); let second = line("2026-09-28T10:00:01.000", "terra", "classifier", 30, 1); - let (_dir, path) = log(&format!("{first}\n")); + let (_dir, path) = log(&format!("not json\n{first}\n")); let today = date("2026-09-28"); let mut reader = Reader::default(); assert_eq!(reader.read(&path, today).expect("read").today.tokens(), 110); @@ -373,25 +348,40 @@ mod tests { assert_eq!(usage.today.classifier["terra"].requests, 1); } + // The old calls exceed the detail limit, so bounded history cannot supply these totals. #[test] - fn drops_old_days_without_rereading_the_log() { + fn totals_the_full_log_by_model_route_and_local_day() { + let old = line("2026-08-01T12:00:00.000", "older-model", "", 100, 10); + let answer = line("2026-09-28T12:00:00.000", "answer", "", 50, 5); + let judge = line("2026-09-28T12:00:01.000", "answer", "classifier", 20, 2); + let with_route = + |text: &str| text.replace("\"model\":", "\"route_id\":\"router\",\"model\":"); let (_dir, path) = log(&format!( - "{}\n{}\n", - line("2026-09-22T10:00:00.000", "luna", "", 100, 10), - line("2026-09-28T10:00:00.000", "luna", "", 100, 10), + "{}{}\n{}\n{}\n", + format!("{old}\n").repeat(5_001), + with_route(&answer), + with_route(&judge), + line("2026-09-28T12:00:02.000", " ", "", 900, 90) )); let mut reader = Reader::default(); + let usage = reader.read(&path, date("2026-09-28")).expect("read"); + assert_eq!(usage.all.requests(), 5_002); + assert_eq!(usage.all.tokens(), 550_187); + assert_eq!(usage.week.tokens(), 77); + assert_eq!(usage.today.routes["router"].requests, 2); + assert_eq!(usage.today.routes["router"].total(), 77); + assert_eq!(usage.days[&date("2026-09-28")], usage.today); assert_eq!( reader - .read(&path, date("2026-09-28")) - .expect("read") - .week - .requests(), - 2 + .read(&path, date("2026-09-29")) + .expect("next day") + .all, + usage.all + ); + assert_eq!( + crate::summary::tray_models(&usage.today), + ["answer — 77 tokens · calls: 2"] ); - let usage = reader.read(&path, date("2026-09-29")).expect("next day"); - assert_eq!(usage.today.requests(), 0); - assert_eq!(usage.week.requests(), 1); } #[test] @@ -409,20 +399,16 @@ mod tests { let usage = reader.read(&path, today).expect("read truncated"); assert_eq!(usage.today.requests(), 1); assert_eq!(usage.today.tokens(), 220); + assert_eq!(usage.all.tokens(), 220); std::fs::rename(&path, path.with_extension("old")).expect("rotate"); std::fs::write(&path, replacement.repeat(3)).expect("replace"); let usage = reader.read(&path, today).expect("read replacement"); assert_eq!(usage.today.requests(), 3); assert_eq!(usage.today.tokens(), 660); - } - - #[test] - fn a_missing_log_is_not_an_error() { - let dir = tempfile::tempdir().expect("tempdir"); - - let usage = read(&dir.path().join("absent.jsonl"), date("2026-09-28")).expect("read"); - - assert!(usage.today.is_empty()); + assert_eq!(usage.all.tokens(), 660); + std::fs::remove_file(&path).expect("remove log"); + let absent = reader.read(&path, today).expect("missing log"); + assert!(absent.today.is_empty() && absent.week.is_empty() && absent.all.is_empty()); } } diff --git a/crates/switchyard-desktop/src/server.rs b/crates/switchyard-desktop/src/server.rs new file mode 100644 index 0000000000..10af793704 --- /dev/null +++ b/crates/switchyard-desktop/src/server.rs @@ -0,0 +1,645 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! This module validates and saves server configs and restarts the server service. + +use std::fs::OpenOptions; +use std::io::{ErrorKind, Write}; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::{Duration, Instant}; + +use chrono::Local; + +use crate::config::Config; +use crate::health::{ServerStatus, probe}; +use crate::models; +use crate::server_config::{Algorithm, Choice, ServerConfig}; + +/// RESTART_WAIT limits how long the app waits for the restarted server to answer `/health`. +const RESTART_WAIT: Duration = Duration::from_secs(10); + +/// BACKUPS_PER_SECOND limits the number of backups for one file within one second. +const BACKUPS_PER_SECOND: u32 = 100; + +/// SaveResult distinguishes a saved config from a failed restart or health check. +pub struct SaveResult { + pub message: String, + pub warning: bool, + pub details: Vec, +} + +/// This function switches a route to new models, checks the result with the server's +/// `--dry-run`, saves it, and restarts the server. +/// +/// The result names the changed file and reports whether the restart succeeded. +/// The details contain the backup path and edit notes. An error means the file was not changed. +pub fn apply( + settings: &Config, + route: &str, + algorithm: &Algorithm, + choices: &[Choice], + expected: &str, +) -> Result { + let path = &settings.config_file; + // Read the file again so an edit made while the window was open is kept. + let text = std::fs::read_to_string(path).map_err(|error| { + format!( + "Could not read {}: {error}. Check config_file in the app settings.", + path.display() + ) + })?; + check_original(&text, expected)?; + let config = ServerConfig::parse(&text)?; + let edited = config.edit(route, algorithm, choices)?; + save_edit(settings, &text, edited, "Updated the route.") +} + +pub fn remove( + settings: &Config, + route: &str, + id: &str, + expected: &str, +) -> Result { + let text = std::fs::read_to_string(&settings.config_file).map_err(|error| error.to_string())?; + check_original(&text, expected)?; + let config = ServerConfig::parse(&text)?; + if !config.routes().iter().any(|r| r.key == route && r.id == id) { + return Err("The route changed or was removed. Refresh and choose it again.".into()); + } + let edited = config.remove(route)?; + save_edit(settings, &text, edited, "Removed the route.") +} + +pub fn save_route_change( + settings: &Config, + expected: &str, + edited: crate::server_config::Edited, +) -> Result { + let text = std::fs::read_to_string(&settings.config_file).map_err(|error| error.to_string())?; + check_original(&text, expected)?; + save_edit(settings, &text, edited, "Changed the routes.") +} + +fn check_original(text: &str, expected: &str) -> Result<(), String> { + if text != expected { + return Err("The server config changed since the editor loaded. Review it again; your draft is preserved.".into()); + } + Ok(()) +} + +fn save_edit( + settings: &Config, + text: &str, + edited: crate::server_config::Edited, + summary: &str, +) -> Result { + let path = &settings.config_file; + if edited.text == text { + return Ok(SaveResult { + message: "Nothing to save. The route already uses this algorithm and these models." + .into(), + warning: false, + details: Vec::new(), + }); + } + + let binary = server_binary()?; + let backup = save_checked(&binary, path, text, &edited.text).map_err(explain)?; + let saved = format!("{summary} Changed {}.", path.display()); + let health = format!("{}/health", settings.server_url); + let (message, warning) = match restart(&settings.launchd_label) { + Err(error) => ( + format!( + "{saved} Switchyard could not restart: {error}. Use Restart server in Settings to try again." + ), + true, + ), + Ok(()) if wait_for_health(&settings.server_url) => { + (format!("{saved} Restarted Switchyard."), false) + } + Ok(()) => ( + format!( + "{saved} Switchyard restarted but did not answer {health} within {} seconds. Check the server log in ~/.switchyard/logs/.", + RESTART_WAIT.as_secs() + ), + true, + ), + }; + let mut details = vec![format!("Backup: {}", backup.display())]; + details.extend(edited.notes); + if warning { + details.push(format!( + "To undo, copy {} over {} and use Restart server in Settings.", + backup.display(), + path.display() + )); + } + Ok(SaveResult { + message, + warning, + details, + }) +} + +/// This function adds advice to a check error that comes from a missing `api_key_env` +/// variable. The check runs with this app's environment, which is not the +/// user's shell environment. +fn explain(error: String) -> String { + let variable = error + .split_once("could not read api_key_env ") + .and_then(|(_, rest)| rest.split([':', ' ', '\n']).next()) + .filter(|variable| !variable.is_empty()) + .map(str::to_string); + match variable { + Some(variable) => format!("{error}\n{}", models::missing_env_note(&variable)), + None => error, + } +} + +/// This function finds the installed server from the app's installation metadata. +fn server_binary() -> Result { + let executable = std::env::current_exe().map_err(|error| { + format!( + "Could not find switchyard-server next to the desktop app: {error}. Run cargo desktop install from the Switchyard repository to install it." + ) + })?; + let metadata = switchyard_desktop_install::bundle_metadata(&executable); + if metadata.try_exists().map_err(|error| error.to_string())? { + return switchyard_desktop_install::Settings::load(&metadata) + .map(|settings| settings.binary("switchyard-server")); + } + Ok( + executable.with_file_name(switchyard_desktop_install::executable_name( + "switchyard-server", + )), + ) +} + +/// This function checks `text` with `switchyard-server --dry-run`, then replaces `path` +/// with it, keeping `original`, the text that `path` held, as a timestamped +/// backup. Returns the backup's path. +/// +/// The real file is `path`, or the file it points to when `path` is a +/// symlink. The new text goes to a temporary file next to the real file. The +/// temporary file is synced to disk and then renamed over the real file, so a +/// reader never sees a half-written config. When the check fails, or when +/// the real file no longer holds `original` after the backup is written, the +/// temporary file and the new backup are removed, `path` does not change, +/// and the error is returned. +fn save_checked(binary: &Path, path: &Path, original: &str, text: &str) -> Result { + // Resolve a symlink and replace the file it points to, so the link stays. + let file = std::fs::canonicalize(path) + .map_err(|error| format!("Could not find {}: {error}", path.display()))?; + let dir = file.parent().unwrap_or(Path::new("/")); + let mut candidate = tempfile::Builder::new() + .prefix(".switchyard-picker-") + .suffix(".toml") + .tempfile_in(dir) + .map_err(|error| format!("Could not create a file in {}: {error}", dir.display()))?; + candidate + .write_all(text.as_bytes()) + .and_then(|()| candidate.as_file().sync_all()) + .map_err(|error| format!("Could not write {}: {error}", candidate.path().display()))?; + if let Ok(metadata) = std::fs::metadata(&file) { + candidate + .as_file() + .set_permissions(metadata.permissions()) + .map_err(|error| { + format!( + "Could not copy the permissions of {}: {error}", + path.display() + ) + })?; + } + + // Name the real file in errors, not the temporary one. + check(binary, candidate.path(), path).map_err(|error| { + error.replace( + &candidate.path().display().to_string(), + &path.display().to_string(), + ) + })?; + + let backup = back_up(&file, original) + .map_err(|error| format!("Could not back up {}: {error}", path.display()))?; + // The check and the backup take a moment. Keep an edit that another + // program saved in the meantime. Compare right before the rename, because + // the rename replaces the file whatever it holds. + if std::fs::read_to_string(&file).ok().as_deref() != Some(original) { + let _ = std::fs::remove_file(&backup); + return Err(format!( + "Could not replace {}, because it changed on disk while Apply was checking it. \ + Click Apply again to apply your choices to the new file.", + path.display() + )); + } + candidate + .persist(&file) + .map_err(|error| format!("Could not replace {}: {}", path.display(), error.error))?; + Ok(backup) +} + +/// This function writes `text` to a new backup of `file` and returns its path. The backup +/// is `.switchyard-backup.`, the name the installer uses, +/// with `-2`, `-3`, and so on added when that name is taken, so a backup +/// never replaces another one. The backup gets the file's permissions before +/// it gets the text. +fn back_up(file: &Path, text: &str) -> Result { + let permissions = std::fs::metadata(file)?.permissions(); + let stamp = Local::now().format("%Y%m%d%H%M%S"); + for number in 1..=BACKUPS_PER_SECOND { + let suffix = if number == 1 { + String::new() + } else { + format!("-{number}") + }; + let backup = PathBuf::from(format!( + "{}.switchyard-backup.{stamp}{suffix}", + file.display() + )); + let mut out = match OpenOptions::new() + .write(true) + .create_new(true) + .open(&backup) + { + Ok(out) => out, + Err(error) if error.kind() == ErrorKind::AlreadyExists => continue, + Err(error) => return Err(error), + }; + let written = out + .set_permissions(permissions.clone()) + .and_then(|()| out.write_all(text.as_bytes())) + .and_then(|()| out.sync_all()); + if let Err(error) = written { + let _ = std::fs::remove_file(&backup); + return Err(error); + } + return Ok(backup); + } + Err(std::io::Error::new( + ErrorKind::AlreadyExists, + format!("{BACKUPS_PER_SECOND} backups from this second already exist"), + )) +} + +/// This function asks the installed helper to run the server's dry-run on `config`. +/// The helper reads saved credentials for `scope`, the original config file, when checking an edited candidate. +fn check(binary: &Path, config: &Path, scope: &Path) -> Result<(), String> { + let config = std::fs::canonicalize(config).map_err(|error| error.to_string())?; + let scope = std::fs::canonicalize(scope).map_err(|error| error.to_string())?; + let helper = binary.with_file_name(switchyard_desktop_install::executable_name( + "switchyard-desktop-install", + )); + let mut validator = Command::new(&helper); + validator + .arg("validate") + .arg("--server") + .arg(binary) + .arg("--config") + .arg(&config) + .arg("--scope") + .arg(&scope); + let output = validator + .output() + .map_err(|error| { + format!( + "Could not run {}: {error}. Apply needs the installed server and credential helper. Run cargo desktop install from the Switchyard repository to install them.", + helper.display() + ) + })?; + if output.status.success() { + return Ok(()); + } + let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + Err(if stderr.is_empty() { stdout } else { stderr }) +} + +pub fn check_config(settings: &Config) -> Result<(), String> { + check( + &server_binary()?, + &settings.config_file, + &settings.config_file, + ) + .map_err(explain) +} + +pub fn restart_checked(settings: &Config) -> Result { + check_config(settings)?; + restart(&settings.launchd_label)?; + if !wait_for_health(&settings.server_url) { + return Err(format!( + "Switchyard did not answer {}/health within {} seconds. Open the server log and retry.", + settings.server_url.trim_end_matches('/'), + RESTART_WAIT.as_secs() + )); + } + Ok("Restarted Switchyard.".into()) +} + +/// This function restarts the server's LaunchAgent, systemd user service, or Windows scheduled task. +pub fn restart(launchd_label: &str) -> Result<(), String> { + #[cfg(windows)] + { + let _ = launchd_label; + let exe = std::env::current_exe().map_err(|e| e.to_string())?; + let settings = switchyard_desktop_install::Settings::load( + &switchyard_desktop_install::bundle_metadata(&exe), + )?; + switchyard_desktop_install::windows::restart(&settings) + } + #[cfg(not(windows))] + { + if cfg!(target_os = "linux") { + return command("systemctl", &["--user", "restart", "switchyard.service"]).map(|_| ()); + } + let uid = command("id", &["-u"])?; + let target = format!("gui/{}/{launchd_label}", uid.trim()); + command("launchctl", &["kickstart", "-k", &target]).map(|_| ()) + } +} + +pub(crate) fn wait_for_health(server_url: &str) -> bool { + let deadline = Instant::now() + RESTART_WAIT; + loop { + if probe(server_url) == ServerStatus::Running { + return true; + } + if Instant::now() >= deadline { + return false; + } + std::thread::sleep(Duration::from_millis(250)); + } +} + +/// This function runs a command, returning its stdout or a message naming what failed. +pub fn command(program: &str, args: &[&str]) -> Result { + if program == "open" { + #[cfg(windows)] + { + return switchyard_desktop_install::windows::open( + args.first().ok_or("Choose a file or URL.")?, + ) + .map(|()| String::new()); + } + #[cfg(target_os = "linux")] + { + return command("xdg-open", args); + } + } + + let output = Command::new(program) + .args(args) + .output() + .map_err(|error| format!("Could not run {program}: {error}"))?; + if !output.status.success() { + return Err(format!( + "{program} {} failed: {}", + args.join(" "), + String::from_utf8_lossy(&output.stderr).trim() + )); + } + Ok(String::from_utf8_lossy(&output.stdout).into_owned()) +} + +// This function saves the script before opening Terminal so a save error cannot launch it. +// If open fails, this function attempts cleanup; callers supply scripts that remove themselves on startup. +#[cfg(not(windows))] +pub fn open_terminal_script(prefix: &str, text: &str) -> Result<(), String> { + let mut script = tempfile::Builder::new() + .prefix(prefix) + .suffix(".command") + .tempfile() + .map_err(|e| e.to_string())?; + writeln!(script, "{text}").map_err(|e| e.to_string())?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + script + .as_file() + .set_permissions(std::fs::Permissions::from_mode(0o700)) + .map_err(|e| e.to_string())?; + } + let (_, path) = script.keep().map_err(|e| e.to_string())?; + if let Err(error) = command("open", &[&path.display().to_string()]) { + let _ = std::fs::remove_file(path); + return Err(error); + } + Ok(()) +} + +#[cfg(all(test, unix))] +mod tests { + use super::*; + const OLD: &str = "schema_version=1\n[targets]\n[routes]\n#old\n"; + const NEW: &str = "schema_version=1\n[targets]\n[routes]\n#new\n"; + const NEWER: &str = "schema_version=1\n[targets]\n[routes]\n#newer\n"; + use std::os::unix::fs::PermissionsExt; + + /// The fixture forwards the helper's validation command to a stand-in server that runs `body`. + fn fake_server(dir: &Path, body: &str) -> PathBuf { + let path = dir.join("switchyard-server"); + std::fs::write(&path, format!("#!/bin/sh\n{body}\n")).expect("write script"); + std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)) + .expect("make script executable"); + let helper = dir.join("switchyard-desktop-install"); + std::fs::write(&helper, r#"#!/bin/sh +test "$#" -eq 7 && test "$1" = validate && test "$2" = --server && test "$4" = --config && test "$6" = --scope && test -f "$7" || exit 1 +exec "$3" --config "$5" --dry-run +"#).expect("write helper"); + std::fs::set_permissions(&helper, std::fs::Permissions::from_mode(0o755)) + .expect("make helper executable"); + path + } + + fn backups(dir: &Path) -> Vec { + std::fs::read_dir(dir) + .expect("read dir") + .map(|entry| entry.expect("entry").path()) + .filter(|path| path.display().to_string().contains(".switchyard-backup.")) + .collect() + } + + #[test] + fn a_config_that_fails_the_check_is_not_saved() { + let dir = tempfile::tempdir().expect("tempdir"); + let config = dir.path().join("composite.toml"); + std::fs::write(&config, OLD).expect("write config"); + // The real server names the file it was given; the error must name + // the user's file instead of the temporary copy. + let binary = fake_server( + dir.path(), + r#"echo "invalid server config $2: route gateway cannot forward both" >&2; exit 1"#, + ); + + let error = save_checked(&binary, &config, OLD, NEW).expect_err("check fails"); + + assert_eq!( + error, + format!( + "invalid server config {}: route gateway cannot forward both", + config.display() + ) + ); + assert_eq!(std::fs::read_to_string(&config).expect("read"), OLD); + assert!(backups(dir.path()).is_empty()); + assert_eq!( + std::fs::read_dir(dir.path()).expect("read dir").count(), + 3, + "the temporary file is removed" + ); + } + + #[test] + fn a_config_that_passes_the_check_replaces_the_file_and_keeps_a_backup() { + let dir = tempfile::tempdir().expect("tempdir"); + let config = dir.path().join("composite.toml"); + std::fs::write(&config, OLD).expect("write config"); + std::fs::set_permissions(&config, std::fs::Permissions::from_mode(0o640)) + .expect("set permissions"); + let binary = fake_server( + dir.path(), + r#"grep -q new "$2" && echo "server OK: gateway""#, + ); + + let backup = save_checked(&binary, &config, OLD, NEW).expect("saved"); + + assert_eq!(std::fs::read_to_string(&config).expect("read"), NEW); + let mode = |path: &Path| { + std::fs::metadata(path) + .expect("metadata") + .permissions() + .mode() + & 0o777 + }; + assert_eq!(mode(&config), 0o640, "the file keeps its permissions"); + assert_eq!(backups(dir.path()).len(), 1); + assert_eq!(std::fs::read_to_string(&backup).expect("read"), OLD); + assert_eq!(mode(&backup), 0o640, "the backup gets the same permissions"); + save_checked(&binary, &config, NEW, NEWER).expect("saved again"); + let mut kept: Vec<_> = backups(dir.path()) + .iter() + .map(|path| std::fs::read_to_string(path).expect("backup")) + .collect(); + kept.sort(); + assert_eq!(kept, [NEW, OLD]); + assert_eq!(std::fs::read_to_string(&config).expect("read"), NEWER); + } + + #[test] + fn a_file_that_changes_during_the_check_is_not_replaced() { + let dir = tempfile::tempdir().expect("tempdir"); + let config = dir.path().join("composite.toml"); + std::fs::write(&config, OLD).expect("write config"); + // Another program saves the file while the check runs. + let binary = fake_server( + dir.path(), + &format!("echo edited-elsewhere > '{}'", config.display()), + ); + + let error = save_checked(&binary, &config, OLD, NEW).expect_err("file changed"); + + assert!(error.contains(&config.display().to_string()), "{error}"); + assert_eq!( + std::fs::read_to_string(&config).expect("read"), + "edited-elsewhere\n" + ); + assert!(backups(dir.path()).is_empty()); + } + + #[test] + fn saving_through_a_symlink_replaces_the_linked_file() { + let dir = tempfile::tempdir().expect("tempdir"); + let real = dir.path().join("dotfiles.toml"); + let link = dir.path().join("composite.toml"); + std::fs::write(&real, OLD).expect("write config"); + std::os::unix::fs::symlink(&real, &link).expect("symlink"); + let binary = fake_server(dir.path(), r#"echo "server OK: gateway""#); + + save_checked(&binary, &link, OLD, NEW).expect("saved"); + + let link_type = std::fs::symlink_metadata(&link) + .expect("metadata") + .file_type(); + assert!(link_type.is_symlink(), "the link is kept"); + assert_eq!(std::fs::read_to_string(&real).expect("read"), NEW); + } + + // The subprocess isolates PATH so the launcher fixture rejects the script without opening an app. + #[test] + fn a_rejected_terminal_launch_removes_the_kept_script() { + if let Ok(record) = std::env::var("SWITCHYARD_TERMINAL_FIXTURE") { + assert!(open_terminal_script("switchyard-terminal-fixture-", "#!/bin/bash").is_err()); + let path = std::fs::read_to_string(record).expect("script path"); + assert!(!Path::new(path.trim()).exists()); + return; + } + let dir = tempfile::tempdir().expect("fixture"); + let record = dir.path().join("script-path"); + let open = dir.path().join(if cfg!(target_os = "linux") { + "xdg-open" + } else { + "open" + }); + std::fs::write( + &open, + "#!/bin/bash\nprintf '%s' \"$1\" > \"$SWITCHYARD_TERMINAL_FIXTURE\"\nexit 1\n", + ) + .expect("Terminal stub"); + std::fs::set_permissions(&open, std::fs::Permissions::from_mode(0o700)) + .expect("permissions"); + let output = Command::new(std::env::current_exe().expect("runner")) + .args([ + "--exact", + "server::tests::a_rejected_terminal_launch_removes_the_kept_script", + ]) + .env("SWITCHYARD_TERMINAL_FIXTURE", &record) + .env("PATH", dir.path()) + .output() + .expect("child"); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stdout) + ); + } + + #[cfg(target_os = "linux")] + #[test] + fn restart_uses_the_linux_user_service_and_returns_command_failure() { + // The subprocess uses a fixture systemctl so this test cannot restart the user's server. + if let Ok(expected_failure) = std::env::var("SWITCHYARD_RESTART_FIXTURE") { + let result = restart("unused-on-linux"); + if expected_failure == "1" { + assert!(result.expect_err("failed restart").contains("systemctl")); + } else { + result.expect("restart"); + } + return; + } + let dir = tempfile::tempdir().expect("fixture"); + let record = dir.path().join("arguments"); + let systemctl = dir.path().join("systemctl"); + std::fs::write(&systemctl, "#!/bin/sh\nprintf '%s\\n' \"$@\" > \"$SWITCHYARD_RESTART_ARGUMENTS\"\nexit \"$SWITCHYARD_RESTART_FIXTURE\"\n").expect("service fixture"); + std::fs::set_permissions(&systemctl, std::fs::Permissions::from_mode(0o700)) + .expect("permissions"); + for failure in ["0", "1"] { + let output = Command::new(std::env::current_exe().expect("runner")) + .args(["--exact", "server::tests::restart_uses_the_linux_user_service_and_returns_command_failure"]) + .env("SWITCHYARD_RESTART_FIXTURE", failure) + .env("SWITCHYARD_RESTART_ARGUMENTS", &record) + .env("PATH", dir.path()) + .output().expect("child"); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stdout) + ); + assert_eq!( + std::fs::read_to_string(&record).expect("service arguments"), + "--user\nrestart\nswitchyard.service\n" + ); + } + } +} diff --git a/crates/switchyard-desktop/src/server_config.rs b/crates/switchyard-desktop/src/server_config.rs new file mode 100644 index 0000000000..e3e9de21d5 --- /dev/null +++ b/crates/switchyard-desktop/src/server_config.rs @@ -0,0 +1,1746 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! This module reads the server config and rewrites one route's algorithm and models. +//! +//! Edits go through `toml_edit`, so comments, formatting, and every table the +//! edit does not touch stay as the user wrote them. The server's own +//! `--dry-run` decides whether the result is valid; this module only builds it. + +use std::collections::HashMap; + +use toml_edit::{DocumentMut, Item, Table, TableLike, Value}; + +/// Tier identifies the part a model plays in a route. A choice made for one algorithm carries +/// over to the role with the same tier when the user switches algorithms. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum Tier { + Judge, + Capable, + Efficient, +} + +/// Algorithm describes a route `type` the apps can write. +#[derive(Debug)] +pub struct Algorithm { + pub kind: &'static str, + /// This field sets the name shown in both apps. + pub title: &'static str, + pub summary: &'static str, + /// This field lists roles in display order. The list is empty for + /// `random`, whose roles are the entries of its `targets` list. + roles: &'static [RoleSpec], + /// This field lists settings written when a route switches to this type. They + /// match the values the routing docs use in their examples. + settings: &'static [(&'static [&'static str], Setting)], + /// This field records whether the type accepts a nested `subagents` policy. + subagents: bool, +} + +/// RoleSpec describes one role of an algorithm. +#[derive(Debug)] +struct RoleSpec { + label: &'static str, + /// This field explains what the role does. + hint: &'static str, + tier: Tier, + /// This field names the key path to the role's target under the route. + path: &'static [&'static str], +} + +#[derive(Debug)] +enum Setting { + Text(&'static str), + Number(f64), +} + +impl Setting { + fn value(&self) -> Value { + match self { + Self::Text(text) => Value::from(*text), + Self::Number(number) => Value::from(*number), + } + } +} + +const RANDOM: &str = "random"; + +/// ALGORITHMS lists supported route types in display order. +/// `noop` and the experimental `prefill_router` are left out: one calls no +/// model, and the other needs a checkpoint file rather than models. +pub const ALGORITHMS: &[Algorithm] = &[ + Algorithm { + kind: "passthrough", + title: "Single model", + summary: "This algorithm sends every request to one model.", + roles: &[RoleSpec { + label: "Model", + hint: "This model answers every request.", + tier: Tier::Capable, + path: &["target"], + }], + settings: &[], + subagents: true, + }, + Algorithm { + kind: RANDOM, + title: "Random split", + summary: "This algorithm splits requests at random between models for A/B tests and baselines.", + roles: &[], + settings: &[], + subagents: false, + }, + Algorithm { + kind: "llm_classifier", + title: "Judge per request", + summary: "A judge model reads each request and picks the capable or the efficient model.", + roles: &[ + RoleSpec { + label: "Judge", + hint: "The judge reads each request and picks a model.", + tier: Tier::Judge, + path: &["classifier_target"], + }, + RoleSpec { + label: "Capable", + hint: "The capable model answers the hard requests.", + tier: Tier::Capable, + path: &["strong_target"], + }, + RoleSpec { + label: "Efficient", + hint: "The efficient model answers the easy requests.", + tier: Tier::Efficient, + path: &["weak_target"], + }, + ], + settings: &[ + (&["mode"], Setting::Text("capability")), + (&["base_threshold"], Setting::Number(0.5)), + ], + subagents: false, + }, + Algorithm { + kind: "composite", + title: "Judge plus stage router", + summary: "A judge picks the starting model on each user turn, and tool results move \ + requests between the capable and the efficient model.", + roles: &[ + RoleSpec { + label: "Judge", + hint: "The judge picks the starting model on each user turn.", + tier: Tier::Judge, + path: &["classifier", "target"], + }, + RoleSpec { + label: "Capable", + hint: "The capable model takes the hard steps.", + tier: Tier::Capable, + path: &["stage", "capable_target"], + }, + RoleSpec { + label: "Efficient", + hint: "The efficient model takes the easy steps.", + tier: Tier::Efficient, + path: &["stage", "efficient_target"], + }, + ], + settings: &[ + (&["classifier", "base_threshold"], Setting::Number(0.5)), + ( + &["classifier", "classify_trigger"], + Setting::Text("user_turn"), + ), + (&["stage", "confidence_threshold"], Setting::Number(0.5)), + ], + subagents: true, + }, + Algorithm { + kind: "stage_router", + title: "Stage router", + summary: "Tool results and agent progress move each request between the capable and \ + the efficient model.", + roles: &[ + RoleSpec { + label: "Capable", + hint: "The capable model takes the hard steps.", + tier: Tier::Capable, + path: &["capable_target"], + }, + RoleSpec { + label: "Efficient", + hint: "The efficient model takes the easy steps.", + tier: Tier::Efficient, + path: &["efficient_target"], + }, + ], + settings: &[ + (&["picker"], Setting::Text("efficient_first")), + (&["confidence_threshold"], Setting::Number(0.5)), + ], + subagents: true, + }, + Algorithm { + kind: "advisor", + title: "Executor and advisor", + summary: "The executor answers every turn, and a stronger advisor reviews its final \ + answers.", + roles: &[ + RoleSpec { + label: "Executor", + hint: "The executor answers every turn.", + tier: Tier::Efficient, + path: &["executor_target"], + }, + RoleSpec { + label: "Advisor", + hint: "The advisor reviews the executor's final answers.", + tier: Tier::Capable, + path: &["advisor_target"], + }, + ], + settings: &[], + subagents: false, + }, + Algorithm { + kind: "plan_execute", + title: "Plan, then execute", + summary: "The capable model inspects and plans. The efficient model takes over after \ + the first file edit.", + roles: &[ + RoleSpec { + label: "Capable", + hint: "The capable model inspects the code and plans.", + tier: Tier::Capable, + path: &["capable_target"], + }, + RoleSpec { + label: "Efficient", + hint: "The efficient model takes over after the first edit.", + tier: Tier::Efficient, + path: &["efficient_target"], + }, + ], + settings: &[], + subagents: false, + }, + Algorithm { + kind: "auto", + title: "Recommended preset", + summary: "This stage router starts each request on the efficient model.", + roles: &[ + RoleSpec { + label: "Capable", + hint: "The capable model takes the hard steps.", + tier: Tier::Capable, + path: &["capable_target"], + }, + RoleSpec { + label: "Efficient", + hint: "The efficient model starts each request.", + tier: Tier::Efficient, + path: &["efficient_target"], + }, + ], + settings: &[], + subagents: false, + }, +]; + +/// COMMON_ROUTE_KEYS lists keys that every route type accepts. Switching types keeps these and removes +/// the rest, so settings of the old type cannot fail the new type's checks. +const COMMON_ROUTE_KEYS: [&str; 6] = [ + "id", + "type", + "context_window", + "tool_calling", + "reasoning", + "vision", +]; + +/// REQUEST_SETTINGS lists target settings that change the request body. The server keeps one +/// target per model on a client, so it rejects two targets that name the +/// same model on the same client with different values for these. +const REQUEST_SETTINGS: [&str; 3] = ["omit_body_fields", "reasoning_effort", "extra_body"]; + +/// TARGET_SETTINGS lists settings that a route takes on when it uses the target. +const TARGET_SETTINGS: [&str; 4] = [ + "system_prompt", + "reasoning_effort", + "extra_body", + "omit_body_fields", +]; + +/// Route describes one entry under `[routes]`. +#[derive(Clone, Debug, PartialEq)] +pub struct Route { + /// This field names the route's table in this file. Callers never send it. + pub key: String, + /// This field sets the public model ID that callers send. + pub id: String, + pub kind: String, +} + +/// Client describes one entry under `[llm_clients]`. +#[derive(Clone, Debug, PartialEq)] +pub struct Client { + pub name: String, + pub format: String, + pub base_url: String, + pub api_key_env: Option, + pub forward_auth: bool, +} + +impl Client { + /// This function returns the host in `base_url`, such as `chatgpt.com`. A port stays. + pub fn host(&self) -> &str { + let rest = self + .base_url + .split_once("://") + .map_or(self.base_url.as_str(), |(_, rest)| rest); + let authority = rest.split(['/', '?', '#']).next().unwrap_or(rest); + authority.rsplit('@').next().unwrap_or(authority) + } +} + +/// Choice identifies a model and the client that serves it. +#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Choice { + pub client: String, + pub model: String, +} + +/// Role describes a model the algorithm needs, such as the judge. +#[derive(Clone, Debug, PartialEq)] +pub struct Role { + pub label: String, + /// This field explains what the role does. + pub hint: &'static str, + pub tier: Option, + slot: Slot, +} + +/// Slot identifies where a route names a role's target. +#[derive(Clone, Debug, PartialEq)] +enum Slot { + /// Key identifies a key path, such as `stage.capable_target`. + Key(&'static [&'static str]), + /// Listed identifies an entry of a random route's `targets` list. + Listed(usize), +} + +impl Slot { + /// This function returns the slot's key path under the route, such as + /// `stage.capable_target`. + fn path(&self) -> String { + match self { + Self::Key(path) => path.join("."), + Self::Listed(_) => "targets".to_string(), + } + } +} + +/// Edited stores the result of [`ServerConfig::edit`]. +#[derive(Debug)] +pub struct Edited { + /// This field stores the new config text. + pub text: String, + /// This field stores user-facing notes about the edit. + pub notes: Vec, +} + +/// ServerConfig keeps the parsed config and the original text's layout. +#[derive(Clone, Debug)] +pub struct ServerConfig { + doc: DocumentMut, + /// This field maps tables inside generated blocks to each block's first marker. + generated: HashMap, +} + +impl ServerConfig { + pub fn parse(text: &str) -> Result { + text.parse::() + .map(|doc| Self { + doc, + generated: generated_tables(text), + }) + .map_err(|error| { + format!( + "Could not parse the server config. Fix this TOML error and try again:\n\ + {error}" + ) + }) + } + + pub fn routes(&self) -> Vec { + entries(self.table("routes")) + .map(|(key, route)| Route { + key: key.to_string(), + id: text(route, "id").unwrap_or(key).to_string(), + kind: text(route, "type").unwrap_or_default().to_string(), + }) + .collect() + } + + pub fn remove(&self, route: &str) -> Result { + let mut doc = self.doc.clone(); + let routes = doc + .get_mut("routes") + .and_then(Item::as_table_like_mut) + .ok_or("No routes are configured.")?; + let removed = routes + .remove(route) + .ok_or("The route was already removed. Refresh and choose it again.")?; + retain_block_markers(&mut doc, &removed); + Ok(Edited { + text: doc.to_string(), + notes: vec![ + "Removed the route. Its targets and endpoint settings are preserved.".into(), + ], + }) + } + + pub fn change_route( + &self, + operation: &str, + route: &str, + name: &str, + id: &str, + algorithm: Option<&Algorithm>, + choices: &[Choice], + ) -> Result { + if id.trim().is_empty() || id.contains(['\n', '\r']) { + return Err("Enter a nonempty public route ID without line breaks.".into()); + } + if self + .routes() + .iter() + .any(|existing| existing.id == id && !(operation == "rename" && existing.key == route)) + { + return Err("That public route ID is already configured.".into()); + } + let mut doc = self.doc.clone(); + if operation == "rename" { + let table = doc + .get_mut("routes") + .and_then(Item::as_table_like_mut) + .and_then(|routes| routes.get_mut(route)) + .and_then(Item::as_table_like_mut) + .ok_or("Choose an existing route.")?; + table.insert("id", toml_edit::value(id)); + return Ok(Edited { + text: doc.to_string(), + notes: vec![ + "Coding tools that select the old public route ID must choose the new ID." + .into(), + ], + }); + } + if name.is_empty() + || name.len() > 64 + || !name + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_')) + { + return Err("Route names need 1–64 letters, numbers, hyphens, or underscores.".into()); + } + if self.route(name).is_some() { + return Err("That route name is already configured.".into()); + } + let mut table = match operation { + "duplicate" => self + .table("routes") + .and_then(|routes| routes.get(route)) + .cloned() + .ok_or("Choose an existing route.")?, + "create" => { + let mut table = toml_edit::Table::new(); + table.insert("type", toml_edit::value("passthrough")); + table.insert("target", toml_edit::value("")); + Item::Table(table) + } + _ => return Err("Choose Create, Duplicate, or Rename.".into()), + }; + let mut copied = vec![&mut table]; + while let Some(item) = copied.pop() { + if let Some(table) = item.as_table_mut() { + table.set_position(Some(-1)); + if let Some(prefix) = table.decor().prefix().and_then(|prefix| prefix.as_str()) { + let prefix = prefix + .split_inclusive('\n') + .filter(|line| { + !line.trim().starts_with("# >>>") && !line.trim().starts_with("# <<<") + }) + .collect::(); + table.decor_mut().set_prefix(prefix); + } + copied.extend(table.iter_mut().map(|(_, item)| item)); + } + } + table + .as_table_like_mut() + .ok_or("Route must be a table.")? + .insert("id", toml_edit::value(id)); + if doc.get("routes").is_none() { + doc["routes"] = Item::Table(toml_edit::Table::new()); + } + doc["routes"] + .as_table_like_mut() + .ok_or("Routes must be a table.")? + .insert(name, table); + let candidate = ServerConfig::parse(&doc.to_string())?; + if operation == "create" { + candidate.edit( + name, + algorithm.ok_or("Choose a routing algorithm.")?, + choices, + ) + } else { + Ok(Edited{text:doc.to_string(),notes:vec!["The duplicate initially shares targets and endpoint settings with the source route.".into()]}) + } + } + + pub fn clients(&self) -> Vec { + entries(self.table("llm_clients")) + .map(|(name, client)| Client { + name: name.to_string(), + format: text(client, "format").unwrap_or_default().to_string(), + base_url: text(client, "base_url").unwrap_or_default().to_string(), + api_key_env: text(client, "api_key_env").map(str::to_string), + forward_auth: client + .get("forward_auth") + .and_then(Item::as_bool) + .unwrap_or(false), + }) + .collect() + } + + /// This function returns the text of the marker that opens the block another tool + /// writes around `[table.key]`, when the table sits inside one. That tool + /// overwrites the table the next time it runs. + pub fn generated_by(&self, table: &str, key: &str) -> Option<&str> { + self.generated + .get(&format!("{table}.{key}")) + .map(String::as_str) + } + + /// This function returns the model IDs that targets name on `client`, sorted, each once. + pub fn models_on(&self, client: &str) -> Vec { + let mut models: Vec = entries(self.table("targets")) + .filter(|(_, target)| text(*target, "llm_client") == Some(client)) + .filter_map(|(_, target)| text(target, "id").map(str::to_string)) + .collect(); + models.sort(); + models.dedup(); + models + } + + /// This function returns the algorithm a route uses now, or `None` when the picker + /// cannot show it, such as a custom-mode classifier whose groups are + /// free-form. + pub fn algorithm(&self, route: &str) -> Option<&'static Algorithm> { + let route = self.route(route)?; + if text(route, "mode") == Some("custom") { + return None; + } + let kind = text(route, "type")?; + ALGORITHMS.iter().find(|algorithm| algorithm.kind == kind) + } + + /// This function returns the roles that `algorithm` needs on `route`. A random route + /// keeps its number of targets, and has at least two. + pub fn roles(&self, route: &str, algorithm: &Algorithm) -> Vec { + if algorithm.kind != RANDOM { + return algorithm + .roles + .iter() + .map(|spec| Role { + label: spec.label.to_string(), + hint: spec.hint, + tier: Some(spec.tier), + slot: Slot::Key(spec.path), + }) + .collect(); + } + let listed = self + .route(route) + .filter(|route| text(*route, "type") == Some(RANDOM)) + .and_then(|route| { + route + .get("targets")? + .as_array() + .map(|targets| targets.len()) + }) + .unwrap_or(0); + (0..listed.max(2)) + .map(|index| Role { + label: format!("Model {}", index + 1), + hint: "This model gets a share of the requests.", + tier: [Tier::Capable, Tier::Efficient].get(index).copied(), + slot: Slot::Listed(index), + }) + .collect() + } + + /// This function returns the model that each role of the route's current algorithm + /// uses. A role whose target is missing gets an empty choice. + pub fn choices(&self, route: &str) -> Vec { + let Some(algorithm) = self.algorithm(route) else { + return Vec::new(); + }; + self.roles(route, algorithm) + .iter() + .map(|role| { + self.target_at(route, &role.slot) + .and_then(|name| self.target(name)) + .unwrap_or_default() + }) + .collect() + } + + /// This function returns the config text with `route` switched to `algorithm`, using + /// one choice per role, and notes about the edit. + /// + /// Each role keeps its target if it already names the selected model. + /// Otherwise, this function reuses a matching target with equivalent settings, + /// or with request settings that prevent the server from accepting a duplicate. + /// If neither exists, this function edits an unshared target or copies a shared one. + /// Roles earlier in this edit also count as users of a target. A changed or copied target keeps settings + /// such as `extra_body` and `omit_body_fields`, including settings meant + /// for the old model. Targets no route uses any more are left in the file. + pub fn edit( + &self, + route: &str, + algorithm: &Algorithm, + choices: &[Choice], + ) -> Result { + if self.route(route).is_none() { + return Err(format!("The config has no [routes.{route}] table.")); + } + let current = self + .algorithm(route) + .ok_or("This route must be edited in the server config file.")?; + let roles = if algorithm.kind == RANDOM { + if choices.len() < 2 { + return Err("Random routing needs at least two models.".into()); + } + (0..choices.len()) + .map(|index| Role { + label: format!("Model {}", index + 1), + hint: "This model gets a share of the requests.", + tier: [Tier::Capable, Tier::Efficient].get(index).copied(), + slot: Slot::Listed(index), + }) + .collect() + } else { + self.roles(route, algorithm) + }; + if roles.len() != choices.len() { + return Err(format!( + "{} needs {} models, not {}.", + algorithm.kind, + roles.len(), + choices.len() + )); + } + let clients = self.clients(); + for (role, choice) in roles.iter().zip(choices) { + if choice.model.trim().is_empty() { + return Err(format!("Pick a model for {}.", role.label)); + } + if choice.client.is_empty() { + return Err(format!("Pick an endpoint for {}.", role.label)); + } + if !clients.iter().any(|client| client.name == choice.client) { + return Err(format!( + "The config has no LLM client named \"{}\".", + choice.client + )); + } + } + + let same_type = current.kind == algorithm.kind; + let old_roles = self.roles(route, current); + // When the type changes, each new role starts from the old role with + // the same tier, so a composite's capable target stays the capable one. + let by_tier: Vec<(Option, &str)> = old_roles + .iter() + .filter_map(|role| Some((role.tier, self.target_at(route, &role.slot)?))) + .collect(); + let mut users = self.target_users(); + let place = self.new_target_position(); + + let mut doc = self.doc.clone(); + let mut names = Vec::with_capacity(roles.len()); + let mut notes = Vec::new(); + for (role, choice) in roles.iter().zip(choices) { + let existing = if same_type { + self.target_at(route, &role.slot) + } else { + role.tier.and_then(|tier| { + by_tier + .iter() + .find(|(old, _)| *old == Some(tier)) + .map(|(_, name)| *name) + }) + }; + let choice = Choice { + client: choice.client.clone(), + model: choice.model.trim().to_string(), + }; + let (name, moved_from) = + choose_target(&mut doc, &mut users, route, role, &choice, existing, place)?; + if let Some(old) = moved_from + && let Some(note) = kept_settings_note(&doc, &clients, &name, &old, &choice) + { + notes.push(note); + } + let mut others: Vec<&str> = users + .get(name.as_str()) + .into_iter() + .flatten() + .copied() + .filter(|other| *other != route) + .collect(); + others.dedup(); + if Some(name.as_str()) != existing && !others.is_empty() { + let plural = if others.len() == 1 { "" } else { "s" }; + notes.push(format!( + "{} now shares [targets.{name}] with route{plural} {}.", + role.label, + others.join(", ") + )); + } + names.push(name); + } + notes.extend(self.login_note(route, choices)); + notes.extend(self.generated_note(route, &names)); + + let table = doc + .get_mut("routes") + .and_then(Item::as_table_like_mut) + .and_then(|routes| routes.get_mut(route)) + .and_then(Item::as_table_like_mut) + .ok_or_else(|| format!("The config has no [routes.{route}] table."))?; + if !same_type { + let old_slots: Vec = old_roles.iter().map(|role| role.slot.path()).collect(); + let removed = reset_route(table, algorithm, &old_slots); + if !removed.is_empty() { + notes.push(format!( + "Switching to {} removed these settings from [routes.{route}]: {}. The \ + backup still has them.", + algorithm.kind, + removed.join(", ") + )); + } + } + if algorithm.kind == RANDOM { + table.insert( + "targets", + Item::Value(toml_edit::Value::Array(toml_edit::Array::new())), + ); + } + for (role, name) in roles.iter().zip(&names) { + set_slot(table, &role.slot, name); + } + if !same_type { + for (path, setting) in algorithm.settings { + set_at(table, path, setting.value()); + } + } + Ok(Edited { + text: doc.to_string(), + notes, + }) + } + + fn table(&self, name: &str) -> Option<&dyn TableLike> { + self.doc.get(name)?.as_table_like() + } + + fn route(&self, route: &str) -> Option<&dyn TableLike> { + self.table("routes")?.get(route)?.as_table_like() + } + + fn target(&self, name: &str) -> Option { + let target = self.table("targets")?.get(name)?.as_table_like()?; + Some(Choice { + client: text(target, "llm_client")?.to_string(), + model: text(target, "id")?.to_string(), + }) + } + + fn target_at(&self, route: &str, slot: &Slot) -> Option<&str> { + let route = self.route(route)?; + match slot { + Slot::Key(path) => { + let (last, parents) = path.split_last()?; + let mut table = route; + for key in parents { + table = table.get(key)?.as_table_like()?; + } + text(table, last) + } + Slot::Listed(index) => route.get("targets")?.as_array()?.get(*index)?.as_str(), + } + } + + /// Returns, for each target name, the routes that name it: one entry per + /// mention in any route, nested policies included. A target with one + /// entry belongs to one role. + fn target_users(&self) -> HashMap<&str, Vec<&str>> { + let mut users: HashMap<&str, Vec<&str>> = entries(self.table("targets")) + .map(|(name, _)| (name, Vec::new())) + .collect(); + for (route, item) in self + .table("routes") + .into_iter() + .flat_map(|routes| routes.iter()) + { + visit_strings(item, &mut |text| { + if let Some(routes) = users.get_mut(text) { + routes.push(route); + } + }); + } + users + } + + /// This function returns the file position for a new `[targets.*]` table. The table + /// goes after the last target outside every generated block, so that the + /// next rewrite of a block cannot delete it. When every target sits + /// inside a block, any other place is next to a generated target, so the + /// table goes before the first table of the file. + fn new_target_position(&self) -> Option { + self.table("targets")? + .iter() + .filter(|(key, _)| self.generated_by("targets", key).is_none()) + .filter_map(|(_, item)| item.as_table()?.position()) + .max() + .or_else(|| { + self.generated + .keys() + .any(|key| key.starts_with("targets.")) + .then_some(-1) + }) + } + + /// This function says so when the edit sends the callers' logins to another host. A + /// client with `forward_auth` sends each caller's own login, and a caller + /// that has none for the new host gets HTTP 401. + fn login_note(&self, route: &str, choices: &[Choice]) -> Option { + let clients = self.clients(); + let before = login_hosts(&clients, &self.choices(route)); + let after = login_hosts(&clients, choices); + if after.iter().all(|host| before.contains(host)) { + return None; + } + let was = if before.is_empty() { + String::new() + } else { + format!(" It sent them to {} before.", before.join(" and ")) + }; + Some(format!( + "[routes.{route}] now sends each caller's own login to {}.{was} A caller that has \ + no login for the new host gets HTTP 401.", + after.join(" and ") + )) + } + + /// This function returns a warning when the route sits inside a block that another tool + /// writes, which overwrites the route, or when the route uses targets + /// inside such a block, which that tool can change or remove. + fn generated_note(&self, route: &str, targets: &[String]) -> Option { + if let Some(block) = self.generated_by("routes", route) { + return Some(format!( + "[routes.{route}] sits inside a block that another tool writes. That tool \ + overwrites this change the next time it runs. Block marker: \"{block}\"." + )); + } + let inside: Vec = targets + .iter() + .filter(|name| self.generated_by("targets", name).is_some()) + .map(|name| format!("[targets.{name}]")) + .collect(); + let (verb, pronoun) = if inside.len() == 1 { + ("sits", "it") + } else { + ("sit", "them") + }; + (!inside.is_empty()).then(|| { + format!( + "[routes.{route}] uses {}, which {verb} inside a block that another tool \ + writes. The next time that tool runs, it can change or remove {pronoun}.", + inside.join(", ") + ) + }) + } +} + +/// This function picks or writes the target that serves `choice` for one role. Returns its +/// name, and the model that the target named before when the edit changed +/// the target in place or copied it. Reusing a target adds the route to the +/// target's users, so a later role in the same edit copies that target +/// instead of changing it in place. +/// A new target goes at file position `place`, where the next rewrite of a +/// generated block cannot delete it. +fn choose_target<'a>( + doc: &mut DocumentMut, + users: &mut HashMap<&'a str, Vec<&'a str>>, + route: &'a str, + role: &Role, + choice: &Choice, + existing: Option<&str>, + place: Option, +) -> Result<(String, Option), String> { + let targets = doc + .get_mut("targets") + .and_then(Item::as_table_like_mut) + .ok_or("The config has no [targets] table.")?; + + let current = existing + .and_then(|name| targets.get(name)) + .and_then(Item::as_table_like); + if let Some(name) = existing + && current.is_some_and(|target| names_choice(target, choice)) + { + return Ok((name.to_string(), None)); + } + let old = current.and_then(|target| { + Some(Choice { + client: text(target, "llm_client")?.to_string(), + model: text(target, "id")?.to_string(), + }) + }); + if let Some(name) = reusable(&*targets, current, choice).map(str::to_string) { + if let Some(routes) = users.get_mut(name.as_str()) { + routes.push(route); + } + return Ok((name, None)); + } + if let Some(name) = existing + && users.get(name).is_some_and(|routes| routes.len() == 1) + && let Some(target) = targets.get_mut(name).and_then(Item::as_table_like_mut) + { + set_value(target, "id", Value::from(choice.model.as_str())); + set_value(target, "llm_client", Value::from(choice.client.as_str())); + return Ok((name.to_string(), old)); + } + + let mut table = Table::new(); + if let Some(source) = existing + .and_then(|name| targets.get(name)) + .and_then(Item::as_table_like) + { + for (key, item) in source.iter() { + let mut item = item.clone(); + unplace(&mut item); + table.insert(key, item); + } + } + set_value(&mut table, "id", Value::from(choice.model.as_str())); + set_value( + &mut table, + "llm_client", + Value::from(choice.client.as_str()), + ); + let base = format!("{route}_{}", role.label.to_lowercase().replace(' ', "_")); + let mut name = base.clone(); + let mut suffix = 1; + while targets.contains_key(&name) { + suffix += 1; + name = format!("{base}_{suffix}"); + } + // A table with no position is written after the last [targets.*] table in + // the file, which can sit inside a block that another tool writes. + table.set_position(place); + targets.insert(&name, Item::Table(table)); + Ok((name, old)) +} + +/// This function returns a note about the request settings that a target kept when the +/// edit moved it from `old` to `new`, a model of another family or on a +/// client with another request format. A setting meant for the old model can +/// make the provider reject the new model's requests, and `--dry-run` does +/// not catch that. So the note lists the kept settings, or says that the +/// target has none, and the user decides. +fn kept_settings_note( + doc: &DocumentMut, + clients: &[Client], + name: &str, + old: &Choice, + new: &Choice, +) -> Option { + let format = |name: &str| { + clients + .iter() + .find(|client| client.name == name) + .map(|client| client.format.as_str()) + }; + if family(&old.model) == family(&new.model) && format(&old.client) == format(&new.client) { + return None; + } + let target = doc + .get("targets")? + .as_table_like()? + .get(name)? + .as_table_like()?; + let kept: Vec = REQUEST_SETTINGS + .iter() + .filter_map(|key| Some(format!("{key} = {}", setting(target, key)?))) + .collect(); + Some(if kept.is_empty() { + format!( + "[targets.{name}] now names {} and has no omit_body_fields, reasoning_effort, or \ + extra_body. Check whether the new model needs one of them.", + new.model + ) + } else { + format!( + "[targets.{name}] now names {} and kept {} from {}. Check that the new model \ + accepts these settings.", + new.model, + kept.join(", "), + old.model + ) + }) +} + +/// This function returns a model's family: the first word of the last part of its ID, +/// such as `gpt` for `openai/gpt-5.6-sol`. +fn family(model: &str) -> String { + let name = model.rsplit('/').next().unwrap_or(model); + name.split(['-', '.', '_', ':']) + .next() + .unwrap_or(name) + .to_lowercase() +} + +/// This function returns another target that names `choice` and that a role whose target +/// is `current` may use. The role takes on that target's settings. So it +/// uses a target with the same settings as `current`, or else a target whose +/// request settings differ from `current`, because the server would reject a +/// second target for this model with other request settings. +fn reusable<'t>( + targets: &'t dyn TableLike, + current: Option<&dyn TableLike>, + choice: &Choice, +) -> Option<&'t str> { + let same = |target: &dyn TableLike, keys: &[&str]| { + keys.iter() + .all(|key| setting(target, key) == current.and_then(|current| setting(current, key))) + }; + let candidates: Vec<(&str, &dyn TableLike)> = targets + .iter() + .filter_map(|(name, target)| Some((name, target.as_table_like()?))) + .filter(|(_, target)| names_choice(*target, choice)) + .collect(); + candidates + .iter() + .find(|(_, target)| same(*target, &TARGET_SETTINGS)) + .or_else(|| { + candidates + .iter() + .find(|(_, target)| !same(*target, &REQUEST_SETTINGS)) + }) + .map(|(name, _)| *name) +} + +/// This function returns a target's setting as plain data, so the same value written in +/// another layout compares equal. +fn setting(target: &dyn TableLike, key: &str) -> Option { + let mut table = Table::new(); + table.insert(key, target.get(key)?.clone()); + DocumentMut::from(table) + .to_string() + .parse::() + .ok()? + .remove(key) +} + +/// This function clears the file position of every table in `item`. A copied sub-table +/// then follows the header of the table it is copied into, not the header +/// of the table it came from. +fn unplace(item: &mut Item) { + if let Some(table) = item.as_table_mut() { + table.set_position(None); + for (_, child) in table.iter_mut() { + unplace(child); + } + } +} + +/// This function returns whether a target names exactly this model on this client. +fn names_choice(target: &dyn TableLike, choice: &Choice) -> bool { + text(target, "llm_client") == Some(choice.client.as_str()) + && text(target, "id") == Some(choice.model.as_str()) +} + +/// This function removes the old type's settings, sets the new `type`, and returns the +/// removed settings as key paths, such as `classifier.base_threshold`. The +/// list leaves out `old_slots`, the old roles' targets, which the new roles +/// replace. +fn reset_route( + route: &mut dyn TableLike, + algorithm: &Algorithm, + old_slots: &[String], +) -> Vec { + let kept = + |key: &str| COMMON_ROUTE_KEYS.contains(&key) || (algorithm.subagents && key == "subagents"); + let stale: Vec = route + .iter() + .map(|(key, _)| key) + .filter(|key| !kept(key)) + .map(str::to_string) + .collect(); + let mut removed = Vec::new(); + for key in stale { + if let Some(item) = route.remove(&key) { + key_paths(&key, &item, &mut removed); + } + } + removed.retain(|path| !old_slots.contains(path)); + set_value(route, "type", Value::from(algorithm.kind)); + removed +} + +/// This function adds the key path of every value in `item` to `paths`. +fn key_paths(path: &str, item: &Item, paths: &mut Vec) { + match item.as_table_like() { + Some(table) => { + for (key, child) in table.iter() { + key_paths(&format!("{path}.{key}"), child, paths); + } + } + None => paths.push(path.to_string()), + } +} + +fn set_slot(route: &mut dyn TableLike, slot: &Slot, name: &str) { + match slot { + Slot::Key(path) => set_at(route, path, Value::from(name)), + Slot::Listed(index) => { + if !route.get("targets").is_some_and(Item::is_array) { + route.insert("targets", toml_edit::value(toml_edit::Array::new())); + } + if let Some(targets) = route.get_mut("targets").and_then(Item::as_array_mut) { + if *index < targets.len() { + targets.replace(*index, name); + } else { + targets.push(name); + } + } + } + } +} + +/// This function sets a value at a key path, creating missing tables on the way. +fn set_at(table: &mut dyn TableLike, path: &[&str], value: Value) { + match path { + [] => {} + [key] => set_value(table, key, value), + [key, rest @ ..] => { + if !table.get(key).is_some_and(Item::is_table_like) { + table.insert(key, Item::Table(Table::new())); + } + if let Some(child) = table.get_mut(key).and_then(Item::as_table_like_mut) { + set_at(child, rest, value); + } + } + } +} + +/// This function replaces a value, keeping the spacing and trailing comment around it. +fn set_value(table: &mut dyn TableLike, key: &str, mut value: Value) { + if let Some(old) = table.get_mut(key).and_then(Item::as_value_mut) { + *value.decor_mut() = old.decor().clone(); + *old = value; + } else { + table.insert(key, Item::Value(value)); + } +} + +fn entries(table: Option<&dyn TableLike>) -> impl Iterator { + table + .into_iter() + .flat_map(|table| table.iter()) + .filter_map(|(key, item)| Some((key, item.as_table_like()?))) +} + +fn text<'a>(table: &'a dyn TableLike, key: &str) -> Option<&'a str> { + table.get(key)?.as_str() +} + +fn visit_strings(item: &Item, visit: &mut dyn FnMut(&str)) { + match item { + Item::Value(value) => visit_value(value, visit), + Item::Table(table) => table + .iter() + .for_each(|(_, item)| visit_strings(item, visit)), + Item::ArrayOfTables(tables) => tables + .iter() + .flat_map(Table::iter) + .for_each(|(_, item)| visit_strings(item, visit)), + Item::None => {} + } +} + +fn visit_value(value: &Value, visit: &mut dyn FnMut(&str)) { + match value { + Value::String(text) => visit(text.value()), + Value::Array(array) => array.iter().for_each(|value| visit_value(value, visit)), + Value::InlineTable(table) => table + .iter() + .for_each(|(_, value)| visit_value(value, visit)), + _ => {} + } +} + +/// This function returns the hosts that get the callers' own logins: the hosts of the +/// `forward_auth` clients that `choices` name, sorted, each once. +fn login_hosts<'c>(clients: &'c [Client], choices: &[Choice]) -> Vec<&'c str> { + let mut hosts: Vec<&str> = choices + .iter() + .filter_map(|choice| clients.iter().find(|client| client.name == choice.client)) + .filter(|client| client.forward_auth) + .map(Client::host) + .collect(); + hosts.sort_unstable(); + hosts.dedup(); + hosts +} + +// Generated-block markers can occur on nested route tables and still apply to retained targets. +fn retain_block_markers(doc: &mut DocumentMut, removed: &Item) { + let mut markers = Vec::new(); + let mut tables: Vec<_> = removed.as_table().into_iter().collect(); + while let Some(table) = tables.pop() { + if let Some(position) = table.position() + && let Some(prefix) = table.decor().prefix().and_then(|value| value.as_str()) + && prefix + .lines() + .any(|line| line.trim().starts_with("# >>>") || line.trim().starts_with("# <<<")) + { + markers.push((position, prefix.to_string())); + } + tables.extend(table.iter().filter_map(|(_, item)| item.as_table())); + } + markers.sort_by_key(|(position, _)| *position); + let mut positions = Vec::new(); + let mut tables = vec![doc.as_table()]; + while let Some(table) = tables.pop() { + positions.extend(table.position()); + tables.extend(table.iter().filter_map(|(_, item)| item.as_table())); + } + let mut prefixes: HashMap = HashMap::new(); + let mut trailing = String::new(); + for (removed_position, prefix) in markers { + if let Some(next) = positions + .iter() + .copied() + .filter(|position| *position > removed_position) + .min() + { + prefixes.entry(next).or_default().push_str(&prefix); + } else { + trailing.push_str(&prefix); + } + } + let mut tables = vec![doc.as_table_mut()]; + while let Some(table) = tables.pop() { + if let Some(position) = table.position() + && let Some(mut prefix) = prefixes.remove(&position) + { + prefix.push_str( + table + .decor() + .prefix() + .and_then(|value| value.as_str()) + .unwrap_or_default(), + ); + table.decor_mut().set_prefix(prefix); + } + tables.extend(table.iter_mut().filter_map(|(_, item)| item.as_table_mut())); + } + trailing.push_str(doc.trailing().as_str().unwrap_or_default()); + doc.set_trailing(trailing); +} + +/// This function finds the route and target tables that sit inside blocks another tool +/// writes. A block starts at a line that begins with `# >>>` and ends at the +/// next line that begins with `# <<<`, the markers that the installer scripts +/// use. Each table maps to the text of its block's first marker, with the `#` +/// and the `>>>` removed. +fn generated_tables(text: &str) -> HashMap { + let mut tables = HashMap::new(); + let mut block: Option = None; + for line in text.lines().map(str::trim) { + if line.starts_with("# >>>") { + block = Some( + line.trim_matches(|c: char| c == '#' || c == '>' || c.is_whitespace()) + .to_string(), + ); + } else if line.starts_with("# <<<") { + block = None; + } else if let Some(block) = &block + && let Some(header) = line.strip_prefix('[') + && let Some((table, rest)) = header.split_once('.') + && matches!(table, "routes" | "targets") + && let Some(key) = table_key(rest) + { + tables + .entry(format!("{table}.{key}")) + .or_insert_with(|| block.clone()); + } + } + tables +} + +/// This function returns the first key of a table header's dotted path, bare or in double +/// quotes: `name` for `name.child]`, and `a.b` for `"a.b"]`. +fn table_key(path: &str) -> Option<&str> { + if let Some(quoted) = path.strip_prefix('"') { + return quoted.split_once('"').map(|(key, _)| key); + } + path.split(['.', ']']).next().filter(|key| !key.is_empty()) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// This fixture uses a GPT judge and Claude on one gateway with different API formats. + const GATEWAY: &str = r#"schema_version = 1 + +[llm_clients.gateway] +format = "openai_responses" +base_url = "https://gateway.example.com/v1" +forward_auth = true + +# Claude caches prompts on the chat endpoint. +[llm_clients.gateway_chat] +format = "openai_chat" +base_url = "https://gateway.example.com/v1" +forward_auth = true + +# The gateway rejects any thinking setting for these models. +[targets.gateway_capable] +id = "claude-opus-5-5" +llm_client = "gateway_chat" # chat, for caching +omit_body_fields = ["reasoning_effort"] + +[targets.gateway_efficient] +id = "claude-sonnet-5" +llm_client = "gateway_chat" +omit_body_fields = ["reasoning_effort"] + +[targets.gateway_judge] +id = "gpt-5.6-terra" +llm_client = "gateway" + +[routes.gateway] +id = "switchyard-gateway" +type = "composite" +context_window = 200000 + +[routes.gateway.classifier] +target = "gateway_judge" +base_threshold = 0.5 +classify_trigger = "user_turn" +message_hash_fallback = true + +[routes.gateway.stage] +capable_target = "gateway_capable" +efficient_target = "gateway_efficient" +confidence_threshold = 0.5 +"#; + + fn choice(client: &str, model: &str) -> Choice { + Choice { + client: client.to_string(), + model: model.to_string(), + } + } + + fn algorithm(kind: &str) -> &'static Algorithm { + ALGORITHMS + .iter() + .find(|algorithm| algorithm.kind == kind) + .expect("known algorithm") + } + + /// This function edits the config and checks the result with the server's parser. + fn edit(text: &str, route: &str, kind: &str, choices: &[Choice]) -> Edited { + let edited = ServerConfig::parse(text) + .expect("parse") + .edit(route, algorithm(kind), choices) + .expect("edit"); + if let Err(error) = switchyard_runner::Runner::from_toml(&edited.text) { + panic!( + "the server rejects the edited config: {error}\n{}", + edited.text + ); + } + edited + } + + fn capable_target(config: &ServerConfig) -> Option<&str> { + config.target_at("gateway", &Slot::Key(&["stage", "capable_target"])) + } + + #[test] + fn removing_a_route_keeps_generated_block_markers_for_retained_targets() { + for (suffix, target, owned) in [ + ( + "# >>> generated >>>\n[routes.generated]\nid='generated'\ntype='passthrough'\ntarget='owned'\n[targets.owned]\nid='m'\nllm_client='gateway'\n# <<< generated <<<\n", + "owned", + true, + ), + ( + "# >>> generated >>>\n[targets.owned]\nid='m'\nllm_client='gateway'\n# <<< generated <<<\n[routes.generated]\nid='generated'\ntype='passthrough'\ntarget='owned'\n[targets.unowned]\nid='other'\nllm_client='gateway'\n", + "unowned", + false, + ), + ( + "[routes.generated]\nid='generated'\ntype='composite'\n# >>> generated >>>\n[routes.generated.classifier]\ntarget='owned'\n[targets.owned]\nid='m'\nllm_client='gateway'\n# <<< generated <<<\n", + "owned", + true, + ), + ] { + let config = ServerConfig::parse(&format!("{GATEWAY}\n{suffix}")).expect("config"); + assert_eq!(config.generated_by("targets", target).is_some(), owned); + let edited = config.remove("generated").expect("remove"); + let remaining = ServerConfig::parse(&edited.text).expect("config"); + assert_eq!( + remaining.generated_by("targets", target).is_some(), + owned, + "{}", + edited.text + ); + } + } + + #[test] + // Routes can share targets, so deleting one route must leave those targets usable. + fn removing_a_route_preserves_shared_targets_and_other_routes() { + let original = format!( + "{GATEWAY}\n[routes.keep]\nid='keep'\ntype='passthrough'\ntarget='gateway_capable'\n" + ); + let config = ServerConfig::parse(&original).expect("config"); + let edited = config.remove("gateway").expect("remove"); + let remaining = ServerConfig::parse(&edited.text).expect("config"); + assert_eq!(remaining.routes().len(), 1); + assert_eq!(remaining.routes()[0].id, "keep"); + assert_eq!(remaining.models_on("gateway"), config.models_on("gateway")); + switchyard_runner::Runner::from_toml(&edited.text).expect("server accepts remaining route"); + assert!(config.remove("missing").is_err()); + } + + #[test] + fn route_tools_preserve_targets_and_reject_conflicting_names() { + let config = ServerConfig::parse(GATEWAY).expect("config"); + let duplicate = config + .change_route("duplicate", "gateway", "copy", "copy-public", None, &[]) + .expect("duplicate"); + let copied = ServerConfig::parse(&duplicate.text).expect("copy"); + let managed_text = GATEWAY + .replace("[routes.gateway]", "# >>> generated >>>\n[routes.gateway]") + + "\n# <<< generated <<<\n"; + let managed = ServerConfig::parse(&managed_text).expect("managed source"); + let durable = managed + .change_route( + "duplicate", + "gateway", + "durable", + "durable-public", + None, + &[], + ) + .expect("durable duplicate"); + let durable = ServerConfig::parse(&durable.text).expect("durable config"); + assert!(durable.generated_by("routes", "durable").is_none()); + assert!(durable.generated_by("routes", "gateway").is_some()); + + assert_eq!(copied.choices("copy"), config.choices("gateway")); + switchyard_runner::Runner::from_toml(&duplicate.text).expect("server accepts duplicate"); + let rename = copied + .change_route("rename", "copy", "", "renamed-public", None, &[]) + .expect("rename"); + let renamed = ServerConfig::parse(&rename.text).expect("rename"); + assert_eq!(renamed.choices("copy"), config.choices("gateway")); + assert!( + copied + .change_route("rename", "copy", "", "switchyard-gateway", None, &[]) + .is_err() + ); + for name in ["", "../bad", "a/b", "a\nb"] { + assert!( + config + .change_route("duplicate", "gateway", name, "new", None, &[]) + .is_err() + ); + } + let algorithm = ALGORITHMS + .iter() + .find(|algorithm| algorithm.kind == "passthrough") + .expect("algorithm"); + let created = config + .change_route( + "create", + "", + "new", + "new-public", + Some(algorithm), + &[choice("gateway", "manual-model")], + ) + .expect("create"); + switchyard_runner::Runner::from_toml(&created.text).expect("server accepts created route"); + let random = ALGORITHMS + .iter() + .find(|algorithm| algorithm.kind == RANDOM) + .expect("random"); + let choices = vec![ + choice("gateway", "a"), + choice("gateway", "b"), + choice("gateway", "c"), + choice("gateway", "d"), + ]; + let expanded = config.edit("gateway", random, &choices).expect("expand"); + let expanded = ServerConfig::parse(&expanded.text).expect("expanded"); + assert_eq!(expanded.choices("gateway"), choices); + let shrunk = expanded + .edit("gateway", random, &choices[..2]) + .expect("shrink"); + let shrunk = ServerConfig::parse(&shrunk.text).expect("shrunk"); + assert_eq!(shrunk.choices("gateway"), choices[..2]); + assert!(expanded.edit("gateway", random, &choices[..1]).is_err()); + } + + #[test] + fn every_algorithm_writes_a_config_the_server_accepts() { + let gateway = ServerConfig::parse(GATEWAY).expect("parse"); + let models = [ + choice("gateway", "gpt-5.6-terra"), + choice("gateway", "gpt-5.6-sol"), + choice("gateway_chat", "claude-sonnet-5"), + ]; + for algorithm in ALGORITHMS { + let roles = gateway.roles("gateway", algorithm).len(); + + let edited = edit(GATEWAY, "gateway", algorithm.kind, &models[3 - roles..]); + + let config = ServerConfig::parse(&edited.text).expect("parse"); + assert_eq!( + config.algorithm("gateway").map(|a| a.kind), + Some(algorithm.kind) + ); + assert_eq!(config.choices("gateway"), models[3 - roles..]); + } + } + + #[test] + fn a_target_one_role_reuses_is_not_changed_for_another_role() { + // Capable moves to the efficient model, and Efficient moves to a new model. + let chosen = [ + choice("gateway", "gpt-5.6-terra"), + choice("gateway_chat", "claude-sonnet-5"), + choice("gateway", "gpt-5.6-luna"), + ]; + + let edited = edit(GATEWAY, "gateway", "composite", &chosen); + + assert_eq!( + ServerConfig::parse(&edited.text) + .expect("parse") + .choices("gateway"), + chosen + ); + } + + #[test] + fn changing_the_algorithm_replaces_only_the_route_settings() { + let edited = edit( + GATEWAY, + "gateway", + "stage_router", + &[ + choice("gateway_chat", "claude-opus-5-5"), + choice("gateway_chat", "claude-sonnet-5"), + ], + ); + + let config = ServerConfig::parse(&edited.text).expect("parse"); + assert_eq!( + config.algorithm("gateway").map(|a| a.kind), + Some("stage_router") + ); + let route = config.route("gateway").expect("route"); + assert_eq!(text(route, "id"), Some("switchyard-gateway")); + assert_eq!( + route.get("context_window").and_then(Item::as_integer), + Some(200000) + ); + assert_eq!(text(route, "capable_target"), Some("gateway_capable")); + assert_eq!(text(route, "efficient_target"), Some("gateway_efficient")); + assert_eq!(text(route, "picker"), Some("efficient_first")); + assert!(route.get("classifier").is_none() && route.get("stage").is_none()); + assert!( + edited.text.contains("[targets.gateway_judge]"), + "unused targets stay in the file" + ); + let removed = edited.notes.join("\n"); + assert!( + removed.contains("classifier.message_hash_fallback") + && !removed.contains("stage.capable_target"), + "the result lists the removed settings, not the replaced targets: {removed}" + ); + } + + #[test] + fn a_target_another_route_uses_is_copied_not_changed() { + // The shared target has a sub-table, which the copy must keep under + // its own header. + let shared = GATEWAY.replace( + "omit_body_fields = [\"reasoning_effort\"]\n\n[targets.gateway_efficient]", + "omit_body_fields = [\"reasoning_effort\"]\n\n[targets.gateway_capable.extra_body]\n\ + top_k = 5\n\n[targets.gateway_efficient]", + ) + "\n[routes.direct]\nid = \"direct\"\ntype = \"passthrough\"\ntarget = \"gateway_capable\"\n"; + + let edited = edit( + &shared, + "gateway", + "composite", + &[ + choice("gateway", "gpt-5.6-terra"), + choice("gateway", "gpt-5.6-sol"), + choice("gateway_chat", "claude-sonnet-5"), + ], + ); + + let config = ServerConfig::parse(&edited.text).expect("parse"); + assert_eq!( + config.choices("direct"), + [choice("gateway_chat", "claude-opus-5-5")] + ); + assert_eq!( + config.choices("gateway")[1], + choice("gateway", "gpt-5.6-sol") + ); + let copy = capable_target(&config).expect("capable target"); + assert_ne!(copy, "gateway_capable"); + let targets = config.table("targets").expect("targets"); + let (source, copied) = ( + targets.get("gateway_capable").and_then(Item::as_table_like), + targets.get(copy).and_then(Item::as_table_like), + ); + for key in TARGET_SETTINGS { + assert_eq!( + source.and_then(|target| setting(target, key)), + copied.and_then(|target| setting(target, key)), + "the copy keeps {key}" + ); + } + let header = |name: &str| edited.text.find(&format!("[{name}]")).expect(name); + assert!( + header(&format!("targets.{copy}")) < header(&format!("targets.{copy}.extra_body")), + "the copied sub-table follows the copy's header:\n{}", + edited.text + ); + } + + #[test] + fn uses_another_routes_target_only_when_the_route_keeps_its_settings_or_must_share() { + // `prompted` names the model that Capable moves to, and has a + // system prompt that the gateway route does not have. + for (omit, shared) in [ + // Same request settings: the server accepts a second target for + // the model, so Capable keeps its own target. + ("omit_body_fields = [\"reasoning_effort\"]\n", false), + // Other request settings: the server would reject a second + // target, so Capable shares `prompted`, and the result says so. + ("", true), + ] { + let text = format!( + "{GATEWAY}\n[targets.prompted]\nid = \"gpt-5.6-sol\"\nllm_client = \"gateway\"\n\ + system_prompt = \"You answer for route other.\"\n{omit}\n[routes.other]\n\ + id = \"other\"\ntype = \"passthrough\"\ntarget = \"prompted\"\n" + ); + + let edited = edit( + &text, + "gateway", + "composite", + &[ + choice("gateway", "gpt-5.6-terra"), + choice("gateway", "gpt-5.6-sol"), + choice("gateway_chat", "claude-sonnet-5"), + ], + ); + + let config = ServerConfig::parse(&edited.text).expect("parse"); + assert_eq!( + config.choices("gateway")[1], + choice("gateway", "gpt-5.6-sol") + ); + assert_eq!( + capable_target(&config) == Some("prompted"), + shared, + "{omit}" + ); + assert_eq!( + edited + .notes + .iter() + .any(|note| note.contains("prompted") && note.contains("other")), + shared, + "{:?}", + edited.notes + ); + } + } + + /// This fixture places the last target and route inside a generated block. + /// A hand-written route shares the capable target. + fn with_generated_block() -> String { + format!( + "{GATEWAY}\n[routes.direct]\nid = \"direct\"\ntype = \"passthrough\"\n\ + target = \"gateway_capable\"\n\n\ + # >>> sync: generated by sync.py; edits between the markers are overwritten >>>\n\n\ + [targets.generated]\nid = \"gpt-5.5\"\nllm_client = \"gateway\"\n\n\ + [routes.generated]\nid = \"generated\"\ntype = \"passthrough\"\n\ + target = \"generated\"\n\n# <<< sync <<<\n" + ) + } + + #[test] + fn new_targets_stay_outside_generated_blocks() { + let all_generated = GATEWAY.replace( + "# The gateway rejects", + "# >>> sync: generated by sync.py; edits between the markers are overwritten >>>\n# The gateway rejects", + ).replace("[routes.gateway]\n", "# <<< sync <<<\n\n[routes.gateway]\n") + + "\n[routes.direct]\nid='direct'\ntype='passthrough'\ntarget='gateway_capable'\n"; + for text in [with_generated_block(), all_generated] { + let edited = edit( + &text, + "gateway", + "composite", + &[ + choice("gateway", "gpt-5.6-terra"), + choice("gateway", "gpt-5.6-sol"), + choice("gateway_chat", "claude-sonnet-5"), + ], + ); + let config = ServerConfig::parse(&edited.text).expect("parse"); + let copy = capable_target(&config).expect("capable target"); + assert_ne!(copy, "gateway_capable"); + assert_eq!(config.generated_by("targets", copy), None); + let header = edited + .text + .find(&format!("[targets.{copy}]")) + .expect("target header"); + let marker = edited.text.find("# >>> sync").expect("block marker"); + assert!(header < marker); + } + } +} diff --git a/crates/switchyard-desktop/src/sessions.rs b/crates/switchyard-desktop/src/sessions.rs new file mode 100644 index 0000000000..e35d50464b --- /dev/null +++ b/crates/switchyard-desktop/src/sessions.rs @@ -0,0 +1,686 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! This module starts CLI sessions with separate worktrees and model settings. + +use crate::harness::{self, Harness}; +use std::path::Path; + +// PendingSession attempts cleanup until open accepts the launch request. +// A successful launch keeps the worktree and settings for the coding session. +struct PendingSession { + project: std::path::PathBuf, + worktree: std::path::PathBuf, + private: std::path::PathBuf, + branch: String, + opened: bool, +} + +impl Drop for PendingSession { + fn drop(&mut self) { + if self.opened { + return; + } + let _ = std::process::Command::new("git") + .arg("-C") + .arg(&self.project) + .args(["worktree", "remove"]) + .arg(&self.worktree) + .output(); + let _ = std::process::Command::new("git") + .arg("-C") + .arg(&self.project) + .args(["branch", "-d", &self.branch]) + .output(); + let _ = std::fs::remove_dir_all(&self.private); + } +} + +pub(crate) fn quote(text: &str) -> String { + format!("'{}'", text.replace('\'', "'\\''")) +} + +pub fn preview(project: &Path) -> Result { + if project.as_os_str().is_empty() { + return Err("Choose a Git checkout directory.".into()); + } + let project = std::fs::canonicalize(crate::config::expand_home(project)) + .map_err(|error| format!("Open project: {error}"))?; + let git = |args: &[&str]| -> Result { + let output = std::process::Command::new("git") + .arg("-C") + .arg(&project) + .args(args) + .output() + .map_err(|error| error.to_string())?; + if !output.status.success() { + return Err("Choose a Git checkout with a committed HEAD.".into()); + } + Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) + }; + let repository = git(&["rev-parse", "--show-toplevel"])?; + let repository_path = std::fs::canonicalize(&repository).map_err(|error| error.to_string())?; + let relative = project + .strip_prefix(&repository_path) + .map_err(|error| error.to_string())?; + Ok( + serde_json::json!({"repository":repository,"project":project,"relative_directory":relative,"revision":git(&["rev-parse","HEAD"])? ,"branch":git(&["branch","--show-current"])? ,"dirty":!git(&["status","--porcelain"])? .is_empty(),"notice":"The session starts from committed HEAD. Uncommitted changes stay in the original checkout."}), + ) +} + +fn session_root() -> std::path::PathBuf { + crate::config::expand_home(Path::new("~/.switchyard/worktrees")) +} + +pub fn inventory() -> Result, String> { + let root = session_root(); + let entries = match std::fs::read_dir(&root) { + Ok(entries) => entries, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()), + Err(error) => return Err(format!("Read session inventory: {error}")), + }; + let mut sessions = Vec::new(); + for entry in entries { + let entry = entry.map_err(|error| error.to_string())?; + if entry + .file_type() + .map_err(|error| error.to_string())? + .is_dir() + && entry.file_name().to_string_lossy().ends_with("-settings") + { + let receipt = entry.path().join("session.json"); + if receipt.exists() { + let mut value: serde_json::Value = serde_json::from_slice( + &std::fs::read(&receipt).map_err(|error| error.to_string())?, + ) + .map_err(|error| format!("Read {}: {error}", receipt.display()))?; + value + .as_object_mut() + .ok_or("The session receipt does not match this worktree.")? + .insert("process_status".into(), serde_json::json!("unknown")); + sessions.push(value); + } + } + } + sessions.sort_by(|left, right| { + right["created_at"] + .as_str() + .cmp(&left["created_at"].as_str()) + }); + Ok(sessions) +} + +pub fn managed_session(id: &str) -> Result { + let suffix = id.strip_prefix("sy-").ok_or("Choose a managed session.")?; + uuid::Uuid::parse_str(suffix).map_err(|_| "Choose a managed session.")?; + let root = session_root(); + let private = root.join(format!("{id}-settings")); + let worktree = root.join(id); + for path in [&root, &private, &worktree] { + if std::fs::symlink_metadata(path) + .map_err(|error| error.to_string())? + .file_type() + .is_symlink() + { + return Err("Session directories must not be symlinks.".into()); + } + } + let receipt: serde_json::Value = serde_json::from_slice( + &std::fs::read(private.join("session.json")).map_err(|error| error.to_string())?, + ) + .map_err(|error| error.to_string())?; + if receipt["id"] != id || receipt["worktree"] != worktree.display().to_string() { + return Err("The session receipt does not match this worktree.".into()); + } + Ok(receipt) +} + +pub fn cleanup_preview(id: &str) -> Result { + let mut receipt = managed_session(id)?; + let worktree = receipt["worktree"] + .as_str() + .ok_or("Missing session worktree.")? + .to_string(); + let run = |args: &[&str]| -> Result { + let output = std::process::Command::new("git") + .args(["-C", &worktree]) + .args(args) + .output() + .map_err(|error| error.to_string())?; + if !output.status.success() { + return Err(String::from_utf8_lossy(&output.stderr).into_owned()); + } + Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) + }; + receipt["dirty"] = serde_json::json!(!run(&["status", "--porcelain"])?.is_empty()); + receipt["current_revision"] = serde_json::json!(run(&["rev-parse", "HEAD"])?); + receipt["current_branch"] = serde_json::json!(run(&["branch", "--show-current"])?); + receipt["cleanup_notice"] = serde_json::json!( + "Cleanup removes a clean worktree and its unchanged session branch. Recorded usage stays in the routing log. Unknown private files are preserved." + ); + Ok(receipt) +} + +pub fn cleanup(id: &str, reviewed: &serde_json::Value) -> Result { + let current = cleanup_preview(id)?; + if ¤t != reviewed { + return Err("The session changed since review. Review cleanup again.".into()); + } + if current["dirty"] != false + || current["current_revision"] != current["revision"] + || current["current_branch"] != id + { + return Err("Save or move the session’s work before cleanup. Cleanup requires a clean worktree with its original commit and branch.".into()); + } + let repository = current["repository"] + .as_str() + .ok_or("Missing session repository.")?; + let worktree = current["worktree"] + .as_str() + .ok_or("Missing session worktree.")?; + let output = std::process::Command::new("git") + .args(["-C", repository, "worktree", "remove", worktree]) + .output() + .map_err(|error| error.to_string())?; + if !output.status.success() { + return Err(String::from_utf8_lossy(&output.stderr).into_owned()); + } + let output = std::process::Command::new("git") + .args(["-C", repository, "branch", "-d", id]) + .output() + .map_err(|error| error.to_string())?; + if !output.status.success() { + return Ok("Removed the clean session worktree. Git kept the branch; inspect it before deleting it.".into()); + } + let private = session_root().join(format!("{id}-settings")); + // Cleanup removes only the receipt because settings can contain later user edits. + std::fs::remove_file(private.join("session.json")).map_err(|error| error.to_string())?; + let _ = std::fs::remove_dir(&private); + Ok("Removed the clean session worktree and branch. Retained private settings and recorded usage.".into()) +} + +pub fn launch( + tool: Harness, + project: &Path, + model: &str, + root: &str, + login: bool, + account: Option<&Path>, + revision: &str, +) -> Result { + let root = harness::validate(tool, root, model, login)?; + if let Some(account) = account { + crate::accounts::validate(tool, account)?; + } + if tool == Harness::CodexApp { + return Err("Choose Codex CLI to launch a worktree session. Codex app uses its own workspace controls.".into()); + } + let binary = harness::binary(tool).ok_or("Install the selected coding tool first.")?; + if project.as_os_str().is_empty() { + return Err("Choose a Git checkout directory.".into()); + } + let project = crate::config::expand_home(project); + let project = std::fs::canonicalize(&project).map_err(|e| format!("Open project: {e}"))?; + let output = std::process::Command::new("git") + .arg("-C") + .arg(&project) + .args(["rev-parse", "--show-toplevel"]) + .output() + .map_err(|e| e.to_string())?; + if !output.status.success() { + return Err("Choose a Git checkout directory.".into()); + } + let repository = Path::new( + std::str::from_utf8(&output.stdout) + .map_err(|error| error.to_string())? + .trim(), + ) + .to_path_buf(); + let repository = std::fs::canonicalize(repository).map_err(|error| error.to_string())?; + let relative = project + .strip_prefix(&repository) + .map_err(|error| error.to_string())? + .to_path_buf(); + let id = format!("sy-{}", uuid::Uuid::now_v7()); + let worktree = crate::config::expand_home(Path::new("~/.switchyard/worktrees")).join(&id); + std::fs::create_dir_all( + worktree + .parent() + .ok_or("Worktree needs a parent directory")?, + ) + .map_err(|e| e.to_string())?; + let output = std::process::Command::new("git") + .arg("-C") + .arg(&project) + .args(["worktree", "add", "-b"]) + .arg(&id) + .arg(&worktree) + .arg(revision) + .output() + .map_err(|e| e.to_string())?; + if !output.status.success() { + return Err(format!( + "Could not create worktree: {}", + String::from_utf8_lossy(&output.stderr) + )); + } + let private = worktree + .parent() + .ok_or("Worktree needs a parent directory")? + .join(format!("{id}-settings")); + let mut pending = PendingSession { + project, + worktree: worktree.clone(), + private: private.clone(), + branch: id.clone(), + opened: false, + }; + let files = match tool { + Harness::Pi => vec![private.join("models.json"), private.join("settings.json")], + _ => vec![private.join("settings.json")], + }; + if matches!(tool, Harness::Claude | Harness::Pi) { + std::fs::create_dir_all(&private).map_err(|e| e.to_string())?; + harness::install(tool, &files, &root, model, login)?; + } + std::fs::create_dir_all(&private).map_err(|error| error.to_string())?; + let receipt = serde_json::json!({"id":id,"tool":tool,"route":model,"repository":repository,"revision":preview(&worktree)?["revision"],"worktree":worktree,"branch":id,"created_at":chrono::Utc::now().to_rfc3339(),"settings_directory":private,"account_directory":account}); + std::fs::write( + private.join("session.json"), + serde_json::to_vec_pretty(&receipt).map_err(|error| error.to_string())?, + ) + .map_err(|error| error.to_string())?; + #[cfg(windows)] + { + let mut environment = Vec::new(); + let mut remove_environment = Vec::new(); + let args = match tool { + Harness::CodexCli => vec![ + "-c".into(), + format!("model={}", toml_edit::Value::from(model)), + "-c".into(), + "model_provider=\"sy\"".into(), + "-c".into(), + format!( + "model_providers.sy={{name=\"Switchyard\",base_url={},wire_api=\"responses\",requires_openai_auth={login},http_headers={{\"x-switchyard-session-id\"={},\"x-switchyard-origin\"=\"codex\"}}}}", + toml_edit::Value::from(format!("{}/v1", root.trim_end_matches('/'))), + toml_edit::Value::from(id.as_str()) + ), + ], + Harness::Claude => { + environment.push(( + "ANTHROPIC_CUSTOM_HEADERS".into(), + format!("x-switchyard-session-id: {id}\nx-switchyard-origin: claude"), + )); + if login { + remove_environment.extend( + [ + "ANTHROPIC_AUTH_TOKEN", + "ANTHROPIC_API_KEY", + "CLAUDE_CODE_OAUTH_TOKEN", + ] + .map(String::from), + ); + } + vec![ + "--settings".into(), + files[0].display().to_string(), + "--model".into(), + model.into(), + ] + } + Harness::Pi => { + environment.push(("PI_CODING_AGENT_DIR".into(), private.display().to_string())); + vec![ + "--provider".into(), + "switchyard".into(), + "--model".into(), + model.into(), + ] + } + Harness::CodexApp => return Err("Choose a CLI tool.".into()), + }; + let default_files = harness::paths(tool); + let account = account.or_else(|| match tool { + Harness::Claude if std::env::var_os("CLAUDE_CONFIG_DIR").is_none() => None, + _ => default_files[0].parent(), + }); + if let Some(path) = account { + match tool { + Harness::CodexCli => { + environment.push(("CODEX_HOME".into(), path.display().to_string())) + } + Harness::Claude => { + environment.push(("CLAUDE_CONFIG_DIR".into(), path.display().to_string())) + } + _ => {} + } + } + crate::windows::open_terminal(crate::windows::TerminalJob { + binary, + args, + directory: Some(worktree.join(&relative)), + environment, + remove_environment, + })?; + } + #[cfg(not(windows))] + { + let program = quote(&binary.display().to_string()); + let command = match tool { + Harness::CodexCli => format!( + "{program} -c {} -c {} -c {}", + quote(&format!("model={}", toml_edit::Value::from(model))), + quote("model_provider=\"sy\""), + quote(&format!( + "model_providers.sy={{name=\"Switchyard\",base_url={},wire_api=\"responses\",requires_openai_auth={login},http_headers={{\"x-switchyard-session-id\"={},\"x-switchyard-origin\"=\"codex\"}}}}", + toml_edit::Value::from(format!("{}/v1", root.trim_end_matches('/'))), + toml_edit::Value::from(id.as_str()) + )) + ), + Harness::Claude => format!( + "env ANTHROPIC_CUSTOM_HEADERS={} {program} --settings {} --model {}", + quote(&format!( + "x-switchyard-session-id: {id}\nx-switchyard-origin: claude" + )), + quote(&files[0].display().to_string()), + quote(model) + ), + Harness::Pi => format!( + "env PI_CODING_AGENT_DIR={} {program} --provider switchyard --model {}", + quote(&private.display().to_string()), + quote(model) + ), + Harness::CodexApp => return Err("Choose a CLI tool.".into()), + }; + let default_files = harness::paths(tool); + let account = account.or_else(|| match tool { + Harness::Claude if std::env::var_os("CLAUDE_CONFIG_DIR").is_none() => None, + _ => default_files[0].parent(), + }); + let account_env = match (tool, account) { + (Harness::CodexCli, Some(path)) => { + format!("export CODEX_HOME={}\n", quote(&path.display().to_string())) + } + (Harness::Claude, Some(path)) => format!( + "export CLAUDE_CONFIG_DIR={}\n", + quote(&path.display().to_string()) + ), + _ => String::new(), + }; + let subscription_env = if tool == Harness::Claude && login { + "unset ANTHROPIC_AUTH_TOKEN ANTHROPIC_API_KEY CLAUDE_CODE_OAUTH_TOKEN\n" + } else { + "" + }; + let script = format!( + "#!/bin/bash\nexport PATH=\"$HOME/.cargo/bin:/opt/homebrew/bin:/usr/local/bin:$PATH\"\nrm -- \"$0\"\ncd {} || exit 1\n{account_env}{subscription_env}{command}", + quote(&worktree.join(relative).display().to_string()) + ); + crate::server::open_terminal_script("switchyard-session-", &script)?; + } + pending.opened = true; + Ok(format!( + "Opened Terminal to start the session.\nWorktree: {}\nBranch: {id}\nModel route: {model}\nRemove the worktree with git worktree remove after saving or discarding its changes.", + worktree.display() + )) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn failed_session_setup_removes_its_worktree_branch_and_private_settings() { + let dir = tempfile::tempdir().expect("directory"); + let project = dir.path().join("project"); + std::fs::create_dir(&project).expect("project"); + let git = |args: &[&str]| { + let output = std::process::Command::new("git") + .arg("-C") + .arg(&project) + .args(args) + .output() + .expect("git"); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + output + }; + git(&["init"]); + git(&[ + "-c", + "user.name=Fixture", + "-c", + "user.email=fixture@example.invalid", + "-c", + "commit.gpgsign=false", + "commit", + "--allow-empty", + "-m", + "fixture", + ]); + let worktree = dir.path().join("worktree"); + let private = dir.path().join("settings"); + git(&[ + "worktree", + "add", + "-b", + "session-fixture", + worktree.to_str().expect("path"), + ]); + std::fs::create_dir(&private).expect("private"); + std::fs::write(private.join("settings.json"), "{}").expect("settings"); + drop(PendingSession { + project: project.clone(), + worktree: worktree.clone(), + private: private.clone(), + branch: "session-fixture".into(), + opened: false, + }); + assert!(!worktree.exists()); + assert!(!private.exists()); + assert!( + !String::from_utf8_lossy(&git(&["branch", "--list", "session-fixture"]).stdout) + .contains("session-fixture") + ); + } + // The subprocess isolates HOME and PATH. + // The Terminal stub injects a conflicting directory when the script must select one. + // Claude's default case requires an unset CLAUDE_CONFIG_DIR, not an explicit ~/.claude. + #[cfg(unix)] + #[test] + fn generated_sessions_use_explicit_accounts_without_changing_user_defaults() { + use std::os::unix::fs::PermissionsExt; + if let Ok(fixture) = std::env::var("SWITCHYARD_SESSION_FIXTURE") { + let fixture = std::path::PathBuf::from(fixture); + let tool = if std::env::var("SWITCHYARD_SESSION_TOOL").expect("tool") == "codex" { + Harness::CodexCli + } else { + Harness::Claude + }; + let account = + std::env::var_os("SWITCHYARD_SESSION_ACCOUNT").map(std::path::PathBuf::from); + let revision = preview(&fixture.join("project")).expect("preview")["revision"] + .as_str() + .expect("revision") + .to_string(); + // Advancing the original checkout makes a launch from HEAD differ from the review. + if tool == Harness::CodexCli && account.is_none() { + let committed = std::process::Command::new("git") + .arg("-C") + .arg(fixture.join("project")) + .args([ + "-c", + "user.name=Fixture", + "-c", + "user.email=fixture@example.invalid", + "-c", + "commit.gpgsign=false", + "commit", + "--allow-empty", + "-m", + "advanced", + ]) + .output() + .expect("advance HEAD"); + assert!(committed.status.success()); + } + launch( + tool, + &fixture.join("project"), + "route'$(literal)", + "http://localhost:4123", + tool == Harness::Claude, + account.as_deref(), + &revision, + ) + .expect("launch"); + if tool == Harness::CodexCli && account.is_none() { + let records = inventory().expect("inventory"); + assert_eq!(records.len(), 1); + assert_eq!(records[0]["revision"], revision); + assert_ne!( + preview(&fixture.join("project")).expect("advanced project")["revision"], + revision + ); + let id = records[0]["id"].as_str().expect("session id"); + let receipt = session_root().join(format!("{id}-settings/session.json")); + let original = std::fs::read(&receipt).expect("receipt"); + // Valid JSON roots must still be objects before inventory adds process_status. + for malformed in ["null", "[]", "false", "42"] { + std::fs::write(&receipt, malformed).expect("malformed receipt"); + assert!(inventory().is_err()); + assert!(cleanup_preview(id).is_err()); + } + std::fs::write(&receipt, original).expect("restore receipt"); + let clean = cleanup_preview(id).expect("preview cleanup"); + let worktree = Path::new(records[0]["worktree"].as_str().expect("worktree")); + let unsaved = worktree.join("unsaved-user-work.txt"); + std::fs::write(&unsaved, "keep this work").expect("user work"); + assert!(cleanup(id, &clean).is_err()); + assert!(unsaved.exists()); + let dirty = cleanup_preview(id).expect("dirty preview"); + assert!(cleanup(id, &dirty).is_err()); + assert!(worktree.exists()); + std::fs::remove_file(&unsaved).expect("save fixture work"); + let clean = cleanup_preview(id).expect("clean preview"); + cleanup(id, &clean).expect("safe cleanup"); + assert!(!worktree.exists()); + assert!(inventory().expect("inventory after cleanup").is_empty()); + } + return; + } + for (name, named, inherited_dir) in [ + ("codex", false, true), + ("codex", true, true), + ("claude", false, true), + ("claude", true, true), + ("claude", false, false), + ("claude", true, false), + ] { + let fixture = tempfile::tempdir().expect("fixture"); + let home = fixture.path().join("home"); + let bin = fixture.path().join("bin"); + let project = fixture.path().join("project"); + for path in [&home, &bin, &project] { + std::fs::create_dir(path).expect("directory"); + } + for args in [ + vec!["init"], + vec![ + "-c", + "user.name=Fixture", + "-c", + "user.email=fixture@example.invalid", + "-c", + "commit.gpgsign=false", + "commit", + "--allow-empty", + "-m", + "fixture", + ], + ] { + assert!( + std::process::Command::new("git") + .arg("-C") + .arg(&project) + .args(args) + .output() + .expect("git") + .status + .success() + ); + } + let default = home.join("default-login"); + std::fs::create_dir(&default).expect("default"); + std::fs::write(default.join("settings.json"), "{\"user\":true}").expect("settings"); + let account = home.join(".switchyard/accounts").join(name).join("named"); + if named { + std::fs::create_dir_all(&account).expect("account"); + } + let output = fixture.path().join("arguments"); + let captured = fixture.path().join("session.command"); + let open = bin.join(if cfg!(target_os = "linux") { + "xdg-open" + } else { + "open" + }); + let terminal_claude = if inherited_dir || named { + "export CLAUDE_CONFIG_DIR=wrong-terminal-login" + } else { + "unset CLAUDE_CONFIG_DIR" + }; + std::fs::write(&open, format!("#!/bin/bash\ncp -- \"$1\" {}\nexport CODEX_HOME=wrong-terminal-login ANTHROPIC_AUTH_TOKEN=wrong-token\n{terminal_claude}\nbash \"$1\"\n",quote(&captured.display().to_string()))).expect("open"); + let tool = bin.join(name); + std::fs::write(&tool, format!("#!/bin/bash\nprintf '%s\\n' \"${{CODEX_HOME:-}}\" \"${{CLAUDE_CONFIG_DIR:-}}\" \"${{ANTHROPIC_AUTH_TOKEN:-}}\" \"$@\" > {}\n", quote(&output.display().to_string()))).expect("tool"); + for path in [&open, &tool] { + std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700)) + .expect("executable"); + } + let mut child = std::process::Command::new(std::env::current_exe().expect("runner")); + child.args(["--exact", "sessions::tests::generated_sessions_use_explicit_accounts_without_changing_user_defaults"]) + .env("SWITCHYARD_SESSION_FIXTURE", fixture.path()).env("SWITCHYARD_SESSION_TOOL", name) + .env("HOME", &home).env("CODEX_HOME", &default).env("CLAUDE_CONFIG_DIR", &default) + .env("PATH", format!("{}:/usr/bin:/bin", bin.display())); + if named { + child.env("SWITCHYARD_SESSION_ACCOUNT", &account); + } + if !inherited_dir { + child.env_remove("CLAUDE_CONFIG_DIR"); + } + let result = child.output().expect("child"); + assert!( + result.status.success(), + "{}\n{}", + String::from_utf8_lossy(&result.stdout), + String::from_utf8_lossy(&result.stderr) + ); + let arguments = std::fs::read_to_string(output).expect("arguments"); + let lines: Vec<_> = arguments.lines().collect(); + let expected = if named { + account.to_str().expect("path") + } else if inherited_dir { + default.to_str().expect("path") + } else { + "" + }; + assert_eq!(lines[usize::from(name == "claude")], expected); + if name == "claude" { + assert_eq!(lines[2], ""); + } + assert!(arguments.contains("route'$(literal)")); + let script = std::fs::read_to_string(captured).expect("script"); + assert!(script.contains("x-switchyard-session-id")); + if name == "claude" && !named && !inherited_dir { + assert!(!script.contains("export CLAUDE_CONFIG_DIR=")); + } + assert_eq!( + std::fs::read_to_string(default.join("settings.json")).expect("settings"), + "{\"user\":true}" + ); + } + } +} diff --git a/crates/switchyard-desktop/src/summary.rs b/crates/switchyard-desktop/src/summary.rs new file mode 100644 index 0000000000..db479977e0 --- /dev/null +++ b/crates/switchyard-desktop/src/summary.rs @@ -0,0 +1,179 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! This module turns totals and prices into usage summary rows. + +use crate::config::Config; +use crate::health::ServerStatus; +use crate::pricing::{Savings, estimate, unpriced}; +use crate::rollup::{Totals, Usage}; + +/// MAX_MODEL_ROWS limits the number of models shown in the usage summary. +const MAX_MODEL_ROWS: usize = 5; + +/// This function builds the menu body for the current day and trailing week. +pub fn build(status: ServerStatus, usage: &Usage, config: &Config) -> Vec { + let state = match status { + ServerStatus::Running => "running", + ServerStatus::Stopped => "not responding", + }; + let mut rows = vec![ + format!("Server: {state} · {}", config.server_url), + String::new(), + ]; + + if usage.week.is_empty() { + rows.push("No requests recorded yet".into()); + return rows; + } + + rows.extend(period("Today", &usage.today, config)); + rows.push(String::new()); + rows.extend(period("This week", &usage.week, config)); + + let models = model_shares(&usage.week); + if !models.is_empty() { + rows.push(String::new()); + rows.push("This week by model".into()); + rows.extend(models); + } + + if config.prices.is_empty() { + rows.push(String::new()); + rows.push("Add prices to desktop.toml to see savings".into()); + } else { + let seen = usage + .week + .routed + .keys() + .chain(usage.week.classifier.keys()) + .map(String::as_str); + let baseline = std::iter::once(config.baseline_model.as_str()); + let missing = unpriced(seen.chain(baseline), &config.prices); + if !missing.is_empty() { + rows.push(String::new()); + rows.push(format!( + "Savings hidden for this week: desktop.toml has no price for {}", + listed(&missing) + )); + } + } + rows +} + +/// This function names at most three models, so that the menu row stays short. +fn listed(models: &[&str]) -> String { + match models { + [first, second, third, rest @ ..] if !rest.is_empty() => { + format!("{first}, {second}, {third}, and {} more", rest.len()) + } + _ => models.join(", "), + } +} + +fn period(name: &str, totals: &Totals, config: &Config) -> Vec { + let requests = totals.requests(); + let mut rows = vec![format!( + "{name} — {requests} request{} · {} tokens", + if requests == 1 { "" } else { "s" }, + tokens(totals.tokens()) + )]; + if let Some(savings) = estimate(totals, &config.prices, &config.baseline_model) { + rows.push(format!(" Saved {}", saved(&savings))); + } + rows +} + +fn saved(savings: &Savings) -> String { + let amount = money(savings.saved()); + match savings.percent() { + Some(percent) => format!("{amount} ({percent:.0}% of {})", money(savings.baseline)), + None => amount, + } +} + +fn model_shares(week: &Totals) -> Vec { + let total: u64 = week.routed.values().map(|tokens| tokens.total()).sum(); + if total == 0 { + return Vec::new(); + } + let mut models: Vec<_> = week + .routed + .iter() + .map(|(model, tokens)| (model, tokens.total())) + .collect(); + // Models appear by decreasing share; the model ID breaks ties for a stable order. + models.sort_by(|left, right| right.1.cmp(&left.1).then_with(|| left.0.cmp(right.0))); + models + .into_iter() + .take(MAX_MODEL_ROWS) + .map(|(model, count)| format!(" {model} — {:.0}%", count as f64 / total as f64 * 100.0)) + .collect() +} + +/// This function formats a token count compactly, since exact totals are not useful here. +fn tokens(value: u64) -> String { + match value { + 0..=9_999 => value.to_string(), + 10_000..=999_999 => format!("{:.0}K", value as f64 / 1_000.0), + 1_000_000..=999_999_999 => format!("{:.1}M", value as f64 / 1_000_000.0), + _ => format!("{:.1}B", value as f64 / 1_000_000_000.0), + } +} + +/// This function formats dollars, keeping the sign readable when routing cost more than it saved. +fn money(value: f64) -> String { + if value < 0.0 { + format!("-${:.2}", -value) + } else { + format!("${value:.2}") + } +} + +/// The tray ranks models by recorded tokens and combines models below the top three. +#[cfg(any(target_os = "macos", windows, test))] +pub fn tray_models(totals: &Totals) -> Vec { + let mut models = totals.routed.clone(); + for (model, overhead) in &totals.classifier { + let counts = models.entry(model.clone()).or_default(); + counts.requests += overhead.requests; + counts.input += overhead.input; + counts.cached_input += overhead.cached_input; + counts.output += overhead.output; + } + let mut models: Vec<_> = models.into_iter().collect(); + models.sort_by(|left, right| { + right + .1 + .total() + .cmp(&left.1.total()) + .then_with(|| left.0.cmp(&right.0)) + }); + if models.is_empty() { + return vec!["No model calls recorded".into()]; + } + let mut rows: Vec<_> = models + .iter() + .take(3) + .map(|(model, counts)| { + format!( + "{model} — {} tokens · calls: {}", + tokens(counts.total()), + counts.requests + ) + }) + .collect(); + if models.len() > 3 { + let remaining: u64 = models + .iter() + .skip(3) + .map(|(_, counts)| counts.total()) + .sum(); + rows.push(format!( + "{} more models — {} tokens", + models.len() - 3, + tokens(remaining) + )); + } + rows +} diff --git a/crates/switchyard-desktop/src/tui.rs b/crates/switchyard-desktop/src/tui.rs new file mode 100644 index 0000000000..b7ddc010cb --- /dev/null +++ b/crates/switchyard-desktop/src/tui.rs @@ -0,0 +1,1217 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Ratatui renders snapshots and submits typed operations to the shared controller. + +use crate::{ + controller::{Action, Controller}, + harness::Harness, + server_config::Choice, +}; +use ratatui::{ + crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}, + layout::{Constraint, Layout}, + style::{Color, Style}, + widgets::{Block, Paragraph, Sparkline, Tabs, Wrap}, +}; +use serde_json::Value; +use std::{ + sync::mpsc, + thread, + time::{Duration, Instant}, +}; +const PAGES: &[&str] = &[ + "Overview", + "Routes", + "Coding tools", + "Usage", + "Sessions", + "Settings", +]; +#[derive(Clone)] +struct Form { + title: String, + fields: Vec<(String, String)>, + index: usize, + kind: FormKind, + cursor: usize, + page_scroll: u16, +} +impl Form { + fn byte_cursor(&self) -> usize { + self.fields[self.index] + .1 + .char_indices() + .nth(self.cursor) + .map_or(self.fields[self.index].1.len(), |(index, _)| index) + } + fn insert(&mut self, text: &str) { + let text = text.replace(['\r', '\n'], ""); + let index = self.byte_cursor(); + self.fields[self.index].1.insert_str(index, &text); + self.cursor += text.chars().count(); + } + fn edit(&mut self, key: KeyEvent) { + match key.code { + KeyCode::Tab | KeyCode::BackTab => { + self.index = if key.code == KeyCode::Tab { + (self.index + 1) % self.fields.len() + } else { + (self.index + self.fields.len() - 1) % self.fields.len() + }; + self.cursor = self.fields[self.index].1.chars().count(); + } + KeyCode::Left => self.cursor = self.cursor.saturating_sub(1), + KeyCode::Right => { + self.cursor = (self.cursor + 1).min(self.fields[self.index].1.chars().count()) + } + KeyCode::Home => self.cursor = 0, + KeyCode::End => self.cursor = self.fields[self.index].1.chars().count(), + KeyCode::Backspace if self.cursor > 0 => { + self.cursor -= 1; + let index = self.byte_cursor(); + self.fields[self.index].1.remove(index); + } + KeyCode::Delete if self.cursor < self.fields[self.index].1.chars().count() => { + let index = self.byte_cursor(); + self.fields[self.index].1.remove(index); + } + KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => { + self.fields[self.index].1.clear(); + self.cursor = 0; + } + KeyCode::Char(c) + if !key + .modifiers + .intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) => + { + self.insert(&c.to_string()) + } + _ => {} + } + } +} +#[derive(Clone)] +enum FormKind { + Algorithm, + Apply(String, Option), + Project, + Account, + Models, + Key, + Filter, + RouteFilter, + SettingsFile, + Remove, + Restore, + Update, + ConfirmApply(Box), + ConfirmLaunch(Box), +} +#[derive(Clone)] +struct View { + snapshot: Value, + page: usize, + route: usize, + tool: usize, + account: usize, + session: usize, + filter: String, + route_filter: String, + settings_file: String, + preview: Option, + scroll: u16, + message: String, + form: Option
, + details: String, + showing_details: bool, + selected_session: Option, + missing_account: Option, +} +impl View { + fn routes(&self) -> &[Value] { + self.snapshot["routes"] + .as_array() + .map_or(&[], Vec::as_slice) + } + fn tools(&self) -> &[Value] { + self.snapshot["tools"].as_array().map_or(&[], Vec::as_slice) + } + fn matches_route(&self, route: &Value) -> bool { + let kind = string(route, "kind"); + let title = crate::server_config::ALGORITHMS + .iter() + .find(|algorithm| algorithm.kind == kind) + .map_or("", |algorithm| algorithm.title); + format!("{} {title}", string(route, "label")) + .to_lowercase() + .contains(&self.route_filter.to_lowercase()) + } + fn route(&self) -> Result<&Value, String> { + if self.page == 1 + && self + .routes() + .get(self.route) + .is_some_and(|route| !self.matches_route(route)) + { + return Err("No route matches the search. Clear or change the search first.".into()); + } + self.routes() + .get(self.route) + .ok_or_else(|| "Choose a route first.".into()) + } + fn tool(&self) -> Harness { + serde_json::from_value(self.tools()[self.tool]["tool"].clone()).unwrap_or(Harness::CodexCli) + } + fn account(&self) -> Option { + if self.missing_account.is_some() { + return self.missing_account.clone(); + } + self.tools().get(self.tool)?["accounts"] + .as_array() + .and_then(|a| self.account.checked_sub(1).and_then(|i| a.get(i))) + .and_then(Value::as_str) + .map(str::to_string) + } + fn dispatch(&mut self, controller: &mut Controller, action: Action) { + match controller.dispatch(action) { + Ok(reply) => { + self.message = reply.message; + self.details = reply.data["details"] + .as_array() + .map(|details| { + details + .iter() + .filter_map(Value::as_str) + .collect::>() + .join("\n") + }) + .unwrap_or_default(); + if let Err(e) = self.refresh(controller) { + self.message = e; + } + } + Err(e) => self.message = e, + } + } + fn refresh(&mut self, controller: &mut Controller) -> Result<(), String> { + let route = self.routes().get(self.route).map(|r| string(r, "key")); + let tool = self.tools().get(self.tool).map(|t| string(t, "tool")); + let account = self.account(); + self.snapshot = controller.snapshot()?; + if let Some(route) = route { + self.route = self + .routes() + .iter() + .position(|r| string(r, "key") == route) + .unwrap_or(usize::MAX); + } + if let Some(tool) = tool { + self.tool = self + .tools() + .iter() + .position(|t| string(t, "tool") == tool) + .unwrap_or(0); + } + if let Some(account) = account { + self.account = self + .tools() + .get(self.tool) + .and_then(|t| t["accounts"].as_array()) + .and_then(|a| a.iter().position(|v| v.as_str() == Some(&account))) + .map_or(usize::MAX, |i| i + 1); + self.missing_account = (self.account == usize::MAX).then_some(account); + } + if let Some(session) = &self.selected_session { + self.session = self.snapshot["sessions"] + .as_array() + .and_then(|a| a.iter().position(|v| v.as_str() == Some(session))) + .map_or(usize::MAX, |i| i + 1); + } + Ok(()) + } + fn preview_install(&mut self, controller: &mut Controller) -> Result<(), String> { + self.preview = None; + let route = self.route()?; + let reply = controller.dispatch(Action::PreviewInstall { + tool: self.tool(), + account: self.account(), + settings_file: self.custom_file(), + route: string(route, "key"), + id: string(route, "id"), + })?; + self.preview = Some(reply.data); + self.message = + "Review the diff. Press i to install; changing the selection clears the preview." + .into(); + Ok(()) + } + fn custom_file(&self) -> Option { + (!self.settings_file.is_empty()).then(|| self.settings_file.clone()) + } + fn move_route(&mut self, forward: bool) { + let visible: Vec<_> = self + .routes() + .iter() + .enumerate() + .filter(|(_, r)| self.page != 1 || self.matches_route(r)) + .map(|(i, _)| i) + .collect(); + if visible.is_empty() { + return; + } + let index = visible.iter().position(|i| *i == self.route).unwrap_or(0); + let next = if forward { + (index + 1) % visible.len() + } else { + index.saturating_sub(1) + }; + self.route = visible[next]; + self.preview = None; + } + fn form(&mut self, title: &str, kind: FormKind, fields: Vec<(String, String)>) { + let cursor = fields.first().map_or(0, |f| f.1.chars().count()); + self.form = Some(Form { + title: title.into(), + fields, + index: 0, + kind, + cursor, + page_scroll: self.scroll, + }); + self.scroll = 0; + } + fn submit(&mut self, controller: &mut Controller) -> Result<(), String> { + let retained = self.form.clone().ok_or("No form is open.")?; + let result = self.submit_form(controller); + if result.is_err() { + self.form = Some(retained); + } else if self.form.is_none() { + self.scroll = retained.page_scroll; + } + result + } + fn submit_form(&mut self, controller: &mut Controller) -> Result<(), String> { + let form = self.form.take().ok_or("No form is open.")?; + let value = |i: usize| form.fields[i].1.trim().to_string(); + let action = match form.kind { + FormKind::Algorithm => { + let route = self.route()?; + let algorithm = value(0); + let reply = controller.dispatch(Action::Editor { + generation: self.snapshot["generation"].as_u64().unwrap_or_default(), + route: string(route, "key"), + algorithm: algorithm.clone(), + })?; + let roles = reply.data["roles"].as_array().ok_or("No roles returned.")?; + let choices: Vec = + serde_json::from_value(route["choices"].clone()).map_err(|e| e.to_string())?; + let mut fields = Vec::new(); + for (i, role) in roles.iter().enumerate() { + // A role keeps its model by tier when the new algorithm changes role order. + let matching = route["tiers"].as_array().and_then(|tiers| { + role["tier"] + .as_str() + .and_then(|tier| tiers.iter().position(|t| t.as_str() == Some(tier))) + }); + let choice = matching + .and_then(|index| choices.get(index)) + .or_else(|| { + (algorithm == string(route, "kind")) + .then(|| choices.get(i)) + .flatten() + }) + .or_else(|| choices.first()) + .cloned() + .unwrap_or_default(); + fields.push((format!("{} endpoint", string(role, "label")), choice.client)); + fields.push(( + if string(role, "label") == "Model" { + "Model".into() + } else { + format!("{} model", string(role, "label")) + }, + choice.model, + )); + } + self.form( + "Route models · Enter applies", + FormKind::Apply(algorithm, route["revision"].as_str().map(str::to_owned)), + fields, + ); + return Ok(()); + } + FormKind::Apply(algorithm, revision) => { + let choices = form + .fields + .chunks(2) + .map(|f| Choice { + client: f[0].1.trim().into(), + model: f[1].1.trim().into(), + }) + .collect(); + let action = Action::Apply { + generation: self.snapshot["generation"].as_u64().unwrap_or_default(), + route: string(self.route()?, "key"), + algorithm, + choices, + revision: revision.clone(), + }; + let preview_action = match &action { + Action::Apply { + generation, + route, + algorithm, + choices, + .. + } => Action::PreviewRoute { + generation: *generation, + route: route.clone(), + algorithm: algorithm.clone(), + choices: choices.clone(), + revision, + }, + _ => unreachable!(), + }; + let reply = controller.dispatch(preview_action)?; + self.preview = Some(reply.data); + self.form( + "Review route changes · type SAVE to apply", + FormKind::ConfirmApply(Box::new(action)), + vec![("Confirmation".into(), String::new())], + ); + return Ok(()); + } + FormKind::ConfirmApply(action) => { + if value(0) != "SAVE" { + return Err("Type SAVE to apply the reviewed changes.".into()); + } + *action + } + FormKind::ConfirmLaunch(action) => { + if value(0) != "LAUNCH" { + return Err("Type LAUNCH to open the reviewed session.".into()); + } + *action + } + FormKind::Update => { + if value(0) != "UPDATE" { + return Err("Type UPDATE to start the reviewed source update.".into()); + } + Action::Update { + preview_token: self + .preview + .as_ref() + .and_then(|p| p["preview_token"].as_str()) + .map(str::to_owned), + } + } + FormKind::Project => { + let r = self.route()?; + let reply = controller.dispatch(Action::PreviewLaunch { + tool: self.tool(), + account: self.account(), + route: string(r, "key"), + id: string(r, "id"), + project: value(0), + })?; + let action = Action::Launch { + preview_token: reply.data["preview_token"].as_str().map(str::to_owned), + tool: self.tool(), + account: self.account(), + route: string(r, "key"), + id: string(r, "id"), + project: value(0), + }; + self.preview = Some(reply.data); + self.form( + "Review committed HEAD session · type LAUNCH", + FormKind::ConfirmLaunch(Box::new(action)), + vec![("Confirmation".into(), String::new())], + ); + return Ok(()); + } + FormKind::Account => Action::AddAccount { + tool: self.tool(), + name: value(0), + }, + FormKind::Models | FormKind::Key => { + let key = if matches!(form.kind, FormKind::Key) { + Some(value(1)) + } else { + None + }; + let reply = controller.dispatch(Action::Models { + client: value(0), + refresh: true, + key, + })?; + self.message = reply.message; + self.details = reply.data["models"] + .as_array() + .map(|m| { + m.iter() + .filter_map(Value::as_str) + .collect::>() + .join("\n") + }) + .unwrap_or_default(); + self.showing_details = true; + self.scroll = 0; + return Ok(()); + } + FormKind::RouteFilter => { + self.route_filter = value(0); + if !self + .routes() + .get(self.route) + .is_some_and(|r| self.matches_route(r)) + { + self.route = self + .routes() + .iter() + .position(|r| self.matches_route(r)) + .unwrap_or(0); + } + self.preview = None; + self.scroll = 0; + return Ok(()); + } + FormKind::SettingsFile => { + self.settings_file = value(0); + self.account = 0; + self.preview = None; + self.preview_install(controller)?; + return Ok(()); + } + FormKind::Restore => { + if value(0) != "RESTORE" { + return Err("Confirmation did not match. Nothing was restored.".into()); + } + Action::Restore { + tool: self.tool(), + account: self.account(), + settings_file: self.custom_file(), + preview_token: self + .preview + .as_ref() + .and_then(|p| p["preview_token"].as_str()) + .map(str::to_owned), + } + } + FormKind::Remove => { + let route = self.route()?; + if value(0) != string(route, "id") { + return Err("Route name did not match. Nothing was deleted.".into()); + } + Action::Remove { + generation: self.snapshot["generation"].as_u64().unwrap_or_default(), + route: string(route, "key"), + id: string(route, "id"), + revision: self + .preview + .as_ref() + .and_then(|p| p["revision"].as_str()) + .map(str::to_owned), + } + } + FormKind::Filter => { + self.filter = value(0); + self.scroll = 0; + return Ok(()); + } + }; + let reply = controller.dispatch(action)?; + self.message = reply.message; + self.details = reply.data["details"] + .as_array() + .map(|d| { + d.iter() + .filter_map(Value::as_str) + .collect::>() + .join("\n") + }) + .unwrap_or_default(); + self.refresh(controller)?; + Ok(()) + } + fn body(&self) -> String { + let errors = self.snapshot["errors"] + .as_array() + .map(|a| { + a.iter() + .filter_map(Value::as_str) + .collect::>() + .join("\n") + }) + .unwrap_or_default(); + let body = if self.showing_details { + format!("{}\n\n{}", self.message, self.details) + } else { + self.page_body() + }; + if errors.is_empty() { + body + } else { + format!("DATA ERRORS\n{errors}\n\n{body}") + } + } + fn page_body(&self) -> String { + match self.page { + 0 if self.snapshot["data_state"]["usage"]["state"] == "unavailable" => { + format!("CONNECTION\n{} · {}", if self.snapshot["metrics"]["running"] == true {"Connected"} else {"Not responding"}, string(&self.snapshot["metrics"], "server_url")) + } + 3 if self.snapshot["data_state"]["history"]["state"] == "unavailable" => String::new(), + 0=>{ + let mut period=""; + let details=self.snapshot["summary"].as_array().map(|rows|rows.iter().filter_map(Value::as_str).filter_map(|line| { + if line.starts_with("Today —") {period="Today";} + if line.starts_with("This week —") {period="Past 7 days";} + if line.contains("Saved") {Some(format!("{period}: {}",line.trim()))} + else if line.contains("prices")||line.contains("Savings hidden")||line.trim().ends_with('%') {Some(line.trim().into())} + else {None} + }).collect::>().join("\n")).unwrap_or_default(); + format!("CONNECTION\n{} · {}\n\nTODAY\n{} model calls · {} tokens\n\nPAST 7 DAYS\n{} model calls · {} tokens\n\nUSAGE DETAILS\n{}",if self.snapshot["metrics"]["running"]==true {"Connected"} else {"Not responding"},string(&self.snapshot["metrics"],"server_url"),self.snapshot["metrics"]["today"]["requests"],self.snapshot["metrics"]["today"]["tokens"],self.snapshot["metrics"]["week"]["requests"],self.snapshot["metrics"]["week"]["tokens"],details) + }, + 1=>{ + let visible:Vec<_>=self.routes().iter().enumerate().filter(|(_,r)|self.matches_route(r)).collect(); + let index=visible.iter().position(|(i,_)|*i==self.route).unwrap_or(0); + let mut rows=vec![format!("/ Search routes e Edit d Delete route m Refresh models k Save endpoint key\n{} of {} routes · Search: {}\n",visible.len(),self.routes().len(),self.route_filter)]; + for(i,r) in visible.iter().skip(index.saturating_sub(4)).take(10){rows.push(format!("{} {}",if *i==self.route {">"}else{" "},string(r,"label")));} + if visible.is_empty(){ rows.push("No routes match your search.".into()); } + if let Ok(route)=self.route(){rows.push(format!("\nSELECTED ROUTE\n{}",string(route,"label")));} + rows.join("\n") + } + 2|4=>{ + let mut body=format!("t Next tool r Next route a Next account\n{}\n\nTool: {} ({})\nAccount: {}\nRoute: {}\nSettings: {}\n\nDetected user settings:\n{}",if self.page==2 {"p Preview diff i Review / install u Restore backup x Custom settings file\nn Sign in to another subscription account"}else{"l Launch worktree session n Sign in to another subscription account"},string(&self.tools()[self.tool],"label"),if self.tools()[self.tool]["available"]==true {"Detected"}else{"Binary not found"},self.account().unwrap_or_else(||"Existing user settings".into()),self.route().map(|r|string(r,"label")).unwrap_or_default(),if self.settings_file.is_empty(){"Detected location"}else{&self.settings_file},string(&self.tools()[self.tool],"status")); + if let Some(preview)=&self.preview { + body.push_str(&format!("\n\nSELECTED SETTINGS\n{}",string(preview,"current"))); + body.push_str("\n\nSETTINGS DIFF · − current / + proposed\n"); + if let Some(error)=preview["error"].as_str(){body.push_str(error);} + if let Some(changes)=preview["changes"].as_array(){for change in changes{body.push_str(&format!("\n{} · {}\n− {}\n+ {}\n",string(change,"file"),string(change,"key"),string(change,"before"),string(change,"after")));}} + body.push_str(&format!("\n{}",string(preview,"authentication"))); + } + body + } + 3=>{ + let selected=self.selected_session.as_deref(); + let mut lines=vec![format!("s Next session f Filter turn/model/route\nSession: {}{} Filter: {}\n{} recent calls{}; {} unreadable records skipped.\nEach row is one model call; a turn can contain several calls.\n",selected.unwrap_or("All sessions"),if self.session==usize::MAX {" (outside recent history)"} else {""},self.filter,self.snapshot["entries"].as_array().map_or(0,Vec::len),if self.snapshot["limited"]==true{" (8 MiB / 5,000 record limit)"}else{""},self.snapshot["skipped"])]; + if let Some(entries)=self.snapshot["entries"].as_array(){for e in entries.iter().rev(){ + if selected.is_some_and(|s|e["session_id"].as_str()!=Some(s)){continue;} + if !format!("{} {} {}",string(e,"model"),string(e,"turn_id"),string(e,"route_id")).to_lowercase().contains(&self.filter.to_lowercase()){continue;} + lines.push(format!("{} {}{}\nSession {} Turn {} Route {}\nUncached input {} Cached input {} Output {}\n",string(e,"ts"),string(e,"model"),if e["tier"]=="classifier"{" (routing overhead)"}else{" (answer call)"},e["session_id"].as_str().filter(|s|!s.is_empty()).unwrap_or("Not recorded"),e["turn_id"].as_str().filter(|s|!s.is_empty()).unwrap_or("Not recorded"),e["route_id"].as_str().filter(|s|!s.is_empty()).unwrap_or("Not recorded"),e["prompt_tokens"].as_u64().unwrap_or_default().saturating_sub(e["cached_tokens"].as_u64().unwrap_or_default()),e["cached_tokens"],e["completion_tokens"])); + }}lines.join("\n") + } + _=>"o Open server config\na Open app settings\nc Check config\nb Restart server\nu Update from source\n\nUpdate uses the checkout that installed Switchyard.app.\nNative account limits still apply. Switchyard does not rotate login tokens.".into(), + } + } + fn key(&mut self, controller: &mut Controller, code: KeyCode) -> Result<(), String> { + match code { + KeyCode::Tab => { + self.page = (self.page + 1) % PAGES.len(); + self.scroll = 0; + } + KeyCode::BackTab => { + self.page = (self.page + PAGES.len() - 1) % PAGES.len(); + self.scroll = 0; + } + KeyCode::Down | KeyCode::Char('r') => self.move_route(true), + KeyCode::Up => self.move_route(false), + KeyCode::PageDown => self.scroll = self.scroll.saturating_add(10), + KeyCode::PageUp => self.scroll = self.scroll.saturating_sub(10), + KeyCode::F(5) => self.refresh(controller)?, + KeyCode::Char('t') => { + self.tool = (self.tool + 1) % self.tools().len(); + self.account = 0; + self.missing_account = None; + self.settings_file.clear(); + self.preview = None; + } + KeyCode::Char('a') if self.page == 2 || self.page == 4 => { + self.account = self.account.saturating_add(1) + % (self.tools()[self.tool]["accounts"] + .as_array() + .map_or(0, Vec::len) + + 1); + self.missing_account = None; + self.settings_file.clear(); + self.preview = None; + } + KeyCode::Char('e') if self.page == 1 => { + let kind = string(self.route()?, "kind"); + self.form( + "Routing algorithm", + FormKind::Algorithm, + vec![("Algorithm".into(), kind)], + ); + } + KeyCode::Char('m' | 'k') if self.page == 1 => { + let mut fields = vec![( + "Endpoint name".into(), + self.snapshot["clients"][0]["name"] + .as_str() + .unwrap_or_default() + .into(), + )]; + if code == KeyCode::Char('k') { + fields.push(("API key".into(), String::new())); + } + self.form( + "Models", + if code == KeyCode::Char('k') { + FormKind::Key + } else { + FormKind::Models + }, + fields, + ); + } + KeyCode::Char('p') if self.page == 2 => self.preview_install(controller)?, + KeyCode::Char('x') if self.page == 2 => self.form( + "Custom settings file; empty uses detected settings", + FormKind::SettingsFile, + vec![( + "Absolute file path (Pi: models.json)".into(), + self.settings_file.clone(), + )], + ), + KeyCode::Char('/') if self.page == 1 => self.form( + "Find a route", + FormKind::RouteFilter, + vec![( + "Name, method, endpoint, or model".into(), + self.route_filter.clone(), + )], + ), + KeyCode::Char('d') if self.page == 1 => { + let route = self.route()?; + self.preview = Some(serde_json::json!({"revision":route["revision"]})); + self.form( + &format!( + "Delete {}? Targets stay; config is backed up", + string(self.route()?, "id") + ), + FormKind::Remove, + vec![("Type the route name to delete".into(), String::new())], + ); + } + KeyCode::Char('i') if self.page == 2 => { + if self.preview.is_none() { + self.preview_install(controller)?; + } else if let Some(error) = self.preview.as_ref().and_then(|p| p["error"].as_str()) + { + return Err(error.into()); + } else { + let r = self.route()?; + self.dispatch( + controller, + Action::Install { + tool: self.tool(), + account: self.account(), + settings_file: self.custom_file(), + route: string(r, "key"), + id: string(r, "id"), + preview_token: self + .preview + .as_ref() + .and_then(|p| p["preview_token"].as_str()) + .map(str::to_owned), + }, + ); + self.preview = None; + } + } + KeyCode::Char('u') if self.page == 2 => { + let reply = controller.dispatch(Action::PreviewRestore { + tool: self.tool(), + account: self.account(), + settings_file: self.custom_file(), + })?; + self.preview = Some(reply.data); + self.form( + "Restore coding-tool settings? Switchyard routes are kept", + FormKind::Restore, + vec![("Type RESTORE to confirm".into(), String::new())], + ); + } + KeyCode::Char('n') if self.page == 2 || self.page == 4 => self.form( + "Sign in to another subscription account", + FormKind::Account, + vec![("Account name".into(), String::new())], + ), + KeyCode::Char('l') if self.page == 4 => self.form( + "Launch worktree session", + FormKind::Project, + vec![("Git project absolute path".into(), String::new())], + ), + KeyCode::Char('s') if self.page == 3 => { + self.session = self.session.saturating_add(1) + % (self.snapshot["sessions"].as_array().map_or(0, Vec::len) + 1); + self.selected_session = self + .session + .checked_sub(1) + .and_then(|i| self.snapshot["sessions"].as_array()?.get(i)) + .and_then(Value::as_str) + .map(str::to_owned); + self.scroll = 0; + } + KeyCode::Char('f') if self.page == 3 => self.form( + "Filter turn, model, or route", + FormKind::Filter, + vec![("Search".into(), self.filter.clone())], + ), + KeyCode::Char('o') if self.page == 5 => { + self.dispatch(controller, Action::OpenConfig {}) + } + KeyCode::Char('c') if self.page == 5 => { + self.dispatch(controller, Action::CheckConfig {}) + } + KeyCode::Char('a') if self.page == 5 => { + self.dispatch(controller, Action::OpenSettings {}) + } + KeyCode::Char('b') if self.page == 5 => self.dispatch(controller, Action::Restart {}), + KeyCode::Char('u') if self.page == 5 => { + let reply = controller.dispatch(Action::PreviewUpdate {})?; + self.preview = Some(reply.data); + self.form( + "Review source update · type UPDATE to start", + FormKind::Update, + vec![("Confirmation".into(), String::new())], + ); + } + KeyCode::Char('h') => { + self.showing_details = !self.showing_details; + self.scroll = 0; + } + KeyCode::Esc => { + self.message.clear(); + self.showing_details = false; + self.scroll = 0; + } + _ => {} + } + Ok(()) + } +} +fn string(v: &Value, key: &str) -> String { + v[key].as_str().unwrap_or_default().into() +} +enum WorkKind { + Key(KeyCode), + Submit, + Refresh, +} +struct Work { + view: View, + kind: WorkKind, +} +struct PasteMode; +impl Drop for PasteMode { + fn drop(&mut self) { + let _ = ratatui::crossterm::execute!(std::io::stdout(), event::DisableBracketedPaste); + } +} +pub fn run(mut controller: Controller) -> Result<(), String> { + let (snapshot, page) = match controller.snapshot() { + Ok(snapshot) => (snapshot, 0), + Err(error) => ( + serde_json::json!({"generation":0,"errors":[error],"routes":[],"clients":[],"sessions":[],"history":[],"tools":crate::harness::HARNESSES.iter().map(|(tool,label)|serde_json::json!({"tool":tool,"label":label,"accounts":[],"status":"Fix app settings, then refresh."})).collect::>(),"summary":[],"settings":{}}), + 5, + ), + }; + let mut view = View { + snapshot, + page, + route: 0, + tool: 0, + account: 0, + session: 0, + filter: String::new(), + route_filter: String::new(), + settings_file: String::new(), + preview: None, + scroll: 0, + message: String::new(), + form: None, + details: String::new(), + showing_details: false, + selected_session: None, + missing_account: None, + }; + ratatui::crossterm::execute!(std::io::stdout(), event::EnableBracketedPaste) + .map_err(|e| e.to_string())?; + let _paste_mode = PasteMode; + let (requests, jobs) = mpsc::channel::(); + let (completed, results) = mpsc::channel::(); + // One worker owns the controller while the terminal continues to draw and read input. + let worker = thread::spawn(move || { + while let Ok(mut job) = jobs.recv() { + let result = match job.kind { + WorkKind::Key(code) => job.view.key(&mut controller, code), + WorkKind::Submit => job.view.submit(&mut controller), + WorkKind::Refresh => job.view.refresh(&mut controller), + }; + if let Err(error) = result { + job.view.message = error; + } + if completed.send(job.view).is_err() { + break; + } + } + }); + let result = ratatui::run(|terminal| -> Result<(), String> { + let mut last = Instant::now(); + let mut busy = false; + let mut quit_pending = false; + let mut work_page = 0; + loop { + if let Ok(mut updated) = results.try_recv() { + if view.page != work_page { + updated.page = view.page; + updated.scroll = view.scroll; + } + view = updated; + busy = false; + if quit_pending { + return Ok(()); + } + } + terminal.draw(|frame| { + let areas = Layout::vertical([ + Constraint::Length(3), + Constraint::Min(5), + Constraint::Length(5), + ]).split(frame.area()); + frame.render_widget(Tabs::new(PAGES.iter().copied()).select(view.page).highlight_style(Style::default().fg(Color::White).bg(Color::Rgb(40,56,76))).block(Block::bordered().title("Switchyard")),areas[0]); + let mut body_area=areas[1]; + if view.page==0 && view.form.is_none() { + let split=Layout::vertical([Constraint::Min(5),Constraint::Length(6)]).split(body_area); + body_area=split[0]; + let data=view.snapshot["activity"].as_array().map(|items|items.iter().filter_map(Value::as_u64).collect::>()).unwrap_or_default(); + frame.render_widget(Sparkline::default().data(&data).style(Style::default().fg(Color::LightBlue)).block(Block::bordered().title("Recent model calls · past 24h · hourly")),split[1]); + } + let (body, title) = if let Some(form) = &view.form { + let rows = form.fields.iter().enumerate().map(|(i, (label, value))| { + let marker = if i == form.index { ">" } else { " " }; + let mut value = if matches!(form.kind, FormKind::Key) && i == 1 { + "•".repeat(value.chars().count()) + } else { value.clone() }; + if i==form.index { + let cursor=value.char_indices().nth(form.cursor).map_or(value.len(),|(index,_)|index); + value.insert(cursor,'▏'); + } + format!("{marker} {label}: {value}") + }).collect::>().join("\n"); + let hints=match &form.kind { + FormKind::ConfirmApply(..) | FormKind::ConfirmLaunch(..) | FormKind::Update => view.preview.as_ref().map(|p| format!("\n\n{}", serde_json::to_string_pretty(p).unwrap_or_default())).unwrap_or_default(), + FormKind::Algorithm=>format!("\n\nAlgorithms: {}",crate::server_config::ALGORITHMS.iter().map(|a|format!("{} ({})",a.title,a.kind)).collect::>().join(" · ")), + FormKind::Apply(..)=>format!("\n\nEndpoints: {}\nUse the endpoint name and a model ID. Unlisted model IDs are allowed.",view.snapshot["clients"].as_array().map(|a|a.iter().map(|c|string(c,"name")).collect::>().join(", ")).unwrap_or_default()), + FormKind::Restore=>view.preview.as_ref().map(|p|format!("\n\n{}\n{}",string(p,"warning"),p["files"].as_array().map(|a|a.iter().map(|f|format!("{}: {}",string(f,"action"),string(f,"file"))).collect::>().join("\n")).unwrap_or_default())).unwrap_or_default(), + _=>String::new(), + }; + (format!("{rows}{hints}"), form.title.clone()) + } else { (view.body(), PAGES[view.page].into()) }; + frame.render_widget( + Paragraph::new(body) + .wrap(Wrap { trim: false }) + .scroll((view.scroll, 0)) + .block(Block::bordered().title(title)), + body_area, + ); + frame.render_widget( + Paragraph::new(format!("Tab: section · PgUp/PgDn: scroll · F5: refresh · h: details · Esc: dismiss · q: quit\nForms: Tab: field · ↑/↓: choices · ←/→: cursor · Ctrl-U: clear · Enter: submit · Esc: cancel\n{}", if busy {if quit_pending {"Finishing the operation before quitting…"} else {"Working… Navigation is available; wait before starting another action."}} else {&view.message})), + areas[2], + ); + }).map_err(|e|e.to_string())?; + if event::poll(Duration::from_millis(200)).map_err(|e| e.to_string())? { + let event = event::read().map_err(|e| e.to_string())?; + if let Event::Paste(text) = &event { + if !busy && let Some(form) = &mut view.form { + form.insert(text); + } + continue; + } + let Event::Key(key) = event else { + continue; + }; + if key.kind != KeyEventKind::Press { + continue; + } + if busy { + match key.code { + KeyCode::Char('q') if view.form.is_none() => quit_pending = true, + KeyCode::Tab if view.form.is_none() => { + view.page = (view.page + 1) % PAGES.len(); + view.scroll = 0; + } + KeyCode::BackTab if view.form.is_none() => { + view.page = (view.page + PAGES.len() - 1) % PAGES.len(); + view.scroll = 0; + } + KeyCode::PageDown => view.scroll = view.scroll.saturating_add(10), + KeyCode::PageUp => view.scroll = view.scroll.saturating_sub(10), + _ => {} + } + continue; + } + if let Some(form) = view.form.as_mut() { + match key.code { + KeyCode::Esc => { + view.scroll = form.page_scroll; + view.form = None; + } + KeyCode::Enter => { + work_page = view.page; + requests + .send(Work { + view: view.clone(), + kind: WorkKind::Submit, + }) + .map_err(|e| e.to_string())?; + busy = true; + } + KeyCode::Up | KeyCode::Down => { + let options: Vec = match &form.kind { + FormKind::Algorithm => crate::server_config::ALGORITHMS + .iter() + .map(|a| a.kind.to_string()) + .collect(), + FormKind::Apply(..) if form.index % 2 == 0 => { + view.snapshot["clients"] + .as_array() + .map(|clients| { + clients + .iter() + .filter_map(|c| { + c["name"].as_str().map(str::to_owned) + }) + .collect() + }) + .unwrap_or_default() + } + _ => Vec::new(), + }; + if !options.is_empty() { + let index = options + .iter() + .position(|option| option == &form.fields[form.index].1) + .unwrap_or(0); + let next = if key.code == KeyCode::Down { + (index + 1) % options.len() + } else { + (index + options.len() - 1) % options.len() + }; + form.fields[form.index].1 = options[next].clone(); + form.cursor = options[next].chars().count(); + } + } + KeyCode::PageDown => view.scroll = view.scroll.saturating_add(10), + KeyCode::PageUp => view.scroll = view.scroll.saturating_sub(10), + _ => form.edit(key), + } + continue; + } + if key.code == KeyCode::Char('q') { + return Ok(()); + } + work_page = view.page; + requests + .send(Work { + view: view.clone(), + kind: WorkKind::Key(key.code), + }) + .map_err(|e| e.to_string())?; + busy = true; + } + if last.elapsed() + > Duration::from_secs(view.snapshot["refresh_seconds"].as_u64().unwrap_or(30)) + && view.form.is_none() + && !busy + { + work_page = view.page; + requests + .send(Work { + view: view.clone(), + kind: WorkKind::Refresh, + }) + .map_err(|e| e.to_string())?; + busy = true; + last = Instant::now(); + } + } + }); + drop(requests); + worker + .join() + .map_err(|_| "The terminal operation worker stopped unexpectedly.".to_string())?; + if result.is_err() { + let _ = ratatui::try_restore(); + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::Config; + use serde_json::json; + + fn view() -> View { + View { + snapshot: json!({"routes":[{"key":"a","id":"a","label":"alpha"},{"key":"b","id":"b","label":"beta"}]}), + page: 1, + route: 0, + tool: 0, + account: 0, + session: 0, + filter: String::new(), + route_filter: "alpha".into(), + settings_file: String::new(), + preview: Some(json!({"proposed":"old settings"})), + scroll: 0, + message: String::new(), + form: None, + details: String::new(), + showing_details: false, + selected_session: None, + missing_account: None, + } + } + + #[test] + fn route_search_preserves_matching_selection_and_selects_the_first_match() { + let dir = tempfile::tempdir().expect("directory"); + let mut controller = Controller::new(Config::default(), dir.path().join("app.toml")); + let mut view = view(); + view.route = 1; + for (filter, expected) in [("", 1), ("beta", 1), ("alpha", 0), ("missing", 0)] { + view.form( + "Search", + FormKind::RouteFilter, + vec![("Search".into(), filter.into())], + ); + view.submit(&mut controller).expect("search"); + assert_eq!(view.route, expected); + assert!(view.preview.is_none()); + } + assert!(view.route().is_err()); + } + + #[test] + // A mismatched confirmation must fail before controller dispatch can create files. + fn unconfirmed_restore_leaves_files_untouched() { + let directory = tempfile::tempdir().expect("directory"); + let mut controller = Controller::new(Config::default(), directory.path().join("app.toml")); + let mut view = view(); + view.form( + "Restore", + FormKind::Restore, + vec![("Confirmation".into(), "no".into())], + ); + assert_eq!( + view.submit(&mut controller).expect_err("reject"), + "Confirmation did not match. Nothing was restored." + ); + assert_eq!( + std::fs::read_dir(directory.path()).expect("files").count(), + 0 + ); + } + + #[test] + fn refreshing_the_snapshot_keeps_the_install_preview_readable() { + let directory = tempfile::tempdir().expect("directory"); + let settings = directory.path().join("app.toml"); + std::fs::write( + &settings, + format!( + "config_file={:?}\nrouting_log={:?}\nserver_url='http://127.0.0.1:0'\n", + directory.path().join("server.toml"), + directory.path().join("history.jsonl") + ), + ) + .expect("settings"); + let mut controller = Controller::new(Config::load(&settings).expect("settings"), settings); + let mut view = view(); + view.refresh(&mut controller).expect("refresh"); + assert_eq!( + view.preview.expect("retained preview")["proposed"], + "old settings" + ); + } + + #[test] + // A read failure must not appear as an empty log or zero recorded usage. + fn unavailable_data_does_not_render_empty_usage() { + let mut view = view(); + view.snapshot["data_state"] = + json!({"usage":{"state":"unavailable"},"history":{"state":"unavailable"}}); + view.snapshot["errors"] = json!(["Could not read usage"]); + view.page = 0; + assert!(view.body().contains("Could not read usage")); + assert!(!view.body().contains("model calls")); + view.page = 3; + assert!(view.body().contains("Could not read usage")); + assert!(!view.body().contains("recent calls")); + } + + #[test] + // Snapshot choices must keep their original revision when the file changes before editing. + fn externally_changed_routes_cannot_bless_snapshot_choices() { + let dir = tempfile::tempdir().expect("fixture"); + let settings = dir.path().join("desktop.toml"); + let routes = dir.path().join("server.toml"); + let original = "[llm_clients.c]\nbase_url='http://localhost:1'\nformat='openai_responses'\n[targets.m]\nid='original'\nllm_client='c'\n[routes.r]\nid='public'\ntype='passthrough'\ntarget='m'\n"; + std::fs::write(&routes, original).expect("routes"); + std::fs::write( + &settings, + format!( + "config_file={routes:?}\nrouting_log={:?}\nserver_url='http://127.0.0.1:0'", + dir.path().join("log") + ), + ) + .expect("settings"); + let mut controller = Controller::new(Config::load(&settings).expect("config"), settings); + let mut view = view(); + view.route_filter.clear(); + view.refresh(&mut controller).expect("snapshot"); + view.route = 0; + let external = original.replace("id='original'", "id='external'"); + std::fs::write(&routes, &external).expect("external edit"); + view.form( + "Algorithm", + FormKind::Algorithm, + vec![("Algorithm".into(), "passthrough".into())], + ); + view.submit(&mut controller).expect("editor"); + assert!(view.submit(&mut controller).is_err()); + assert!(matches!( + view.form.as_ref().expect("retained draft").kind, + FormKind::Apply(..) + )); + assert_eq!( + std::fs::read_to_string(&routes).expect("preserved"), + external + ); + assert_eq!(std::fs::read_dir(dir.path()).expect("files").count(), 2); + } + + #[test] + fn editing_a_pasted_path_preserves_unicode_and_the_suffix() { + let mut view = view(); + view.form( + "Project", + FormKind::Project, + vec![("Folder".into(), String::new())], + ); + let form = view.form.as_mut().expect("form"); + form.insert("/tmp/équipe/project"); + form.cursor = 6; + form.edit(KeyEvent::new(KeyCode::Delete, KeyModifiers::NONE)); + form.insert("x"); + assert_eq!(form.fields[0].1, "/tmp/éxuipe/project"); + form.edit(KeyEvent::new(KeyCode::Home, KeyModifiers::NONE)); + form.insert("prefix"); + assert_eq!(form.fields[0].1, "prefix/tmp/éxuipe/project"); + } +} diff --git a/crates/switchyard-desktop/src/windows.rs b/crates/switchyard-desktop/src/windows.rs new file mode 100644 index 0000000000..6b8b9cb6b3 --- /dev/null +++ b/crates/switchyard-desktop/src/windows.rs @@ -0,0 +1,119 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Coding tools run in their own Windows console with explicit arguments and account directories. + +use serde::{Deserialize, Serialize}; +use std::{ + path::{Path, PathBuf}, + process::Command, +}; + +/// This function reads current-user MSIX registrations without launching or changing the Codex app. +pub fn codex_app() -> Option { + use windows::{Management::Deployment::PackageManager, core::HSTRING}; + // An empty SID restricts discovery to packages registered for the current user. + let packages = PackageManager::new() + .ok()? + .FindPackagesByUserSecurityId(&HSTRING::new()) + .ok()?; + for package in packages { + let Some(name) = package.Id().ok().and_then(|id| id.Name().ok()) else { + continue; + }; + if name == "OpenAI.Codex" { + return Some(PathBuf::from(package.InstalledPath().ok()?.to_os_string())); + } + } + None +} + +#[derive(Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct TerminalJob { + pub binary: PathBuf, + pub args: Vec, + pub directory: Option, + pub environment: Vec<(String, String)>, + pub remove_environment: Vec, +} +/// The child reads structured launch settings so arguments and account directories remain separate. +pub fn open_terminal(job: TerminalJob) -> Result<(), String> { + use std::io::Write; + let mut file = tempfile::Builder::new() + .prefix("switchyard-terminal-") + .suffix(".json") + .tempfile() + .map_err(|e| e.to_string())?; + file.write_all(&serde_json::to_vec(&job).map_err(|e| e.to_string())?) + .map_err(|e| e.to_string())?; + let path = file.into_temp_path(); + let mut child = Command::new(std::env::current_exe().map_err(|e| e.to_string())?); + child.arg("--terminal-job").arg(&path); + child.spawn().map_err(|error| error.to_string())?; + path.keep().map_err(|error| error.to_string())?; + Ok(()) +} +pub fn attach_console() { + unsafe { + windows_console(false); + } +} +pub fn allocate_console() { + unsafe { + windows_console(true); + } +} +unsafe fn windows_console(allocate: bool) { + // The GUI has no console; terminal mode and coding-tool jobs allocate one only when needed. + unsafe extern "system" { + fn AllocConsole() -> i32; + fn AttachConsole(pid: u32) -> i32; + } + unsafe { + if allocate { + AllocConsole(); + } else { + AttachConsole(u32::MAX); + } + } +} +pub fn terminal_job() -> Option> { + use std::io::IsTerminal; + let args: Vec<_> = std::env::args_os().skip(1).collect(); + if args.first().is_none_or(|v| v != "--terminal-job") { + return None; + } + Some((|| { + if args.len() != 2 { + return Err("The terminal job requires one saved launch file.".into()); + } + let path = Path::new(&args[1]); + let job: TerminalJob = + serde_json::from_slice(&std::fs::read(path).map_err(|e| e.to_string())?) + .map_err(|e| e.to_string())?; + std::fs::remove_file(path).map_err(|e| e.to_string())?; + allocate_console(); + let mut child = Command::new(&job.binary); + child.args(&job.args).envs(job.environment); + for name in job.remove_environment { + child.env_remove(name); + } + if let Some(directory) = job.directory { + child.current_dir(directory); + } + let status = child + .status() + .map_err(|e| format!("Could not start the coding tool: {e}"))?; + println!("Coding tool exited with {status}. Press Enter to close this window."); + if std::io::stdin().is_terminal() { + let mut line = String::new(); + let _ = std::io::stdin().read_line(&mut line); + } + if status.success() { + Ok(()) + } else { + Err(format!("Coding tool exited with {status}.")) + } + })()) +} diff --git a/crates/switchyard-desktop/tauri.conf.json b/crates/switchyard-desktop/tauri.conf.json new file mode 100644 index 0000000000..8583bb4324 --- /dev/null +++ b/crates/switchyard-desktop/tauri.conf.json @@ -0,0 +1,32 @@ +{ + "productName": "Switchyard", + "version": "0.3.0", + "identifier": "com.nvidia.switchyard", + "build": { + "frontendDist": "ui" + }, + "app": { + "withGlobalTauri": true, + "windows": [ + { + "label": "main", + "title": "Switchyard", + "width": 1080, + "height": 760, + "minWidth": 760, + "minHeight": 540 + } + ], + "security": { + "csp": "default-src 'self'; script-src 'self'; style-src 'self'; connect-src ipc: http://ipc.localhost; img-src 'self' data:; object-src 'none'; frame-src 'none'; base-uri 'none'" + } + }, + "bundle": { + "active": false, + "icon": [ + "icons/icon.icns", + "icons/icon.png", + "icons/icon.ico" + ] + } +} diff --git a/crates/switchyard-desktop/tests/frontend.test.cjs b/crates/switchyard-desktop/tests/frontend.test.cjs new file mode 100644 index 0000000000..9eedfbb3bf --- /dev/null +++ b/crates/switchyard-desktop/tests/frontend.test.cjs @@ -0,0 +1,148 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +'use strict'; +const {test} = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const vm = require('node:vm'); +function editor(invoke = () => new Promise(() => {})) { + const elements = new Map(); + const element = selector => { + if(!elements.has(selector))elements.set(selector,{dataset:{},append(){},querySelectorAll(){return [];},setAttribute(){},focus(){this.focused=true;}}); + return elements.get(selector); + }; + const context = vm.createContext({ + window: {__TAURI__: {core: {invoke}}}, + document: {createElement:()=>({append(){}}),querySelector: element, querySelectorAll: () => []}, + setTimeout: () => {},structuredClone, + }); + const source=fs.readFileSync(path.join(__dirname, '../ui/app.js'), 'utf8'); + vm.runInContext(source.slice(0,source.indexOf("content.append(node('p','Loading Switchyard…'")), context); + return context; +} +// The classifier occupies position zero, so copying by position would replace the capable model. +test('algorithm switches preserve model roles instead of positions', () => { + const context = editor(); + const result = vm.runInContext(` + const route = {kind:'composite',choices:[{model:'judge'},{model:'capable'},{model:'efficient'}]}; + const draft = {algorithm:'stage_router',choices:route.choices,memory:new Map([['Judge',route.choices[0]],['Capable',route.choices[1]],['Efficient',route.choices[2]]])}; + [roleChoice(draft,route,{tier:'Capable'},0).model,roleChoice(draft,route,{tier:'Efficient'},1).model]; + `, context); + assert.deepEqual(Array.from(result), ['capable', 'efficient']); +}); +// A successful editor redraw must preserve the distinction between a saved route and a failed restart. +test('editor redraw retains the saved route and restart result', async () => { + const context = editor((command, args) => command === 'snapshot' + ? new Promise(() => {}) + : Promise.resolve({message: args.action.kind === 'editor' ? 'Single model.' : 'Saved the route. Restart failed.',data:{warning:args.action.kind!=='editor'}})); + const result = await vm.runInContext(`(async () => { + busy = false; + await run({kind:'apply'}, false); + await run({kind:'editor'}, false); + return status.textContent; + })()`, context); + assert.equal(result, 'Attention: Saved the route. Restart failed.'); +}); + +// Model totals combine answer and classifier calls; session counts use only the chosen period. +test('usage totals retain cached tokens, routing overhead, and session dates', () => { + const context = editor(); + vm.runInContext(fs.readFileSync(path.join(__dirname, '../ui/analytics.js'), 'utf8'), context); + const result = vm.runInContext(`(() => { + const row=modelRows({routed:{model:{requests:2,input:100,cached_input:40,output:20}},classifier:{model:{requests:1,input:10,cached_input:0,output:2}}})[0]; + const entries=[ + {ts:new Date(2026,8,1,12).toISOString(),session_id:'s',prompt_tokens:90,cached_tokens:30,completion_tokens:10}, + {ts:new Date(2026,8,28,12).toISOString(),session_id:'s',prompt_tokens:50,cached_tokens:20,completion_tokens:5}, + {ts:new Date(2026,8,29,0).toISOString(),session_id:null,prompt_tokens:1,cached_tokens:0,completion_tokens:1}, + ]; + return [row.requests,tokenTotal(row),sessionRows(entries,'today','2026-09-28')[0].requests,sessionRows(entries,'all','2026-09-28').length]; + })()`, context); + assert.deepEqual(Array.from(result), [3,172,1,2]); +}); + +// Background refresh must not recreate a dismissed action result. +test('action notifications stay dismissed through refresh', async () => { + const context = editor(command => command === 'snapshot' ? new Promise(() => {}) : Promise.resolve({message:'Removed the route. Restarted Switchyard.'})); + const result = await vm.runInContext(`(async () => { + busy=false; + await run({kind:'remove'},false); + const shown=[notification.hidden,notification.dataset.tone,status.textContent]; + dismissNotification(); + return [...shown,notification.hidden,status.textContent]; + })()`, context); + assert.deepEqual(Array.from(result), [false,'success','✓ Removed the route. Restarted Switchyard.',true,'']); + const refreshing = editor(() => Promise.resolve({errors:['Server is unavailable.']})); + const afterRefresh = await vm.runInContext(`(async () => { + render=()=>{}; + busy=false;reading=false; + notify('Removed the route.'); + dismissNotification(); + await refresh(); + return [notification.hidden,status.textContent,pageErrors.textContent]; + })()`, refreshing); + assert.deepEqual(Array.from(afterRefresh), [true,'','Server is unavailable.']); +}); + +// A saved config with a failed restart needs a warning rather than a save error. +test('saved edits report restart warnings separately from action errors', async () => { + const context = editor(() => Promise.resolve({message:'Removed the route. Switchyard could not restart.',data:{warning:true,details:['Backup: config.backup']}})); + const warning = await vm.runInContext(`(async () => { + busy=false; + await run({kind:'remove'},false); + return [notification.dataset.tone,document.querySelector('#notification-details').hidden,document.querySelector('#notification-detail-text').textContent]; + })()`,context); + assert.deepEqual(Array.from(warning), ['warning',false,'Backup: config.backup']); + const rejected = editor(() => Promise.reject('Invalid config.')); + const failure = await vm.runInContext(`(async () => { + busy=false; + await run({kind:'apply'},false); + return [notification.dataset.tone,status.textContent]; + })()`,rejected); + assert.deepEqual(Array.from(failure), ['error','Error: Invalid config.']); +}); + +// A disabled Refresh button cannot receive focus after dismissal. +test('dismissal keeps keyboard focus reachable during an operation', () => { + const context = editor(); + const result = vm.runInContext(`(() => { + document.querySelector('#refresh').disabled=true; + dismissNotification(); + return [document.querySelector('#refresh').focused||false,document.querySelector('#title').focused||false]; + })()`, context); + assert.deepEqual(Array.from(result), [false,true]); +}); + +// Recovery must clear the page error while keeping the action notification dismissed. +test('route-page polling clears a recovered background error without recreating feedback', async () => { + let calls=0; + const context = editor(() => {calls++;return Promise.resolve({errors:[]});}); + const result = await vm.runInContext(`(async () => { + page='routes';busy=false;reading=false; + const elementsForPoll=document.querySelector; + document.querySelector=selector=>selector==='[role="dialog"]'?null:elementsForPoll(selector); + showPageErrors('Server is unavailable.'); + notify('Removed the route.');dismissNotification(); + await poll(); + return [pageErrors.hidden,pageErrors.textContent,notification.hidden,status.textContent]; + })()`, context); + assert.deepEqual(Array.from(result), [true,'',true,'']); +}); + +// Cached input is part of prompt tokens; equal timestamps use stable log positions. +test('Usage sorts by time, model, and total tokens with deterministic ties', () => { + const context = editor(); + const result = vm.runInContext(`(() => { + const calls=[ + {id:'old',ts:'2026-10-08T10:00:00Z',model:'B',prompt_tokens:200,cached_tokens:0,completion_tokens:5}, + {id:'first',ts:'2026-10-09T10:00:00Z',model:'A',prompt_tokens:100,cached_tokens:100,completion_tokens:10}, + {id:'last',ts:'2026-10-09T10:00:00Z',model:'A',prompt_tokens:100,cached_tokens:100,completion_tokens:10}, + {id:'unknown',ts:'invalid',model:'C',prompt_tokens:0,cached_tokens:0,completion_tokens:0}, + ]; + return ['newest','oldest','model','tokens'].map(order=>sortedCalls(calls,order).map(e=>e.id)); + })()`, context); + assert.deepEqual(JSON.parse(JSON.stringify(result)), [ + ['last','first','old','unknown'],['old','last','first','unknown'], + ['last','first','old','unknown'],['old','last','first','unknown'], + ]); +}); diff --git a/crates/switchyard-desktop/tests/print.rs b/crates/switchyard-desktop/tests/print.rs new file mode 100644 index 0000000000..31d9d8b760 --- /dev/null +++ b/crates/switchyard-desktop/tests/print.rs @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +// An unreadable log must fail instead of reporting that no requests were recorded. +#[test] +fn unreadable_usage_fails_without_an_empty_summary() { + let home = tempfile::tempdir().expect("fixture"); + let settings = home.path().join("desktop.toml"); + std::fs::write(&settings, format!("routing_log={:?}\n", home.path())).expect("settings"); + let output = std::process::Command::new(env!("CARGO_BIN_EXE_switchyard-desktop")) + .arg("--print") + .arg(&settings) + .env("HOME", home.path()) + .output() + .expect("print"); + assert!(!output.status.success()); + assert!(output.stdout.is_empty()); + assert!(String::from_utf8_lossy(&output.stderr).contains("Usage unavailable")); + assert_eq!(std::fs::read_dir(home.path()).expect("files").count(), 1); +} diff --git a/crates/switchyard-desktop/tests/windows_terminal.rs b/crates/switchyard-desktop/tests/windows_terminal.rs new file mode 100644 index 0000000000..2242174fdf --- /dev/null +++ b/crates/switchyard-desktop/tests/windows_terminal.rs @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +#![cfg(windows)] + +#[test] +fn native_and_npm_launchers_preserve_arguments_accounts_and_working_directory() { + use std::{ + fs, + process::{Command, Stdio}, + }; + let root = tempfile::Builder::new() + .prefix("Terminal équipe space ") + .tempdir() + .expect("fixture"); + let source = root.path().join("tool.rs"); + fs::write(&source,r#"fn main() { + let output=std::env::var_os("FIXTURE_OUTPUT").unwrap(); + let text=format!("{:?}\n{:?}\n{:?}\n{:?}",std::env::args().skip(1).collect::>(),std::env::current_dir().unwrap(),std::env::var("CODEX_HOME"),std::env::var("ANTHROPIC_AUTH_TOKEN")); + std::fs::write(output,text).unwrap(); + }"#).expect("tool source"); + let tool = root.path().join("tool.exe"); + assert!( + Command::new("rustc") + .arg(&source) + .arg("-o") + .arg(&tool) + .status() + .expect("compile fixture") + .success() + ); + let shim = root.path().join("tool.cmd"); + fs::write(&shim, "@echo off\r\n@\"%~dp0tool.exe\" %*\r\n").expect("npm shim"); + let args = vec![ + "space équipe".to_string(), + "a\"b".into(), + "%PATH%".into(), + "& echo injected > forbidden-file".into(), + "C:\\space équipe\\".into(), + "|output^".into(), + ]; + let output = root.path().join("arguments.txt"); + let account = root.path().join("account équipe space"); + for binary in [tool, shim] { + let launch = root.path().join("launch.json"); + fs::write(&launch,serde_json::to_vec(&serde_json::json!({"binary":binary,"args":args,"directory":root.path(),"environment":[["CODEX_HOME",account.display().to_string()],["FIXTURE_OUTPUT",output.display().to_string()]],"remove_environment":["ANTHROPIC_AUTH_TOKEN"]})).expect("launch JSON")).expect("launch record"); + let result = Command::new(env!("CARGO_BIN_EXE_switchyard-desktop")) + .arg("--terminal-job") + .arg(&launch) + .env("ANTHROPIC_AUTH_TOKEN", "must-be-removed") + .stdin(Stdio::null()) + .output() + .expect("terminal job"); + assert!( + result.status.success(), + "terminal launch failed for {binary:?}: status={:?}; stdout={}; stderr={}", + result.status, + String::from_utf8_lossy(&result.stdout), + String::from_utf8_lossy(&result.stderr) + ); + let captured = fs::read_to_string(&output).expect("captured tool launch"); + assert!(captured.starts_with(&format!("{args:?}\n")), "{captured}"); + assert!(captured.contains(&format!("Ok({:?})", account.display().to_string()))); + assert!(captured.contains("NotPresent")); + assert!(captured.contains(&format!("{:?}", root.path()))); + assert!(!launch.exists()); + assert!(!root.path().join("forbidden-file").exists()); + fs::remove_file(&output).expect("reset output"); + } +} diff --git a/crates/switchyard-desktop/ui/analytics.js b/crates/switchyard-desktop/ui/analytics.js new file mode 100644 index 0000000000..4b0c4197cd --- /dev/null +++ b/crates/switchyard-desktop/ui/analytics.js @@ -0,0 +1,151 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +'use strict'; +const analyticsView = {period:'today',group:'model',metric:'tokens',search:'',model:null}; +const chartInstances = []; +const tokenFields = ['requests','input','cached_input','output']; +const chartColors = ['#5b91c9','#31856c','#8961b7','#ad7d20','#be5362','#337f9a']; +const emptyTokens = () => ({requests:0,input:0,cached_input:0,output:0}); +const tokenTotal = row => row.input + row.cached_input + row.output; +const compactNumber = value => new Intl.NumberFormat(undefined,{notation:'compact',maximumFractionDigits:1}).format(value); +const dollars = value => new Intl.NumberFormat(undefined,{style:'currency',currency:'USD'}).format(value); +function addTokens(target, source) { for(const key of tokenFields)target[key] += source[key]; return target; } +function modelRows(totals) { + const rows = new Map(); + for(const group of [totals.routed,totals.classifier])for(const [id,tokens] of Object.entries(group)) { + if(!rows.has(id))rows.set(id,{id,...emptyTokens()}); + addTokens(rows.get(id),tokens); + } + return [...rows.values()]; +} +function disposeCharts() { + for(const {chart,observer} of chartInstances) { observer.disconnect(); chart.dispose(); } + chartInstances.length = 0; +} +// Canvas tooltips display recorded names without interpreting them as HTML. +function mountChart(parent, options, height, description, onClick) { + const container=node('div',undefined,'usage-chart');container.style.height=`${height}px`; + container.setAttribute('role','img');container.setAttribute('aria-label',description);parent.append(container); + const colors=getComputedStyle(document.documentElement);const ink=colors.getPropertyValue('--muted').trim(),panel=colors.getPropertyValue('--control').trim(),line=colors.getPropertyValue('--line').trim(); + const chart=window.echarts.init(container,null,{renderer:'canvas'}); + const recolor=value=>{if(Array.isArray(value))return value.map(recolor);if(value&&typeof value==='object')return Object.fromEntries(Object.entries(value).map(([key,v])=>[key,recolor(v)]));return ['#d1d7e0','#f2f4f8'].includes(value)?ink:value==='#454d59'?line:value;}; + chart.setOption(recolor({animation:false,backgroundColor:'transparent',textStyle:{fontFamily:'-apple-system, BlinkMacSystemFont, Segoe UI, sans-serif',color:'#d1d7e0'}, + aria:{enabled:true,decal:{show:true}},tooltip:{trigger:'axis',renderMode:'richText',confine:true,backgroundColor:panel,textStyle:{color:'#f2f4f8'},borderColor:line},...options})); + if(onClick)chart.on('click',onClick); + const observer=new ResizeObserver(()=>chart.resize());observer.observe(container);chartInstances.push({chart,observer}); +} +// Local midnight boundaries match the Rust reader's calendar-day totals. +function inPeriod(timestamp, period, today) { + if(period==='all')return true; + const date=new Date(timestamp),start=new Date(`${today}T00:00:00`),end=new Date(start); + end.setDate(end.getDate()+1); + if(period==='week')start.setDate(start.getDate()-6); + return date>=start&&date>>0;return chartColors[value%chartColors.length];} +function openUsage(group, id) { + usageView.session='';usageView.search=''; + usageView.exact={group,id,period:analyticsView.period};usageView.period=analyticsView.period; + navigate('usage'); +} +function renderOverview() { + const metrics=snapshot.metrics,line=node('div',undefined,'connection-line'); + if(metrics.running)line.append(node('span','Connected','connection-state connected')); + line.append(node('span',metrics.server_url,'connection-address')); + button(line,`Manage ${snapshot.routes.length} routes`,()=>navigate('routes'));content.append(line);renderOnboarding(); + if(usageUnavailable()){const c=card('Usage unavailable');c.append(node('p','The routing log could not be read. Totals and cost estimates are unavailable.'));button(actions(c),'Retry',()=>refresh());button(actions(c),'Open settings',()=>navigate('settings'));return;} + const controls=node('div',undefined,'analytics-controls');content.append(controls); + const period=field(controls,'Period',select([['all','All retained history'],['week','Seven local calendar days'],['today','Today']],analyticsView.period)); + const totals=snapshot.analytics[analyticsView.period],models=modelRows(totals),combined=models.reduce(addTokens,emptyTokens()); + const overhead=Object.values(totals.classifier).reduce(addTokens,emptyTokens()); + const estimate=snapshot.estimates[analyticsView.period],cost=estimate.cost; + const metricsGrid=node('div',undefined,'usage-metrics');content.append(metricsGrid); + for(const [label,value,detail] of [ + ['Recorded tokens',compactNumber(tokenTotal(combined)),`Model calls: ${combined.requests.toLocaleString()} · includes cached reads`], + ['Routing overhead',compactNumber(tokenTotal(overhead)),`Classifier and judge calls: ${overhead.requests.toLocaleString()}`], + ['Estimated cost',cost?dollars(cost.actual):'—',cost?'Includes routing overhead':'Prices required'], + [cost&&cost.actual>cost.baseline?'Estimated extra cost':'Estimated savings',cost?dollars(Math.abs(cost.baseline-cost.actual)):'—',cost?`vs ${snapshot.baseline_model}`:'Set baseline and model prices'], + ]) { + const panel=node('div',undefined,'usage-metric');panel.append(node('p',label),node('strong',value),node('small',detail));metricsGrid.append(panel); + } + const costNote=node('details',undefined,'estimate-note');costNote.append(node('summary','How cost estimates work')); + costNote.append(node('p','Estimates apply your configured per-token prices to recorded usage. The baseline prices the same answer tokens at one model’s rates. Routing overhead reduces savings. These are not subscription charges or measured token savings.')); + if(cost)costNote.append(node('p',`Baseline: ${snapshot.baseline_model} · ${dollars(cost.baseline)}. ${cost.baseline>0?`${(Math.abs(cost.baseline-cost.actual)/cost.baseline*100).toFixed(1)}% estimated ${cost.actual>cost.baseline?'extra cost':'savings'}.`:''}`)); + else {const missing=node('p',`Missing model prices: ${estimate.missing.join(', ')||'the baseline model'}.`,'warning-note');content.append(missing);button(actions(missing),'Open app settings',()=>run({kind:'open_settings'},false));} + content.append(costNote); + const comparison=card(analyticsView.model?`Usage for ${analyticsView.model}`:'Usage by model');comparison.append(node('p',`Period: ${analyticsView.period==='all'?'All retained history':analyticsView.period==='today'?snapshot.today:`${new Date(new Date(`${snapshot.today}T00:00:00`).setDate(new Date(`${snapshot.today}T00:00:00`).getDate()-6)).toLocaleDateString()} – ${snapshot.today}`} · ${Intl.DateTimeFormat().resolvedOptions().timeZone}.`,'muted')); + const filters=node('div',undefined,'analytics-controls');comparison.append(filters); + const group=field(filters,'Compare',select([['model','Models'],['route','Routes'],['session','Sessions']],analyticsView.group)); + const metric=field(filters,'Measure',select([['tokens','Tokens'],['requests','Model calls']],analyticsView.metric)); + const search=field(filters,'Filter names',input(analyticsView.search));search.placeholder='Find a model, route, or session'; + button(actions(comparison),'Clear filters',()=>{analyticsView.search='';analyticsView.model=null;analyticsView.group='model';render();}); + const chartParent=node('div'),tableParent=node('div',undefined,'analytics-table');comparison.append(chartParent,tableParent); + const labelFor=row=>row.id|| (group.value==='session'?'Session not recorded':'Route not recorded'); + const draw=()=>{ + analyticsView.group=group.value;analyticsView.metric=metric.value;analyticsView.search=search.value; + comparison.querySelector('h3').textContent=analyticsView.model&&group.value==='model'?`Usage for ${analyticsView.model}`:`Usage by ${group.value}`; + // A chart owns its resize observer only while its container is visible. + disposeCharts();chartParent.replaceChildren();tableParent.replaceChildren(); + let rows=group.value==='model'?models:group.value==='route'?Object.entries(totals.routes).map(([id,tokens])=>({id,...tokens})):sessionRows(snapshot.entries,period.value,snapshot.today); + const value=row=>metric.value==='tokens'?tokenTotal(row):row.requests; + rows=rows.filter(row=>(!analyticsView.model||group.value!=='model'||row.id===analyticsView.model)&&labelFor(row).toLowerCase().includes(search.value.toLowerCase())).sort((a,b)=>value(b)-value(a)||a.id.localeCompare(b.id)); + chartParent.append(node('p',group.value==='session'?`Session comparison uses ${snapshot.entries.length.toLocaleString()} recent calls${snapshot.limited?' · limited to 5,000 records / 8 MiB':''}.`:'Totals cover completed calls in the retained log, including routing overhead.','muted')); + if(rows.length) { + const shown=rows.slice(0,10),names=shown.map(labelFor); + if(rows.length>10)chartParent.append(node('p',`${rows.length-10} other groups: ${compactNumber(rows.slice(10).reduce((sum,row)=>sum+value(row),0))} ${metric.value==='tokens'?'tokens':'model calls'}. See all groups in the table.`,'muted')); + chartParent.append(node('small',`Showing ${shown.length} of ${rows.length} ${group.value==='model'?'models':group.value==='route'?'routes':'sessions'} · exact values below`)); + mountChart(chartParent,{ + grid:{left:12,right:24,top:36,bottom:24,containLabel:true}, + legend:{top:0,textStyle:{color:'#d1d7e0'},itemWidth:12,itemHeight:8}, + xAxis:{type:'value',axisLabel:{formatter:compactNumber,color:'#d1d7e0'},splitLine:{lineStyle:{color:'#454d59'}}}, + yAxis:{type:'category',inverse:true,data:names,axisLabel:{color:'#f2f4f8',width:160,overflow:'truncate'},axisTick:{show:false},axisLine:{show:false}}, + series:metric.value==='tokens'?['input','cached_input','output'].map((key,i)=>({name:['Uncached input','Cached input','Output'][i],type:'bar',stack:'tokens',barMaxWidth:18,itemStyle:{color:chartColors[i]},data:shown.map(row=>row[key])})):[{name:'Model calls',type:'bar',barMaxWidth:18,itemStyle:{color:chartColors[0]},data:shown.map(row=>row.requests)}], + },Math.max(200,shown.length*38+80),'Usage comparison. Exact counts and links are in the table below.',event=>openUsage(group.value,shown[event.dataIndex].id)); + } else chartParent.append(node('p',search.value?'No names match this filter.':'No model calls recorded for this period.','empty-state')); + const scroll=node('div',undefined,'usage-table-scroll'),table=node('table',undefined,'usage-totals-table'),head=node('tr'),thead=node('thead'),body=node('tbody'); + for(const name of [group.value==='model'?'Model':group.value==='route'?'Route':'Session','Calls','Uncached input','Cached input','Output','Total tokens',...(group.value==='model'?['Answer tokens','Overhead tokens']:[])]) { const th=node('th',name);th.scope='col';head.append(th); } + scroll.tabIndex=0;scroll.setAttribute('role','region');scroll.setAttribute('aria-label','Usage comparison table');thead.append(head);table.append(thead,body);scroll.append(table);tableParent.append(scroll); + for(const row of rows) { + const tr=node('tr'),name=node('td');button(name,labelFor(row),()=>openUsage(group.value,row.id));tr.append(name); + for(const count of [row.requests,row.input,row.cached_input,row.output,tokenTotal(row),...(group.value==='model'?[tokenTotal(totals.routed[row.id]||emptyTokens()),tokenTotal(totals.classifier[row.id]||emptyTokens())]:[])])tr.append(node('td',count.toLocaleString()));body.append(tr); + } + renderDaily(comparison); + }; + period.onchange=()=>{analyticsView.period=period.value;render();};group.onchange=()=>{analyticsView.model=null;draw();};metric.onchange=draw;search.oninput=()=>{analyticsView.model=null;draw();};draw(); + content.append(node('p',`Aggregate totals exclude ${snapshot.analytics.skipped||0} unreadable records. Incomplete log data can undercount usage.`,'coverage-note')); + content.append(node('p','All retained history resets if the routing log is replaced or truncated. Session and turn details use recent history.','coverage-note')); +} +function renderDaily(parent) { + parent.querySelector('.daily-usage')?.remove(); + const panel=node('div',undefined,'daily-usage');parent.append(panel);panel.append(node('h3','Daily usage by model'),node('p',`This chart shows tokens for the seven local calendar days through ${snapshot.today}, including cached reads and routing overhead.`,'muted')); + const end=new Date(`${snapshot.today}T00:00:00`),days=[]; + for(let offset=6;offset>=0;offset--) {const date=new Date(end);date.setDate(date.getDate()-offset);days.push(`${date.getFullYear()}-${String(date.getMonth()+1).padStart(2,'0')}-${String(date.getDate()).padStart(2,'0')}`);} + const models=modelRows(snapshot.analytics.week).sort((a,b)=>tokenTotal(b)-tokenTotal(a)||a.id.localeCompare(b.id)); + if(!models.length){panel.append(node('p','No model calls recorded in the past 7 days.','empty-state'));return;} + const shown=models.slice(0,5),ids=new Set(shown.map(model=>model.id)); + const daily=days.map(day=>new Map(modelRows(snapshot.analytics.days[day]||{routed:{},classifier:{}}).map(row=>[row.id,tokenTotal(row)]))); + const series=shown.map((model,i)=>({name:model.id,type:'bar',stack:'models',barMaxWidth:40,itemStyle:{color:stableColor(model.id)},data:daily.map(totals=>totals.get(model.id)||0)})); + if(models.length>5)series.push({name:'Other models',type:'bar',stack:'models',itemStyle:{color:chartColors[5]},data:daily.map(totals=>[...totals].reduce((sum,[id,count])=>sum+(ids.has(id)?0:count),0))}); + mountChart(panel,{grid:{left:12,right:20,top:20,bottom:28,containLabel:true}, + xAxis:{type:'category',data:days.map(day=>new Date(`${day}T12:00:00`).toLocaleDateString(undefined,{month:'short',day:'numeric'})),axisLabel:{color:'#d1d7e0'},axisTick:{show:false}}, + yAxis:{type:'value',axisLabel:{color:'#d1d7e0',formatter:compactNumber},splitLine:{lineStyle:{color:'#454d59'}}},series},220,'Daily recorded tokens. Exact daily totals follow.'); + const key=node('div',undefined,'chart-key');series.forEach((item,i)=>{const label=node('span'),swatch=node('i');swatch.style.background=item.itemStyle.color;label.append(swatch,document.createTextNode(item.name));key.append(label);});panel.append(key); + const details=node('details');details.append(node('summary','Exact daily totals'));for(const [index,day] of days.entries())details.append(node('p',`${day}: ${series.map(item=>`${item.name} ${item.data[index].toLocaleString()}`).join(' · ')}`,'summary-row'));panel.append(details); +} + +function renderOnboarding() { + if(snapshot.entries.length || sessionStorage.getItem('switchyard-checklist-dismissed'))return; + const c=card('Get started');c.append(node('p','Complete these steps to see your first recorded call.')); + const steps=[['Server ready',snapshot.metrics.running,'settings'],['Choose a route',snapshot.routes.length>0,'routes'],['Connect a coding tool',connectedTools.size>0,'install'],['Open a session in Terminal',sessionOpened,'sessions'],['Find the first recorded call',false,'usage']]; + for(const [label,done,target] of steps)button(c,`${done?'✓ ':''}${label}`,()=>navigate(target));button(actions(c),'Dismiss checklist',()=>{sessionStorage.setItem('switchyard-checklist-dismissed','1');c.remove();}); +} diff --git a/crates/switchyard-desktop/ui/app.css b/crates/switchyard-desktop/ui/app.css new file mode 100644 index 0000000000..0058e0460e --- /dev/null +++ b/crates/switchyard-desktop/ui/app.css @@ -0,0 +1,254 @@ +/* SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. */ +/* SPDX-License-Identifier: Apache-2.0 */ +:root { + color-scheme: light dark; + font: 13px -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; + color: var(--text); + background: var(--canvas); + --canvas: #191c20; + --sidebar: #101216; + --panel: #20242b; + --control: #252b33; + --text: #f2f4f8; + --muted: #d1d7e0; + --line: #454d59; + --control-border: #8491a2; + --hover: #303640; + --selection: #28384c; + --accent: #a5caff; + --accent-hover: #bfd9fa; + --accent-text: #142538; + --focus: #9bc5ff; + --status: #d1e3fa; + --mono: ui-monospace, "SFMono-Regular", Consolas, monospace; +} + +* { box-sizing: border-box; } +body { margin: 0; display: grid; grid-template-columns: 176px minmax(0, 1fr); min-height: 100vh; line-height: 1.5; } +aside { position: sticky; top: 0; height: 100vh; display: flex; flex-direction: column; padding: 24px 12px 16px; background: var(--sidebar); border-right: 1px solid var(--line); } +.brand { padding: 0 12px; } +h1 { font-size: 17px; font-weight: 650; letter-spacing: -.35px; margin: 0; } +.sidebar-caption { margin: 3px 0 0; font-size: 11px; color: var(--muted); } +nav { display: grid; gap: 3px; margin-top: 28px; } +nav button { border: 0; padding: 8px 12px; background: transparent; font-weight: 500; } +nav button.active { background: var(--selection); color: var(--text); } +aside footer { margin-top: auto; padding: 16px 12px 0; border-top: 1px solid var(--line); font-size: 11px; color: var(--muted); } +main { min-width: 0; padding: 26px 28px 36px; } +header { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-height: 34px; margin-bottom: 22px; } +h2 { margin: 0; font-size: 22px; font-weight: 600; letter-spacing: -.5px; } +h3 { margin: 0 0 14px; font-size: 14px; font-weight: 600; line-height: 1.4; overflow-wrap: anywhere; } +p { margin: 0 0 12px; } +p:last-child { margin-bottom: 0; } +small, .muted { color: var(--muted); } +small { font-size: 11px; line-height: 1.5; } +section { display: grid; gap: 18px; } +.card { min-width: 0; border: 1px solid var(--line); border-radius: 7px; padding: 20px; background: var(--panel); } +.card > p { max-width: 76ch; color: var(--muted); } +button, input, select { font: inherit; color: inherit; border: 1px solid var(--control-border); border-radius: 5px; background: var(--control); min-height: 32px; padding: 6px 10px; } +button { cursor: pointer; text-align: left; font-weight: 500; line-height: 1.35; } +button:hover { background: var(--hover); } +button:active:not(:disabled) { transform: translateY(1px); } +button:disabled, input:disabled, select:disabled { opacity: .55; } +button:disabled { cursor: default; } +button.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-text); } +button.primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); } +button:focus-visible, input:focus-visible, select:focus-visible, summary:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; } +input, select { width: 100%; min-width: 0; } +select { padding-right: 28px; text-overflow: ellipsis; } +input::placeholder { color: var(--muted); } +label { display: grid; gap: 6px; margin: 16px 0; font-size: 12px; font-weight: 500; } +.actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 18px; } +.actions + .actions { margin-top: 8px; } +.actions + p, .actions + pre { margin-top: 16px; } +pre + p { margin-top: 12px; } +pre { font: 12px/1.7 var(--mono); white-space: pre-wrap; overflow-wrap: anywhere; margin: 0; } +[hidden] { display: none !important; } +.notification { position: fixed; bottom: 24px; right: 24px; z-index: 90; display: flex; align-items: flex-start; gap: 12px; width: min(520px, calc(100vw - 48px)); padding: 14px 16px; border: 1px solid #527c64; border-radius: 8px; background: #20382c; color: #c8f5d9; box-shadow: 0 6px 24px #0006; } +.notification[data-tone="progress"] { background: var(--panel); color: var(--status); border-color: var(--line); } +.notification[data-tone="warning"] { background: #3c321e; color: #ffe2a6; border-color: #967846; } +.notification[data-tone="error"] { background: #3b252c; color: #ffd1d9; border-color: #af6574; } +.notification-body { min-width: 0; flex: 1; max-height: min(50vh, 360px); overflow: auto; } +#status { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; } +#dismiss-notification { flex: none; min-width: 32px; border: 0; background: transparent; color: inherit; font-size: 20px; line-height: 1; padding: 6px; } +#dismiss-notification:hover { background: #ffffff15; } +.notification details { color: inherit; } +.notification details pre { margin-top: 8px; } +#page-errors { color: #ffe2a6; white-space: pre-wrap; overflow-wrap: anywhere; } +.install-summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr)); gap: 20px; margin-bottom: 18px; } +.install-progress:empty { display: none; } +.install-progress { padding: 12px; border: 1px solid var(--line); color: var(--status); white-space: pre-wrap; overflow-wrap: anywhere; } +[data-role] { padding: 16px 0; border-top: 1px solid var(--line); } +[data-role] label:first-child { margin-top: 0; } +[data-role] .actions { margin-top: 10px; } +[data-role] > small { display: block; } +details { margin-top: 12px; color: var(--muted); } +summary { cursor: pointer; font-size: 12px; } +details[open] { padding-bottom: 2px; } +details label { color: var(--text); } +.scroll { overflow: auto; margin: 18px -20px -20px; border-top: 1px solid var(--line); border-radius: 0 0 7px 7px; } +table { min-width: 820px; table-layout: fixed; width: 100%; border-collapse: collapse; font-size: 12px; line-height: 1.55; } +th, td { text-align: left; padding: 12px; border-bottom: 1px solid var(--line); overflow-wrap: anywhere; vertical-align: top; } +th { background: var(--sidebar); color: var(--muted); font-weight: 500; white-space: nowrap; } +tbody tr:last-child td { border-bottom: 0; } +tbody tr:hover { background: var(--hover); } +td:nth-last-child(-n+3), th:nth-last-child(-n+3) { width: 76px; white-space: nowrap; text-align: right; font-variant-numeric: tabular-nums; } + +/* Summary values and route details use separators instead of individual cards. */ +.summary-list { display: grid; gap: 0; margin: 0; } +.summary-row { margin: 0; padding: 9px 0; border-bottom: 1px solid var(--line); font-variant-numeric: tabular-nums; overflow-wrap: anywhere; } +.summary-row:last-child { border-bottom: 0; padding-bottom: 0; } +.summary-row:first-child { padding-top: 0; } +.route-list { display: grid; gap: 0; } +.empty-state { padding: 20px 12px; text-align: center; color: var(--muted); } + +@media (min-width: 1100px) { + main { padding-left: 36px; padding-right: 36px; } +} +@media (max-width: 700px) { + body { grid-template-columns: 144px minmax(0, 1fr); } + aside { padding: 20px 8px 12px; } + .brand { padding: 0 8px; } + h1 { font-size: 16px; } + nav button { padding: 8px; } + main { padding: 20px 16px 28px; } + .card { padding: 16px; } + .scroll { margin: 16px -16px -16px; } +} +@media (max-width: 460px) { + body { display: block; } + aside { position: static; height: auto; padding: 14px 12px 10px; border-right: 0; border-bottom: 1px solid var(--line); } + .sidebar-caption, aside footer { display: none; } + nav { display: flex; flex-wrap: wrap; gap: 3px; margin-top: 12px; } + nav button { font-size: 12px; padding: 7px 9px; } + header { margin-bottom: 18px; } + main { padding: 18px 14px 26px; } +} +.route-row { padding: 12px 0; border-bottom: 1px solid var(--line); } +.route-row:first-child { padding-top: 0; } +.route-row:last-child { padding-bottom: 0; border-bottom: 0; } +.route-row h4 { display: inline; margin: 0 8px 0 0; font-size: 13px; font-weight: 600; overflow-wrap: anywhere; } +.route-row p { margin: 5px 0 0; font-size: 12px; overflow-wrap: anywhere; } +.badge { display: inline-block; padding: 1px 6px; border: 1px solid var(--line); border-radius: 4px; font-size: 10px; font-weight: 500; color: var(--muted); vertical-align: 1px; } +.field-grid, .role-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 16px; } +@media (max-width: 700px) { + .field-grid, .role-fields { grid-template-columns: 1fr; } +} +.table-primary { display: block; } +.table-secondary { display: block; color: var(--muted); font-size: 11px; margin-top: 3px; } + +.install-comparison { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); margin-top: 16px; border: 1px solid var(--line); border-radius: 5px; background: var(--sidebar); } +.install-comparison > div { min-width: 0; padding: 14px; } +.install-comparison > div + div { border-left: 1px solid var(--line); } +.install-comparison h4 { margin: 0 0 10px; font-size: 12px; font-weight: 600; color: var(--muted); } +.install-auth { margin-top: 12px; } +@media (max-width: 850px) { + .install-comparison { grid-template-columns: 1fr; } + .install-comparison > div + div { border-left: 0; border-top: 1px solid var(--line); } +} + +.route-workspace { display:grid;grid-template-columns:minmax(180px,30%) minmax(0,1fr);gap:20px; } +.route-picker { max-height:60vh;overflow:auto;display:flex;flex-direction:column;gap:4px; } +.route-picker button { border-color:transparent;background:transparent;overflow-wrap:anywhere;padding:10px; } +.route-picker button small { display:block;margin-top:4px; } +.route-picker button.selected { background:var(--selection);border-color:var(--line); } +.route-detail .card { padding:0;border:0;background:none; } +.install-diff { margin-top:18px;border:1px solid var(--line);border-radius:6px;overflow:hidden;background:var(--sidebar); } +.install-diff h4 { font-size:12px;margin:0;padding:12px;border-bottom:1px solid var(--line); } +.install-diff pre { padding:6px 12px; } +.install-diff p { padding:12px; } +.diff-heading { color:var(--muted);border-top:1px solid var(--line);font-size:11px; } +.diff-remove { background:#38262d;color:#ffd1d9; } +.diff-add { background:#21382f;color:#bbf1d2; } +button.primary { min-height:38px;font-weight:650;box-shadow:0 1px 2px #0008; } +button.danger { color:#ffd1d9; } +@media (max-width:700px) { .route-workspace { grid-template-columns:1fr; } .route-picker { max-height:220px; } } + +.field-grid { align-items:start; } + + +.install-diff details { padding:10px 12px; } +.install-diff details pre { padding:10px 0 0; } + +.modal-backdrop { position:fixed;inset:0;display:grid;place-items:center;background:#000b;z-index:100;padding:16px; } +.confirm-dialog { width:100%;max-width:440px;background:var(--panel);border:1px solid var(--control-border);border-radius:8px;padding:24px;box-shadow:0 12px 48px #0008; } +.confirm-dialog .actions { justify-content:flex-end; } +.confirm-dialog button.danger { background:#6e2834;border-color:#b15d6d;color:#ffd1d9; } + +/* Rows grow with their labels; only the list scrolls. */ +.route-picker { display: grid; grid-auto-rows: max-content; align-content: start; } +.route-picker button { min-width: 0; width: 100%; height: auto; white-space: normal; } +.route-picker .route-name, .route-picker small { display: block; min-width: 0; overflow-wrap: anywhere; } +.route-picker .route-name { font-weight: 600; } +.route-detail { min-width: 0; } +.route-detail .role-fields { grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr)); } +.connection-line { display: flex; align-items: center; flex-wrap: wrap; gap: 8px 14px; font-size: 12px; color: var(--muted); } +.connection-state { font-weight: 600; } +.connection-state.connected { color: #a5e9c5; } +.connection-state::before { content: '●'; padding-right: 6px; } +.connection-address { font-family: var(--mono); min-width: 0; overflow-wrap: anywhere; } +.connection-line button { margin-left: auto; } +.analytics-controls { display: flex; flex-wrap: wrap; gap: 12px; align-items: end; } +.analytics-controls label { margin: 0; flex: 1; min-width: 140px; max-width: 300px; } +.usage-metrics { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px; padding: 20px 0; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); } +.usage-metric { min-width: 0; overflow-wrap: anywhere; } +.usage-metric > p { font-size: 12px; color: var(--muted); margin-bottom: 8px; } +.usage-metric > strong { display: block; font-size: 28px; font-weight: 600; font-variant-numeric: tabular-nums; line-height: 1.25; margin-bottom: 8px; } +.estimate-note { margin: -8px 0 0; font-size: 12px; } +.estimate-note p { margin-top: 10px; max-width: 90ch; overflow-wrap: anywhere; } +.usage-chart { width: 100%; min-width: 0; margin-top: 12px; } +.analytics-controls + div > p { margin-top: 16px; font-size: 12px; } +.usage-table-scroll { overflow: auto; max-height: 320px; border-top: 1px solid var(--line); margin-top: 12px; } +.usage-totals-table { min-width: 640px; table-layout: auto; } +.usage-totals-table th { position: sticky; top: 0; } +.usage-totals-table th:first-child { width: 38%; } +.usage-totals-table th:not(:first-child), .usage-totals-table td:not(:first-child) { white-space: nowrap; width: auto; text-align: right; font-variant-numeric: tabular-nums; } +.usage-totals-table td:first-child { min-width: 140px; max-width: 280px; } +.usage-totals-table button { border: 0; background: none; padding: 0; min-height: 24px; color: var(--accent); overflow-wrap: anywhere; max-width: 100%; } +.usage-totals-table button:hover { text-decoration: underline; } +.daily-usage { border-top: 1px solid var(--line); margin-top: 24px; padding-top: 22px; min-width: 0; } +.daily-usage > p { font-size: 12px; } +.chart-key { display: flex; flex-wrap: wrap; gap: 8px 18px; font-size: 11px; margin-top: 12px; } +.chart-key span { display: flex; align-items: center; min-width: 0; overflow-wrap: anywhere; gap: 6px; } +.chart-key i { width: 10px; height: 10px; flex: none; border-radius: 2px; } +.coverage-note { font-size: 11px; color: var(--muted); } +.usage-filter { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; overflow-wrap: anywhere; } +@media (max-width: 1000px) { .usage-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); } } +@media (max-width: 460px) { .usage-metrics { grid-template-columns: 1fr; } .connection-line button { margin-left: 0; } } + +.refresh-controls { display:flex;flex-wrap:wrap;align-items:center;gap:10px; } +.confirm-dialog { max-height:calc(100dvh - 32px);display:flex;flex-direction:column; } +.confirmation-description { overflow:auto;white-space:pre-wrap;min-height:0; } +.confirm-dialog .actions { flex:none; } +.notification { position:relative;bottom:auto;right:auto;width:100%;margin:0 0 18px;box-shadow:none;z-index:1; } +.notification-body { max-height:240px; } +.install-progress { border:0;padding:0;color:var(--muted); } +.field-error { color:#ffb4be;display:block; } +.warning-note { padding:10px;border-left:3px solid #cba45b;color:var(--text); } +.scroll th { position:sticky;top:0;z-index:1; } +.scroll:focus-visible,.usage-table-scroll:focus-visible { outline:2px solid var(--focus);outline-offset:2px; } +@media (max-width:1000px) { .route-workspace { grid-template-columns:1fr; } .route-picker { max-height:240px; } } +@media (prefers-color-scheme:light) { + :root { --canvas:#f7f8fa;--sidebar:#edf0f4;--panel:#fff;--control:#fff;--text:#18212f;--muted:#46556b;--line:#c7cfdb;--control-border:#78879b;--hover:#edf1f7;--selection:#dbe8fa;--accent:#275a9b;--accent-hover:#204c85;--accent-text:#fff;--focus:#174d98;--status:#254c7b; } + .field-error,button.danger { color:#9b2437; } + .connection-state.connected { color:#17633b; } + #page-errors { color:#765015; } + .notification { background:#e4f4e9;color:#16482b; } + .notification[data-tone="warning"] { background:#fff3d6;color:#65450a; } + .notification[data-tone="error"] { background:#fce8ec;color:#7a1c31; } + .diff-remove { background:#fce8ec;color:#7a1c31; } + .diff-add { background:#e4f4e9;color:#16482b; } +} + +body.enlarged { display:block; } +body.enlarged aside { position:static;height:auto;padding:14px 12px;border-right:0;border-bottom:1px solid var(--line); } +body.enlarged nav { display:flex;flex-wrap:wrap;margin-top:12px; } +body.enlarged .sidebar-caption,body.enlarged aside footer { display:none; } +body.enlarged main { padding:16px; } +body.enlarged .field-grid,body.enlarged .role-fields { grid-template-columns:1fr; } + +.server-state { padding:12px;border-left:3px solid #cba45b;margin-bottom:16px;display:flex;align-items:center;flex-wrap:wrap;gap:8px; } +.server-state span { flex:1 1 200px;overflow-wrap:anywhere; } +.price-row { padding:12px 0;border-top:1px solid var(--line); } + +.modal-backdrop { width:calc(100vw / var(--ui-scale, 1));height:calc(100dvh / var(--ui-scale, 1));right:auto;bottom:auto; } +.confirm-dialog { max-height:calc(100dvh / var(--ui-scale, 1) - 32px); } diff --git a/crates/switchyard-desktop/ui/app.js b/crates/switchyard-desktop/ui/app.js new file mode 100644 index 0000000000..1d2d19b353 --- /dev/null +++ b/crates/switchyard-desktop/ui/app.js @@ -0,0 +1,502 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +'use strict'; +const invoke = window.__TAURI__.core.invoke; +const content = document.querySelector('#content'); +const status = document.querySelector('#status'); +const notification = document.querySelector('#notification'); +const pageErrors = document.querySelector('#page-errors'); +// Each action replaces the previous notification so dismissed results stay dismissed. +let notificationReturn; +function notify(message, tone = 'success', details = []) { + if(notification.hidden&&document.activeElement!==document.body)notificationReturn=document.activeElement; + notification.dataset.tone = tone; + const label = {success:'✓',progress:'In progress:',warning:'Attention:',error:'Error:'}[tone]; + status.textContent = message ? `${label} ${message}` : ''; + const disclosure = document.querySelector('#notification-details'); + disclosure.open = false; + disclosure.hidden = !details.length; + document.querySelector('#notification-detail-text').textContent = details.join('\n'); + notification.hidden = !message; +} +// Background failures remain on the page without replacing the last action result. +function showPageErrors(message) { + pageErrors.textContent = message; + pageErrors.hidden = !message; +} +// Dismissal returns keyboard focus to an available element, even during an operation. +function dismissNotification(event) { + notification.hidden = true; + status.textContent = ''; + const refreshButton = document.querySelector('#refresh'); + if(!event || event.detail===0) (notificationReturn?.isConnected && !notificationReturn.disabled ? notificationReturn : refreshButton.disabled ? document.querySelector('#title') : refreshButton).focus(); +} + +let snapshot, page = 'overview', busy = false, reading = false, lastRefresh = null, stale = false; +const sessionView = {tool:'',account:'',route:'',project:''}; +const accountView = {tool:'codex_cli',name:''}; +const activity = []; +const connectedTools=new Set(); +let sessionOpened=false; +let preferencesDraft; +const pageNames = {overview:'Overview',routes:'Routes',install:'Connect tools',usage:'Usage',sessions:'Sessions',settings:'Settings'}; +let operationQueue = Promise.resolve(),ipcQueue=Promise.resolve(); +// The native controller accepts one operation at a time; queued reads leave the page usable. +function request(command,args){const captured=args===undefined?undefined:structuredClone(args);const pending=ipcQueue.then(()=>invoke(command,captured));ipcQueue=pending.catch(()=>{});return pending;} +const drafts = new Map(), lists = new Map(); +const usageView = {session:'',search:'',exact:null,period:'all',role:'all',turn:'',sort:'newest'}; +const routeView = {selected:'',search:''}; +const installView = {tool:'',account:'',route:'',file:'',mode:'default'}; +function node(tag, text, cls) { const n = document.createElement(tag); if (text !== undefined) n.textContent = text; if (cls) n.className = cls; return n; } +function card(title) { const c = node('div', undefined, 'card'); c.append(node('h3', title)); content.append(c); return c; } +function button(parent, text, fn, primary = false) { const b = node('button', text, primary ? 'primary' : ''); b.onclick = fn; parent.append(b); return b; } +// Wry's macOS delegate has no JavaScript confirm handler, so this dialog handles confirmation. +function confirmAction(title, message, label, action, onCancel) { + const previous=document.activeElement,overlay=node('div',undefined,'modal-backdrop'),dialog=node('div',undefined,'confirm-dialog'); + dialog.setAttribute('role','dialog');dialog.setAttribute('aria-modal','true');dialog.setAttribute('aria-labelledby','confirmation-title');dialog.setAttribute('aria-describedby','confirmation-description'); + const heading=node('h3',title);heading.id='confirmation-title';heading.tabIndex=-1; + const description=node('p',message,'confirmation-description');description.id='confirmation-description'; + dialog.append(heading,description);overlay.append(dialog);document.body.append(overlay); + const background=[...document.body.children].filter(element=>element!==overlay).map(element=>[element,element.inert]); + background.forEach(([element])=>{element.inert=true;}); + const close=(cancelled=true)=>{overlay.remove();background.forEach(([element,inert])=>{element.inert=inert;});if(previous?.isConnected)previous.focus();if(cancelled)onCancel?.();}; + const controls=actions(dialog),cancel=button(controls,'Cancel',close),accept=button(controls,label,()=>{close(false);action();});accept.classList.add('danger'); + dialog.onkeydown=event=>{if(event.key==='Escape'){event.preventDefault();close();}else if(event.key==='Tab'){const items=[cancel,accept],index=items.indexOf(document.activeElement);event.preventDefault();items[index<0?(event.shiftKey?items.length-1:0):(index+(event.shiftKey?-1:1)+items.length)%items.length].focus();}}; + if(message.length>300)heading.focus();else cancel.focus(); +} +function field(parent, title, control) { control.setAttribute('aria-label',title); const l = node('label', title); l.append(control); parent.append(l); return control; } +function select(options, chosen) { const s = node('select'); if(chosen && !options.some(([value])=>value===chosen))options=[...options,[chosen,`${chosen} · unavailable in current data`]]; for (const [value, label] of options) { const o = node('option', label); o.value = value; s.append(o); } if (chosen !== undefined) s.value = chosen; return s; } +function input(value = '', type = 'text') { const i = node('input'); i.type = type; i.value = value; return i; } +function text(parent, value) { parent.append(node('pre', value)); } +function actions(parent) { const a = node('div', undefined, 'actions'); parent.append(a); return a; } +let lockedFields=[]; +function setBusy(value) { + busy=value; + if(value){lockedFields=[...document.activeElement?.closest?.('.card')?.querySelectorAll('input,select')||[]].map(control=>[control,control.disabled]);lockedFields.forEach(([control])=>{control.disabled=true;});} + else{for(const [control,disabled] of lockedFields)control.disabled=disabled||control.dataset.blocked==='true';lockedFields=[];} + document.querySelectorAll('button[data-mutation]').forEach(n=>{n.disabled=value || n.dataset.blocked==='true';}); + document.querySelector('#content').setAttribute?.('aria-busy',String(value)); +} +// Reads and writes use the controller queue; only writes lock mutation controls. +function enqueue(operation) { + operationQueue=operationQueue.then(operation).catch(error=>notify(String(error),'error')); + return operationQueue; +} +async function readAction(action) { + return request('action',{action}); +} +async function run(action, refreshAfter = true) { + if(['editor','preview_install','preview_restore','preview_launch','preview_update','preview_route','preview_account_archive','preview_session_cleanup','preview_route_change'].includes(action.kind)) { + try { return await readAction(action); } catch(error) { showPageErrors(String(error)); return; } + } + if(busy)return; + const progress={apply:'Saving the route and restarting Switchyard…',remove:'Removing the route and restarting Switchyard…',restart:'Restarting Switchyard…',install:'Applying routing settings…',restore:'Restoring settings…',launch:'Opening Terminal…',models:'Loading models…'}; + const previous=document.activeElement; + setBusy(true);notificationReturn=previous?.tagName==='BODY'?document.querySelector('#title'):previous;notify(progress[action.kind]||'Working…','progress'); + try { + const result=await request('action',{action}); + notify(result.message,result.data?.warning?'warning':'success',result.data?.details||[]); + if(action.kind==='install')connectedTools.add(action.tool);if(action.kind==='launch')sessionOpened=true; + activity.unshift({message:result.message,time:new Date().toLocaleTimeString()});activity.splice(8); + if(refreshAfter)await refresh(true); + return result; + } catch(error) {notify(String(error),'error');} + finally {setBusy(false);if(previous?.isConnected&&!previous.disabled)previous.focus({preventScroll:true});else document.querySelector('#title').focus();} +} +function capturePageState() { + const focused=document.activeElement; + return {label:focused?.getAttribute?.('aria-label'),id:focused?.id,start:focused?.selectionStart,end:focused?.selectionEnd, + open:[...content.querySelectorAll('details[open]')].map(n=>n.querySelector('summary')?.textContent),scroll:window.scrollY||0, + regions:[...content.querySelectorAll('.scroll,.usage-table-scroll,.route-picker')].map(n=>[n.scrollTop,n.scrollLeft])}; +} +function restorePageState(saved) { + content.querySelectorAll('details').forEach(n=>{n.open=saved.open.includes(n.querySelector('summary')?.textContent);}); + const control=[...content.querySelectorAll('input,select,button')].find(n=>saved.id?n.id===saved.id:saved.label&&n.getAttribute('aria-label')===saved.label); + if(control && !control.disabled){control.focus({preventScroll:true});if(saved.start!==null && saved.start!==undefined && control.setSelectionRange && !['number','date'].includes(control.type))control.setSelectionRange(saved.start,saved.end);} + content.querySelectorAll('.scroll,.usage-table-scroll,.route-picker').forEach((n,i)=>{if(saved.regions[i])[n.scrollTop,n.scrollLeft]=saved.regions[i];}); + window.scrollTo?.(0,saved.scroll); +} +async function refresh(force = true) { + if(reading)return; + reading=true; + const saved=snapshot?capturePageState():null; + document.querySelector('#refresh').setAttribute?.('aria-busy','true'); + try { + const latest=await request('snapshot'); + // A menu, dialog, or action may start while IPC waits; the next poll can apply a fresh snapshot. + if(!force&&(busy||document.querySelector('[role="dialog"]')||document.activeElement?.tagName==='SELECT'))return; + snapshot=latest;lastRefresh=new Date();stale=false; + if(force || ['overview','usage','settings'].includes(page)) {render();if(saved)restorePageState(saved);} + showPageErrors((snapshot.errors||[]).join('\n'));showFreshness(); + } catch(error) {stale=true;showPageErrors(`Could not refresh the page: ${error}`);showFreshness();if(!snapshot)renderRecovery(String(error));} + finally {reading=false;document.querySelector('#refresh').setAttribute?.('aria-busy','false');} +} +function showFreshness() { + const health=document.querySelector('#server-state');if(health&&snapshot?.metrics){health.replaceChildren();const state=snapshot.data_state?.health?.state||(snapshot.metrics.running?'running':'unreachable');health.hidden=state==='running'&&!stale;if(!health.hidden){health.append(node('span',stale?'Server status is stale. Check again before relying on it.':`Server ${state}. ${snapshot.data_state?.health?.reason||snapshot.metrics.server_url}`));button(health,'Check again',()=>refresh(false));mutation(health,'Retry restart',()=>run({kind:'restart'}));button(health,'Open recovery',()=>navigate('settings'));}} + const label=document.querySelector('#freshness'); + if(label)label.textContent=lastRefresh?`${stale?'Stale · last successful check':'Checked'} ${lastRefresh.toLocaleTimeString()}`:'Loading…'; + const connection=document.querySelector('.connection-state');if(stale&&connection){connection.textContent='Unavailable · last check failed';connection.classList.remove('connected');} +} +function navigate(next) {page=next;render();document.querySelector('#title').focus();window.scrollTo?.(0,0);} +function renderRecovery(error) { + content.replaceChildren();const c=card('App settings need attention');c.append(node('p',error)); + button(actions(c),'Open app settings',()=>run({kind:'open_settings'},false));button(actions(c),'Retry',()=>refresh()); +} +function blocked(control, value) {control.dataset.blocked=String(value);control.disabled=(control.dataset.mutation==='true'&&busy)||value;} +function mutation(parent,label,fn,primary=false) {const b=button(parent,label,fn,primary);b.dataset.mutation='true';b.disabled=busy;return b;} +function fieldError(control,message) { + const id=`error-${control.getAttribute('aria-label').replace(/[^a-z0-9]/gi,'-')}`; + let note=control.parentElement.querySelector('.field-error');if(!note){note=node('small',undefined,'field-error');note.id=id;control.parentElement.append(note);} + note.textContent=message;note.hidden=!message;control.setAttribute('aria-describedby',id);control.setAttribute('aria-invalid',String(Boolean(message))); + return !message; +} +function routeMatches(route, query) { return [route.id,route.label,snapshot.algorithms.find(a=>a.kind===route.kind)?.title,...route.choices.flatMap(c=>[c.model,c.client,snapshot.clients.find(e=>e.name===c.client)?.host])].join(' ').toLowerCase().includes(query.toLowerCase()); } +function routeField(c,chosen='',tool='') { + const wrapper=node('div');c.append(wrapper); + const route=field(wrapper,'Route',select(snapshot.routes.map(r=>[r.key,`${r.id} · ${snapshot.algorithms.find(a=>a.kind===r.kind)?.title||r.kind}${tool&&r.compatibility?.[tool]?' · unavailable':''}`]),chosen||undefined)); + if(snapshot.routes.length>8){ + const search=field(wrapper,'Find route',input());search.placeholder='Name, model, or endpoint'; + search.oninput=()=>{const previous=route.value;route.replaceChildren();for(const r of snapshot.routes.filter(r=>routeMatches(r,search.value))){const option=node('option',r.id);option.value=r.key;route.append(option);}if([...route.options].some(o=>o.value===previous))route.value=previous;else {const placeholder=node('option','Choose a matching route');placeholder.value='';route.prepend(placeholder);route.value='';route.onchange?.();}}; + } + return route; +} +function routeArgs(s) { const r=snapshot.routes.find(r=>r.key===s.value); if (!r) throw new Error('Choose a route first.'); return {route:r.key,id:r.id}; } +let generation; +function render() { + if (!snapshot) return; + generation=snapshot.generation; + disposeCharts(); + content.replaceChildren(); + document.querySelector('#title').textContent=pageNames[page]; + document.querySelectorAll('[data-page]').forEach(b=>{b.classList.toggle('active',b.dataset.page===page);if(b.dataset.page===page)b.setAttribute('aria-current','page');else b.removeAttribute('aria-current');}); + if (page==='overview') { + renderOverview(); + } else if (page==='routes') { + renderRouteBrowser(); + + } else if (page==='install') { + const intro=card('Connect your coding tools'); + intro.append(node('p','Choose a Switchyard route for each coding tool, review the settings below, then apply routing settings.')); + const help=node('details');help.append(node('summary','How accounts and settings work'));help.append(node('p','Subscription routes use the coding tool’s login. API routes use credentials configured on the Switchyard server. Keys entered in Routes only load model lists.'));help.append(node('p','Installation backs up the original settings. Shell variables and project settings may override these defaults.'));intro.append(help); + const tool=field(intro,'Coding tool',select(snapshot.tools.map(t=>[t.tool,`${t.label} · ${t.available?'Detected':'Not detected'}`]),installView.tool||snapshot.tools[0].tool)); + installView.tool=tool.value; + const panel=node('div');content.append(panel); + const show=()=>{installView.tool=tool.value;installView.account='';installView.file='';installView.mode='default';panel.replaceChildren();renderInstall(snapshot.tools.find(t=>t.tool===tool.value),panel);}; + tool.onchange=show;renderInstall(snapshot.tools.find(t=>t.tool===tool.value),panel); + renderAccounts(); + } else if(page==='usage')renderUsage(); + else if(page==='sessions'){renderSessions();renderAccounts();} + else if(page==='settings')renderSettings(); + showFreshness(); +} +function usageUnavailable() {return snapshot.data_state?.usage?.state==='unavailable'||snapshot.data_state?.history?.state==='unavailable'||snapshot.usage_status==='unavailable'||snapshot.history_status==='unavailable'||(snapshot.errors||[]).some(e=>/read.*(usage|history|routing log)|usage.*read/i.test(e));} +// The final tie-breaker keeps calls with later log positions first across refreshes. +function sortedCalls(entries, order) { + const compareText=(a,b)=>ab?1:0; + return entries.map((entry,index)=>({entry,index,time:Date.parse(entry.ts)})).sort((a,b)=>{ + const validA=Number.isFinite(a.time),validB=Number.isFinite(b.time); + let time=0; + if(validA!==validB)time=validA?-1:1; + else if(validA)time=order==='oldest'?a.time-b.time:b.time-a.time; + const model=compareText(a.entry.model||'',b.entry.model||''); + const tokens=(b.entry.prompt_tokens+b.entry.completion_tokens)-(a.entry.prompt_tokens+a.entry.completion_tokens); + let primary=0; + if(order==='model')primary=model; + else if(order==='tokens')primary=tokens; + return primary||time||model||b.index-a.index; + }).map(row=>row.entry); +} +function renderUsage() { + const c=card('Model calls');c.append(node('p','Each row shows a completed model call. A turn may include several calls. Choose the order below.')); + const entries=snapshot.entries,dates=entries.map(e=>new Date(e.ts)).filter(d=>!Number.isNaN(d.getTime())).sort((a,b)=>a-b); + c.append(node('p',`${entries.length} retained calls${snapshot.limited?' · limited to 8 MiB / 5,000 records':''} · ${snapshot.skipped} unreadable recent records. ${dates.length?`${dates[0].toLocaleString()} – ${dates.at(-1).toLocaleString()}`:'No retained timestamps.'}`,'muted')); + if(usageView.exact){const pill=node('div',undefined,'usage-filter');pill.append(node('span',`${usageView.exact.group}: ${usageView.exact.id||'Not recorded'}`));button(pill,'Clear comparison',()=>{usageView.exact=null;render();});c.append(pill);} + const filters=node('div',undefined,'field-grid');c.append(filters); + const period=field(filters,'Usage period',select([['all','All recent history'],['week','Seven local calendar days'],['today','Today']],usageView.period)); + const session=field(filters,'Session',select([['','All sessions'],['__missing','Session not recorded'],...snapshot.sessions.map(s=>[s,s])],usageView.session)); + const search=field(filters,'Search turn, model, route, or session',input(usageView.search)); + const role=field(filters,'Call role',select([['all','All calls'],['answer','Answer calls'],['overhead','Routing overhead']],usageView.role)); + const sort=field(filters,'Sort calls',select([['newest','Time: newest first'],['oldest','Time: oldest first'],['model','Model: A to Z'],['tokens','Total tokens: highest first']],usageView.sort)); + const tally=node('p',undefined,'muted');c.append(tally); + const a=actions(c);button(a,'Clear all filters',()=>{Object.assign(usageView,{session:'',search:'',exact:null,role:'all',turn:'',period:'all'});render();}); + button(a,'Copy filtered calls',()=>copyText(JSON.stringify(filteredCalls(),null,2))); + const container=node('div',undefined,'scroll');container.tabIndex=0;container.setAttribute('role','region');container.setAttribute('aria-label','Model calls table');c.append(container); + function filteredCalls(){return sortedCalls(snapshot.entries,usageView.sort).filter(e=>{ + const exact=usageView.exact;if(exact&&(e[{model:'model',route:'route_id',session:'session_id'}[exact.group]]||'')!==exact.id)return false; + if(!inPeriod(e.ts,usageView.period,snapshot.today))return false; + if(usageView.session==='__missing'?Boolean(e.session_id):usageView.session&&e.session_id!==usageView.session)return false; + if(usageView.turn&&e.turn_id!==usageView.turn)return false; + if(usageView.role!=='all'&&(usageView.role==='overhead')!==(e.tier==='classifier'))return false; + return [e.model,e.turn_id,e.route_id,e.session_id].join(' ').toLowerCase().includes(usageView.search.toLowerCase()); + });} + const draw=()=>{ + usageView.session=session.value;usageView.search=search.value;usageView.period=period.value;usageView.role=role.value;usageView.sort=sort.value; + container.replaceChildren();const rows=filteredCalls();tally.textContent=`${rows.length} matching of ${entries.length} retained calls${usageView.turn?` · Turn: ${usageView.turn}`:''}`; + if(usageUnavailable()){container.append(node('p','Usage unavailable. Check the routing log in Settings and retry.','empty-state'));button(container,'Retry',()=>refresh());return;} + const table=node('table'),thead=node('thead'),head=node('tr'),tbody=node('tbody'); + for(const label of ['Time','Model / role','Session / turn','Route','Uncached input','Cached input','Output']){const th=node('th',label);th.scope='col';head.append(th);}thead.append(head);table.append(thead,tbody); + for(const e of rows){const row=node('tr'),time=node('td'),date=new Date(e.ts);time.title=e.ts;time.textContent=Number.isNaN(date.getTime())?e.ts:date.toLocaleString(); + const model=node('td');model.append(node('span',e.model,'table-primary'),node('small',e.tier==='classifier'?'Routing overhead':'Answer call','table-secondary')); + const ids=node('td');if(e.session_id)button(ids,e.session_id,()=>{usageView.session=e.session_id;render();});else ids.append(node('span','Session not recorded'));if(e.turn_id){button(ids,e.turn_id,()=>{usageView.turn=e.turn_id;draw();});button(ids,'Copy turn ID',()=>copyText(e.turn_id));}else ids.append(node('small','Turn not recorded')); + row.append(time,model,ids,node('td',e.route_id||'Route not recorded'));for(const value of [Math.max(0,e.prompt_tokens-e.cached_tokens),e.cached_tokens,e.completion_tokens])row.append(node('td',value.toLocaleString()));tbody.append(row); + }container.append(table); + if(!rows.length)container.append(node('p',entries.length?'No calls match these filters. A selected session may be outside recent history.':'No calls recorded yet. Start a session, then return here.','empty-state')); + if(usageView.exact&&snapshot.limited)container.prepend(node('p','Comparison totals cover the full log; this table only includes the retained range shown above.','coverage-note')); + };for(const control of [period,session,role,sort])control.onchange=draw;search.oninput=draw;draw(); +} +async function choosePath(control,directory,changed){try{const result=await request('choose_path',{directory});const picked=typeof result==='string'?result:result?.data?.path||result?.path;if(picked&&control.isConnected){control.value=picked;changed();}}catch(error){notify(`Could not choose a ${directory?'folder':'file'}: ${error}`,'error');}} +async function copyText(value) {try{await navigator.clipboard.writeText(value);notify('Copied.');}catch{notify('Copy failed. Select and copy the text manually.','error');}} +function sessionPreviewText(data){return [`Repository: ${data.repository}`,`Starting folder: ${data.relative_directory?`${data.relative_directory} within the new worktree`:'New worktree root'}`,`Branch: ${data.branch||'Detached HEAD'}`,`Revision: ${data.revision}`,`Route: ${data.route}`,`Account: ${data.account_directory||'Current login'}`,data.dirty?'Your checkout has uncommitted changes.':'',data.notice,data.settings_notice].filter(Boolean).join('\n\n');} +function renderSessions() { + const c=card('New session');c.append(node('p','Create a Git worktree from committed HEAD and open the coding tool in Terminal. Uncommitted changes stay in the original checkout.')); + const tools=snapshot.tools.filter(t=>['codex_cli','claude','pi'].includes(t.tool)); + const tool=field(c,'Session coding tool',select(tools.map(t=>[t.tool,`${t.label}${t.available?'':' · not detected'}`]),sessionView.tool||tools[0]?.tool));sessionView.tool=tool.value; + const account=field(c,'Session account',select([['','Current login'],...(tools.find(t=>t.tool===tool.value)?.accounts||[]).map(n=>[n,n])],sessionView.account)); + const route=routeField(c,sessionView.route,tool.value); + const project=field(c,'Project folder',input(sessionView.project));project.placeholder='Choose a Git checkout folder'; + const info=node('p',undefined,'muted');c.append(info);const preview=node('div');preview.setAttribute('role','status');preview.setAttribute('aria-live','polite');c.append(preview);let plan; + const a=actions(c),review=button(a,'Review session',async()=>{const reviewed=args();preview.setAttribute('aria-busy','true');preview.textContent='Checking the project and session settings…';try{const r=await readAction({kind:'preview_launch',...reviewed});if(JSON.stringify(reviewed)!==JSON.stringify(args())){preview.textContent='Session inputs changed. Review them again.';return;}plan={reply:r,args:structuredClone(reviewed)};preview.replaceChildren(node('pre',sessionPreviewText(r.data)));r.message=sessionPreviewText(r.data);blocked(launch,false);}catch(error){fieldError(project,String(error));preview.textContent='Session preview unavailable. Correct the project or selected settings and retry.';}finally{preview.setAttribute('aria-busy','false');}}),launch=mutation(a,'Open Terminal…',()=>{if(!plan)return;const reviewed=structuredClone(plan.args),reply=plan.reply;confirmAction('Start this session?',reply.message,'Open Terminal',()=>run({kind:'launch',...reviewed,preview_token:reply.data.preview_token}));},true); + const args=()=>({tool:tool.value,account:account.value||null,...routeArgs(route),project:project.value}); + const changed=()=>{Object.assign(sessionView,{tool:tool.value,account:account.value,route:route.value,project:project.value});plan=null;preview.replaceChildren();blocked(launch,true);const t=tools.find(t=>t.tool===tool.value);const incompatible=snapshot.routes.find(r=>r.key===route.value)?.compatibility?.[tool.value];fieldError(project,'');blocked(review,!t?.available||!route.value||!project.value||Boolean(incompatible));if(incompatible)preview.append(node('p',incompatible,'field-error'));info.textContent=account.value?`Saved account: ${account.value}. ${t?.account_details?.find(a=>a.name===account.value)?.path||''}. Login status is unknown until the coding tool confirms it.`:t?.status||'Select a coding tool.';if(tool.value==='pi')info.append(node('p','Pi uses isolated session settings. Personal settings and extensions are not copied.'));}; + button(a,'Choose project folder…',()=>choosePath(project,true,changed)); + tool.onchange=()=>{sessionView.account='';sessionView.tool=tool.value;render();};account.onchange=changed;route.onchange=changed;project.oninput=changed;changed(); + if(!snapshot.routes.length){c.append(node('p','Add a route before starting a session.'));button(a,'Choose a route',()=>navigate('routes'));} + c.append(node('p','Codex app workspaces are managed in the Codex app. Supported command line tools appear above.','muted')); + const inventory=card('Recorded sessions');inventory.append(node('p','Recorded calls do not establish that a process is running.')); + if(snapshot.session_inventory_error)inventory.append(node('p',snapshot.session_inventory_error,'field-error')); + for(const item of snapshot.session_inventory||[]){const row=node('div',undefined,'route-row'),last=snapshot.entries.filter(e=>e.session_id===item.id).at(-1);row.append(node('h4',item.id),node('p',[item.repository,item.branch,item.route,item.worktree,item.created_at,last?`Last recorded call: ${last.ts}`:'No recent recorded call'].filter(Boolean).join(' · ')));button(actions(row),'Copy path',()=>copyText(item.worktree));button(actions(row),'Open folder',()=>run({kind:'open_session',id:item.id},false));mutation(actions(row),'Review cleanup…',async()=>{const result=await run({kind:'preview_session_cleanup',id:item.id},false);if(!result)return;const data=result.data;confirmAction('Remove session worktree?',`${data.cleanup_notice}\n\nDirectory: ${data.worktree}\nBranch: ${data.current_branch}\nRevision: ${data.current_revision}\n${data.dirty?'This worktree has uncommitted changes. Cleanup will refuse to remove it.':'The worktree has no uncommitted changes.'}`,'Remove session',()=>run({kind:'remove_session',id:item.id,preview_token:data.preview_token}));});inventory.append(row);} + if(!(snapshot.session_inventory||[]).length){inventory.append(node('p','Switchyard has not recorded any managed sessions.'));for(const id of snapshot.sessions){button(actions(inventory),id,()=>{usageView.session=id;navigate('usage');});}} +} +function updatePreviewText(data){return [`Source checkout: ${data.source}`,`Branch: ${data.branch||'Detached HEAD'}`,`Revision: ${data.revision}`,`Local changes: ${data.changes||'None'}`,Array.isArray(data.plan)?data.plan.join('\n'):data.plan].filter(Boolean).join('\n\n');} +function renderSettings() { + const c=card('Server and app');const paths=snapshot.settings||{}; + for(const [label,value] of Object.entries(paths))if(typeof value==='string')c.append(node('p',`${label.replaceAll('_',' ')}: ${value}`)); + if(snapshot.version)c.append(node('p',`Installed version: ${snapshot.version}`)); + const a=actions(c);button(a,'Copy diagnostics',()=>copyText(JSON.stringify({app:'Switchyard desktop',version:snapshot.version||'Not reported',last_check:lastRefresh?.toISOString(),stale,server:snapshot.metrics,paths:Object.fromEntries(Object.entries(snapshot.settings||{}).filter(([key,value])=>typeof value==='string'&&['path','config_file','routing_log'].includes(key))),routes:snapshot.routes.map(r=>r.id),tools:snapshot.tools.map(t=>({label:t.label,available:t.available})),update:snapshot.update_status?.state||'Unknown'},null,2)));for(const [label,kind] of [['Open server config','open_config'],['Open app settings','open_settings'],['Check config','check_config'],['Restart and check health','restart'],['Open server logs','open_logs']])mutation(a,label,()=>run({kind})); + c.append(node('p','Set baseline model, model prices, server address, and refresh interval in app settings. Prices use dollars per million tokens.')); + const update=card('Update from source');update.append(node('p','Review the source checkout and service restarts before building.')); + if(snapshot.update_status){const state=snapshot.update_status;update.append(node('p',`${state.state||'No update running'}${state.stage?` · ${state.stage}`:''}: ${state.message||''}`));} + button(actions(update),'Review update…',async()=>{const result=await run({kind:'preview_update'},false);if(!result)return;const unsaved=[...drafts.values()].some(d=>d.dirty);confirmAction('Build and reinstall?',`${updatePreviewText(result.data)}${unsaved?'\n\nYou have unsaved route edits. Cancel and save or discard them before updating.':''}`,'Build and reinstall',()=>{if(unsaved){notify('Save or discard route drafts before updating.','warning');return;}run({kind:'update',preview_token:result.data.preview_token});});}); + button(actions(update),'Open update log',()=>run({kind:'open_update_log'},false)); + renderPreferences(); + const display=card('Display');const zoom=field(display,'Text size',select([['100','100%'],['125','125%'],['150','150%'],['200','200%']],String(Math.round(Number(document.documentElement.style.zoom||1)*100))));zoom.onchange=()=>{document.documentElement.style.zoom=String(Number(zoom.value)/100);document.documentElement.style.setProperty('--ui-scale',String(Number(zoom.value)/100));document.body.classList.toggle('enlarged',Number(zoom.value)>=150);}; + const recent=card('Recent actions');for(const item of activity)recent.append(node('p',`${item.time} · ${item.message}`));if(!activity.length)recent.append(node('p','Actions from this app session appear here.')); + c.append(node('p','Closing the window keeps the app in the menu bar and the server running. Quit Switchyard exits the app; Restart controls the server.'));text(c,'Terminal: switchyard-desktop --tui'); +} + +function renderPreferences() { + const c=card('Usage estimates and refresh'); + if(!preferencesDraft)preferencesDraft={baseline:snapshot.baseline_model,refresh:String(snapshot.refresh_seconds),prices:Object.entries(snapshot.settings?.prices||{}).map(([model,p])=>({model,input:String(p.input_per_mtok),cached:p.cached_input_per_mtok===null||p.cached_input_per_mtok===undefined?'':String(p.cached_input_per_mtok),output:String(p.output_per_mtok)}))}; + const draft=preferencesDraft; + const baseline=field(c,'Baseline model ID',input(draft.baseline)),refreshInterval=field(c,'Refresh interval in seconds',input(draft.refresh,'number'));refreshInterval.min='1';refreshInterval.step='1'; + c.append(node('p','Rates are dollars per million tokens. Leave cached input empty to use the input rate. Estimates need prices for every recorded model and the baseline.')); + const rows=node('div');c.append(rows); + const draw=()=>{rows.replaceChildren();for(const [index,price] of draft.prices.entries()){const row=node('div',undefined,'price-row field-grid');rows.append(row);for(const [key,label] of [['model','Price model ID'],['input','Uncached input rate'],['cached','Cached input rate'],['output','Output rate']]){const control=field(row,`${label} ${index+1}`,input(price[key],key==='model'?'text':'number'));if(key!=='model'){control.min='0';control.step='any';}control.oninput=()=>{price[key]=control.value;fieldError(control,'');};}button(actions(row),'Remove price',()=>{draft.prices.splice(index,1);draw();});}};draw(); + button(actions(c),'Add model price',()=>{draft.prices.push({model:'',input:'',cached:'',output:''});draw();}); + baseline.oninput=()=>{draft.baseline=baseline.value;fieldError(baseline,'');};refreshInterval.oninput=()=>{draft.refresh=refreshInterval.value;fieldError(refreshInterval,'');}; + mutation(actions(c),'Save app settings',async()=>{ + let valid=fieldError(baseline,!draft.baseline.trim()||/[\n\r]/.test(draft.baseline)?'Enter a baseline model ID without line breaks.':''); + valid=fieldError(refreshInterval,!Number.isSafeInteger(Number(draft.refresh))||Number(draft.refresh)<1?'Enter a whole number of at least one second.':'')&&valid; + const prices={},seen=new Set();for(const [index,price] of draft.prices.entries()){const controls=rows.children[index].querySelectorAll('input');const modelError=!price.model.trim()?'Enter a model ID.':seen.has(price.model)?'This model already has a price.':'';valid=fieldError(controls[0],modelError)&&valid;seen.add(price.model); + for(const [offset,key] of [[1,'input'],[2,'cached'],[3,'output']]){const missing=key!=='cached'&&!price[key].trim();const invalid=missing||price[key]!==''&&(!Number.isFinite(Number(price[key]))||Number(price[key])<0);valid=fieldError(controls[offset],invalid?'Enter a finite rate of zero or more.':'')&&valid;} + prices[price.model]={input_per_mtok:Number(price.input),cached_input_per_mtok:price.cached===''?null:Number(price.cached),output_per_mtok:Number(price.output)}; + } + if(!valid){c.querySelector('[aria-invalid="true"]')?.focus();return;} + const result=await run({kind:'save_preferences',baseline_model:draft.baseline,refresh_seconds:Number(draft.refresh),prices});if(result){preferencesDraft=undefined;render();} + },true); +} + +function draftKey(route) {return `${snapshot.settings?.config_file||generation}:${route.key}`;} +function routeSummary(route){return route.choices.map(c=>`${c.client} / ${c.model}`).join(' → ');} +function renderRouteBrowser() { + const c=card('Route library'),a=actions(c);button(a,'Create route…',()=>routeIdentity('create_route')); + if([...drafts.keys()].some(k=>!k.startsWith(`${snapshot.settings?.config_file||generation}:`)))c.append(node('p','Drafts from another config file are retained in this app session. Return to that config to finish or discard them.','warning-note')); + if(!snapshot.routes.length){c.append(node('p',(snapshot.errors||[]).length?'Routes could not be loaded. Open the server config to fix the error.':'No routes configured. Create a route or open the server config.'));button(a,'Open server config',()=>run({kind:'open_config'},false));return;} + const search=field(c,'Find a route by name, routing method, endpoint, or model',input(routeView.search)); + const count=node('p',undefined,'muted'),layout=node('div',undefined,'route-workspace'),list=node('div',undefined,'route-picker'),detail=node('div',undefined,'route-detail'); + c.append(count,layout);layout.append(list,detail); + const draw=()=>{ + routeView.search=search.value;const routes=snapshot.routes.filter(r=>routeMatches(r,search.value));count.textContent=`${routes.length} of ${snapshot.routes.length} routes`;list.replaceChildren();detail.replaceChildren(); + if(!snapshot.routes.some(r=>r.key===routeView.selected))routeView.selected=routes[0]?.key||''; + for(const route of routes){const b=button(list,'',()=>{routeView.selected=route.key;draw();list.querySelector(`[data-route="${CSS.escape(route.key)}"]`)?.focus();});b.dataset.route=route.key;b.classList.toggle('selected',route.key===routeView.selected);b.setAttribute('aria-pressed',String(route.key===routeView.selected));b.append(node('span',route.id,'route-name'),node('small',snapshot.algorithms.find(a=>a.kind===route.kind)?.title||route.kind),node('small',routeSummary(route)));const badge=node('small','Unsaved edits','draft-badge');badge.hidden=!drafts.get(draftKey(route))?.dirty;b.append(badge);} + if(!routes.length){list.append(node('p','No routes match your search.','empty-state'));button(list,'Clear search',()=>{search.value='';draw();});return;} + const route=routes.find(r=>r.key===routeView.selected); + if(route)renderRoute(route,detail);else detail.append(node('p','Selected route is outside this filter. Clear the search to return to it.','empty-state')); + };search.oninput=draw;draw(); +} +function routeIdentity(kind,route) { + const overlay=node('div',undefined,'modal-backdrop'),form=node('form',undefined,'confirm-dialog');form.setAttribute('role','dialog');form.setAttribute('aria-modal','true');form.setAttribute('aria-label',kind==='create_route'?'Create route':kind==='rename_route'?'Rename route':'Duplicate route');overlay.append(form);document.body.append(overlay); + const siblings=[...document.body.children].filter(n=>n!==overlay).map(n=>[n,n.inert]);siblings.forEach(([n])=>{n.inert=true;});const previous=document.activeElement; + const close=()=>{overlay.remove();siblings.forEach(([n,inert])=>{n.inert=inert;});if(previous?.isConnected)previous.focus();else document.querySelector('#title').focus();}; + form.append(node('h3',kind==='create_route'?'Create route':kind==='rename_route'?'Rename route':'Duplicate route')); + const id=field(form,'Public model ID',input(kind==='rename_route'?route.id:'')),name=kind==='rename_route'?null:field(form,'Config route name',input());form.append(node('small','The public model ID is the name coding tools request. The config name uses 1–64 letters, digits, hyphens, or underscores.'));let client,model; + if(kind==='create_route'){client=field(form,'Endpoint',select(snapshot.clients.map(c=>[c.name,`${c.name} (${c.host})`])));model=field(form,'Model ID',input());} + const a=actions(form);button(a,'Cancel',close).type='button';const save=button(a,'Review changes…',()=>{},true);save.type='submit'; + form.onsubmit=async event=>{ + event.preventDefault();let valid=fieldError(id,!id.value.trim()||/[\n\r]/.test(id.value)?'Enter a public model ID without line breaks.':snapshot.routes.some(r=>r.id===id.value&&!(kind==='rename_route'&&r.key===route?.key))?'That public model ID already exists.':''); + if(name)valid=fieldError(name,!/^[A-Za-z0-9_-]{1,64}$/.test(name.value)?'Use 1–64 letters, digits, hyphens, or underscores.':snapshot.routes.some(r=>r.key===name.value)?'That config name already exists.':'')&&valid; + if(model)valid=fieldError(model,!model.value.trim()?'Enter a model ID.':'')&&valid;if(!valid){form.querySelector('[aria-invalid="true"]')?.focus();return;} + const args={generation,operation:kind.replace('_route',''),route:route?.key||'',name:name?.value||'',id:id.value,algorithm:client?'passthrough':null,choices:client?[{client:client.value,model:model.value}]:[]}; + save.disabled=true;const preview=await run({kind:'preview_route_change',...args},false);save.disabled=false;if(!preview)return; + if(id.value!==args.id||(name&&name.value!==args.name)||(model&&model.value!==args.choices[0].model)||(client&&client.value!==args.choices[0].client)){notify('The form changed during review. Review the current values again.','warning');return;} + confirmAction('Save this route change?', [`Changed file: ${preview.data.file}`,...preview.data.notes,'Switchyard validates the config, saves a backup, and restarts the server.'].join('\n\n'),'Save and restart',async()=>{form.querySelectorAll('input,select,button').forEach(control=>{control.disabled=true;});const result=await run({kind:'route_change',...args,preview_token:preview.data.preview_token});if(result)close();else form.querySelectorAll('input,select,button').forEach(control=>{control.disabled=false;});}); + }; + + form.onkeydown=event=>{if(event.key==='Escape'){event.preventDefault();close();}if(event.key==='Tab'){const controls=[...form.querySelectorAll('input,select,button')],i=controls.indexOf(document.activeElement);if(event.shiftKey&&i===0){event.preventDefault();controls.at(-1).focus();}else if(!event.shiftKey&&i===controls.length-1){event.preventDefault();controls[0].focus();}}};id.focus(); +} +// A preview belongs to its exact inputs; obsolete replies never enable Apply. +function renderInstall(tool,parent) { + const c=card(tool.label);parent.append(c);c.dataset.tool=tool.tool; + c.append(node('p',tool.available?'Coding tool detected.':'Coding tool not detected. You can prepare settings and install the tool later.','muted')); + c.append(node('p',{codex_cli:'Choose the Codex CLI profile file to change.',codex_app:'Detected user settings change defaults shared by the Codex app and CLI. Custom files must be loaded by the coding tool.',claude:'Start a new Claude Code session after changing its settings.',pi:'Pi uses models.json and settings.json together. Model capabilities and prices are presets; check them for your chosen route.'}[tool.tool])); + const fields=node('div',undefined,'field-grid');c.append(fields); + const mode=field(fields,'Destination',select([['default','Detected settings'],['account','Saved account'],['custom','Custom file']],installView.mode)); + const account=field(fields,'Saved account',select(tool.accounts.map(name=>[name,name]),installView.account)); + const file=field(fields,tool.tool==='pi'?'Custom models.json path':'Custom settings file path',input(installView.file));file.placeholder=tool.files?.[0]||'/absolute/path/to/settings'; + const chooseFile=button(c,'Choose settings file…',()=>choosePath(file,false,update)); + const route=routeField(fields,installView.route,tool.tool); + const comparison=node('div',undefined,'install-diff'),authentication=node('p',undefined,'install-auth'),progress=node('p',undefined,'install-progress');progress.setAttribute('role','status');progress.setAttribute('aria-live','polite');c.append(comparison,authentication,progress); + const target=()=>({tool:tool.tool,account:mode.value==='account'?account.value||null:null,settings_file:mode.value==='custom'?file.value||null:null}); + let plan,revision=0;const a=actions(c); + const install=mutation(a,'Apply routing…',()=>{if(!plan)return;const selected=plan;confirmAction(`Apply routing to ${tool.label}?`,`${selected.data.proposed}\n\n${selected.data.authentication}\n\nFiles: ${[...new Set(selected.data.changes.map(change=>change.file))].join(', ')}. Switchyard backs up the originals and preserves unrelated settings.`, 'Apply routing',async()=>{if(!c.isConnected||selected!==plan)return;progress.textContent='Saving settings…';const result=await run(selected.action,false);progress.textContent='';if(result)update();});},true); + const restore=mutation(a,'Review restore…',async()=>{ + blocked(install,true);plan=null;progress.textContent='Checking backup…'; + // The preview and confirmation must use the same destination revision. + const selectedTarget=target(),reviewedRevision=revision; + const result=await run({kind:'preview_restore',...selectedTarget},false); + if(!c.isConnected||reviewedRevision!==revision||JSON.stringify(selectedTarget)!==JSON.stringify(target()))return; + progress.textContent='';if(!result)return; + const data=result.data; + confirmAction('Restore coding-tool settings?',[data.warning,...(data.files||[]).map(file=>`${file.action}: ${file.file}${file.backup?`\nOriginal backup: ${file.backup}`:''}`)].join('\n\n'),'Restore settings',async()=>{if(!c.isConnected||reviewedRevision!==revision||JSON.stringify(selectedTarget)!==JSON.stringify(target()))return;progress.textContent='Restoring settings…';await run({kind:'restore',...selectedTarget,preview_token:data.preview_token},false);progress.textContent='';update();},update); + }); + const update=()=>{ + Object.assign(installView,{mode:mode.value,account:account.value,route:route.value,file:file.value}); + account.parentElement.hidden=mode.value!=='account';file.parentElement.hidden=mode.value!=='custom';chooseFile.hidden=mode.value!=='custom'; + const requested=++revision;plan=null;blocked(install,true);install.textContent='Checking settings…';comparison.setAttribute('aria-busy','true');comparison.replaceChildren(node('p','Loading settings preview…'));authentication.textContent='';progress.textContent='Checking preview…'; + const invalid=mode.value==='custom'&&(!file.value||/[\r\n]/.test(file.value));fieldError(file,invalid?'Enter an absolute settings file path without line breaks.':''); + if(invalid||mode.value==='account'&&!account.value){comparison.replaceChildren(node('p',invalid?'Enter an absolute settings file path.':'No saved account is available. Sign in below or choose Detected settings.'));install.textContent='Preview required';comparison.setAttribute('aria-busy','false');progress.textContent='';blocked(restore,true);return;} + blocked(restore,true);restore.title=''; + const incompatible=snapshot.routes.find(r=>r.key===route.value)?.compatibility?.[tool.tool]; + // A backup can be restored without a route, so check its destination before Apply's route gates. + const requestedTarget=target(); + enqueue(async()=>{ + if(!c.isConnected||requested!==revision)return; + try{ + let backupReady=false;try{await readAction({kind:'preview_restore',...requestedTarget});backupReady=true;}catch(error){if(c.isConnected&&requested===revision)restore.title=String(error);} + if(!c.isConnected||requested!==revision)return;blocked(restore,!backupReady); + if(incompatible){comparison.replaceChildren(node('p',incompatible,'field-error'));install.textContent='Choose a compatible route';progress.textContent='Preview unavailable for this tool.';return;} + if(!route.value){comparison.replaceChildren(node('p','Choose a route to preview its settings.'));button(comparison,'Choose a route',()=>navigate('routes'));install.textContent='Choose a route';progress.textContent='';return;} + const requestedRoute=routeArgs(route); + const result=await readAction({kind:'preview_install',...requestedTarget,...requestedRoute});if(!c.isConnected||requested!==revision)return; + comparison.replaceChildren();const summaries=node('div',undefined,'install-summary');comparison.append(summaries); + for(const [title,value] of [['Current settings',result.data.current],['After applying routing',result.data.proposed]])if(value){const summary=node('div');summary.append(node('h4',title),node('pre',value));summaries.append(summary);} + for(const file of result.data.file_actions||[])summaries.append(node('p',`${file.action==='create'?'Will create':file.action==='preserve'?'Will preserve':'Will update'}: ${file.file}`)); + authentication.textContent=result.data.authentication||''; + if(result.data.error){comparison.append(node('p',result.data.error,'field-error'));install.textContent='Preview unavailable';progress.textContent='Preview unavailable. Review the error.';blocked(restore,!backupReady);return;} + const grouped=new Map();for(const change of result.data.changes){if(!grouped.has(change.file))grouped.set(change.file,[]);grouped.get(change.file).push(change);} + for(const [path,changes] of grouped){const details=node('details');details.append(node('summary',`${path} · ${changes.length} changed settings`));for(const change of changes)details.append(node('pre',change.key,'diff-heading'),node('pre',`− ${change.before}`,'diff-remove'),node('pre',`+ ${change.after}`,'diff-add'));comparison.append(details);} + if(!result.data.changes.length)comparison.append(node('p','Settings already match this route. Saved settings do not confirm that the server is reachable or that the tool has loaded them.')); + const ready=result.data.changes.length>0;if(ready)plan={data:result.data,action:{kind:'install',...requestedTarget,...requestedRoute,preview_token:result.data.preview_token}}; + install.textContent=ready?'Apply routing…':'Routing already applied';blocked(install,!ready);blocked(restore,!backupReady);progress.textContent=ready?'The preview shows the files and authentication above. Review them before applying routing.':'Routing already applied.'; + }catch(error){if(c.isConnected&&requested===revision){install.textContent='Preview unavailable';comparison.replaceChildren(node('p',String(error),'field-error'));progress.textContent='Preview unavailable.';button(comparison,'Retry preview',update);}} + finally{if(c.isConnected&&requested===revision)comparison.setAttribute('aria-busy','false');} + }); + }; + for(const control of [mode,account,route])control.onchange=update; + file.oninput=()=>{installView.file=file.value;revision++;plan=null;blocked(install,true);blocked(restore,true);install.textContent='Preview required';progress.textContent='Finish entering the file path, then press Enter or leave the field to preview.';};file.onchange=update;file.onkeydown=event=>{if(event.key==='Enter')update();};update(); +} +function renderAccounts() { + const c=card('Subscription accounts');c.append(node('p','Account folders keep separate coding-tool settings. Folder existence does not confirm a completed login.')); + for(const tool of snapshot.tools.filter(t=>['codex_cli','claude'].includes(t.tool))){ + if(tool.discovery_error)c.append(node('p',`${tool.label}: ${tool.discovery_error}`,'field-error')); + for(const item of tool.account_details||tool.accounts.map(name=>({name,status:'Login status unknown'}))){const row=node('div',undefined,'route-row');row.append(node('h4',`${tool.label} · ${item.name}`),node('p',item.status||'Login status unknown'),node('pre',item.path||''));if(item.path){button(actions(row),'Copy account path',()=>copyText(item.path));button(actions(row),'Open folder',()=>run({kind:'open_account',tool:tool.tool,name:item.name},false));}mutation(actions(row),'Sign in again…',()=>run({kind:'login_account',tool:tool.tool,name:item.name}));mutation(actions(row),'Review archive…',async()=>{const result=await run({kind:'preview_account_archive',tool:tool.tool,name:item.name},false);if(result)confirmAction('Archive this account folder?',`${result.data.notice}\n\nAccount: ${result.data.name}\nDirectory: ${result.data.path}`,'Archive account',()=>run({kind:'archive_account',tool:tool.tool,name:item.name,preview_token:result.data?.preview_token}));});c.append(row);} + } + const disclosure=node('details');disclosure.append(node('summary','Sign in to another subscription account…'));c.append(disclosure); + disclosure.append(node('p','Finish login in the Terminal window. API routes use credentials configured on the server. The Codex app manages its own account.')); + const tool=field(disclosure,'Account coding tool',select(snapshot.tools.filter(t=>['codex_cli','claude'].includes(t.tool)).map(t=>[t.tool,t.label]),accountView.tool)); + const name=field(disclosure,'Name for the separate account',input(accountView.name));disclosure.append(node('small','Use 1–64 ASCII letters, digits, hyphens, or underscores.')); + const add=mutation(actions(disclosure),'Open login in Terminal…',async()=>{const result=await run({kind:'add_account',tool:tool.value,name:name.value});if(result){accountView.name='';render();}}); + const changed=()=>{accountView.name=name.value;accountView.tool=tool.value;const error=!/^[A-Za-z0-9_-]{1,64}$/.test(name.value)?'Use 1–64 letters, digits, hyphens, or underscores.':snapshot.tools.find(t=>t.tool===tool.value)?.accounts.includes(name.value)?'That account name already exists. Choose another name.':'';fieldError(name,error);blocked(add,Boolean(error)||!snapshot.tools.find(t=>t.tool===tool.value)?.available);};name.oninput=changed;tool.onchange=changed;changed(); +} +// Every algorithm retains its complete choices until the user saves or discards the draft. +function modelListKey(client){return `${snapshot.settings?.config_file||generation}:${snapshot.clients.find(endpoint=>endpoint.name===client)?.host||''}:${client}`;} +function roleChoice(draft,route,role,index) { + return draft.byAlgorithm?.get(draft.algorithm)?.[index] || (role.tier && draft.memory.get(role.tier)) || + ((draft.algorithm===route.kind||draft.loadedAlgorithm===draft.algorithm)&&draft.choices[index]) || + {client:route.choices[0]?.client||snapshot.clients[0]?.name||'',model:''}; +} +function draftDirty(draft,route){return draft.algorithm!==route.kind||JSON.stringify(draft.choices)!==JSON.stringify(route.choices);} +function renderRoute(route,parent) { + const c=card(route.id);parent.append(c);const key=draftKey(route),loadedGeneration=generation; + if(route.generated){c.append(node('p',`Generated by ${route.generated}. Edit the generator’s source to keep changes across regeneration; generated output may replace edits here.`,'warning-note'));button(actions(c),'Open server config',()=>run({kind:'open_config'},false));} + const remove=()=>confirmAction(`Delete route “${route.id}”?`,`This removes the public model ID from the server. Targets and coding-tool settings are kept. Any tool using ${route.id} must choose another route. ${route.dependents?.length?`Known settings: ${route.dependents.map(d=>typeof d==='string'?d:`${d.label}${d.account?` (${d.account})`:''}: ${d.files.join(', ')}`).join('; ')}.`:''}`,'Delete route',()=>run({kind:'remove',generation:loadedGeneration,route:route.key,id:route.id,revision:drafts.get(key)?.revision||route.revision})); + if(!route.editable){c.append(node('p','This routing method has no visual editor. Edit its fields in the server config.'));button(actions(c),'Open server config',()=>run({kind:'open_config'},false));mutation(actions(c),'Delete route…',remove).classList.add('danger');return;} + let draft=drafts.get(key); + if(!draft){draft={algorithm:route.kind,choices:route.choices.map(c=>({...c})),memory:new Map(),byAlgorithm:new Map([[route.kind,route.choices.map(c=>({...c}))]]),revision:route.revision,generation:loadedGeneration,dirty:false};route.tiers?.forEach((tier,index)=>{if(tier)draft.memory.set(tier,{...route.choices[index]});});drafts.set(key,draft);} + const algorithm=field(c,'Routing algorithm',select(snapshot.algorithms.map(a=>[a.kind,a.title]),draft.algorithm)); + const description=node('p',undefined,'description'),roles=node('div',undefined,'role-fields'),editorState=node('p',undefined,'muted');editorState.setAttribute('role','status');c.append(description,roles,editorState); + const a=actions(c);let revision=0; + if(draft.generation!==loadedGeneration){c.append(node('p','This draft was retained while another server config was selected. Review it against the current saved route before saving.','warning-note'));button(a,'Review restored draft…',()=>confirmAction('Use the current saved route as the baseline?',`Current saved route: ${route.kind} · ${routeSummary(route)}\n\nRetained draft: ${draft.algorithm} · ${draft.choices.map(choice=>`${choice.client} / ${choice.model}`).join(' → ')}\n\nThe next Save review will show settings removed or retained from the current file.`,'Use current baseline',()=>{draft.revision=route.revision;draft.generation=loadedGeneration;render();}));} + const updateDirty=()=>{draft.dirty=draftDirty(draft,route);editorState.textContent=draft.dirty?'Unsaved edits':'Saved route';blocked(save,!draft.dirty||draft.generation!==loadedGeneration);blocked(revert,!draft.dirty);const badge=document.querySelector(`[data-route="${CSS.escape(route.key)}"] .draft-badge`);if(badge)badge.hidden=!draft.dirty;}; + const saveChoices=()=>{roles.querySelectorAll('[data-role]').forEach(row=>{const controls=row.querySelectorAll('select,input');const choice={client:controls[0].value,model:controls[1].value};draft.choices[Number(row.dataset.role)]=choice;if(row.dataset.tier)draft.memory.set(row.dataset.tier,choice);});draft.byAlgorithm.set(draft.algorithm,draft.choices.map(c=>({...c})));updateDirty();}; + const save=mutation(a,'Review and save…',async()=>{ + saveChoices();let valid=true;roles.querySelectorAll('[data-role] input[type="text"]').forEach(control=>{valid=fieldError(control,!control.value.trim()?'Enter a model ID. Manual IDs are allowed.':'')&&valid;});if(!valid)return; + const args={generation:loadedGeneration,route:route.key,algorithm:draft.algorithm,choices:draft.choices.map(choice=>({...choice})),revision:draft.revision}; + const preview=await run({kind:'preview_route',...args},false);if(!preview)return; + if(draft.algorithm!==args.algorithm||JSON.stringify(draft.choices)!==JSON.stringify(args.choices)){notify('The route draft changed during review. Review the current edits again.','warning');return;} + confirmAction(`Save route “${route.id}”?`,[`Changed file: ${preview.data.file}.`,...preview.data.notes,'Switchyard validates the config, saves a backup, and restarts the server.'].join('\n\n'),'Save and restart',async()=>{const result=await run({kind:'apply',...args});if(result){if(draft.algorithm===args.algorithm&&JSON.stringify(draft.choices)===JSON.stringify(args.choices))drafts.delete(key);else{draft.revision=undefined;notify('Saved the reviewed route. Your newer edits remain unsaved.','warning',result.data?.details||[]);}render();document.querySelector('#title').focus();}}); + },true); + const revert=mutation(a,'Discard unsaved edits…',()=>{if(!draft.dirty)return;confirmAction('Discard route edits?','This returns the route editor to the saved configuration.','Discard edits',()=>{drafts.delete(key);refresh();});}); + button(a,'Reload saved route…',()=>confirmAction('Reload the saved route?','This discards the current route draft and reads the latest server config.','Reload route',()=>{drafts.delete(key);refresh();})); + mutation(a,'Duplicate…',()=>routeIdentity('duplicate_route',route));mutation(a,'Rename…',()=>routeIdentity('rename_route',route));mutation(a,'Delete route…',remove).classList.add('danger'); + const draw=async()=>{ + const current=++revision,chosen=draft.algorithm;editorState.textContent='Loading routing roles…';roles.setAttribute('aria-busy','true'); + try{const result=await readAction({kind:'editor',generation:loadedGeneration,route:route.key,algorithm:chosen});if(!c.isConnected||current!==revision)return; + description.textContent=result.message;if(!draft.revision)draft.revision=result.data.revision;roles.replaceChildren(); + let roleList=result.data.roles; + if(chosen==='random'&&draft.byAlgorithm.has(chosen)){const template=roleList[0];roleList=draft.byAlgorithm.get(chosen).map((_,i)=>({...template,label:`Model ${i+1}`,tier:null}));} + draft.choices=roleList.map((role,index)=>({...roleChoice(draft,route,role,index)})); + roleList.forEach((role,index)=>{ + const choice=draft.choices[index],row=node('div');row.dataset.role=index;row.dataset.tier=role.tier||''; + const client=field(row,`${role.label} endpoint`,select(snapshot.clients.map(c=>[c.name,`${c.name} (${c.host})`]),choice.client)); + const model=field(row,role.label==='Model'?'Model':`${role.label} model`,input(choice.model));row.append(node('small',role.hint)); + const datalist=node('datalist');datalist.id=`models-${loadedGeneration}-${route.key}-${index}`;model.setAttribute('list',datalist.id);row.append(datalist); + const guidance=node('div'),listStatus=node('small',undefined,'model-list-status');row.append(guidance,listStatus); + const fill=()=>{const cached=lists.get(modelListKey(client.value)),values=cached?.models||snapshot.clients.find(c=>c.name===client.value)?.models||[];datalist.replaceChildren();for(const value of values){const option=node('option');option.value=value;datalist.append(option);}listStatus.textContent=`${values.length} suggestions · ${cached?`${cached.source==='cache'?'Switchyard cached suggestions':cached.source==='local'?'Codex cached suggestions':cached.source==='config'?'Configured targets':'Endpoint suggestions'} · ${Number.isFinite(cached.fetchedAt)&&cached.fetchedAt>0?`fetched ${new Date(cached.fetchedAt*1000).toLocaleString()}`:'fetch time unavailable'}${cached.warning?` · refresh failed at ${cached.time}`:''}`:'configured targets'}${values.length?'':' · enter a model ID manually'}.`; + }; + const endpointGuidance=()=>{guidance.replaceChildren();const endpoint=snapshot.clients.find(e=>e.name===client.value); + if(endpoint?.note)guidance.append(node('p',endpoint.note,'warning-note')); + if(endpoint?.accepts_key){const details=node('details');details.append(node('summary','Key for model suggestions'));const keyInput=field(details,'Model-list key (stored in macOS Keychain)',input('','password'));keyInput.autocomplete='off';details.append(node('small','This key loads model suggestions. Configure server credentials in the server config.')); + mutation(actions(details),'Save key and load models',async()=>{const listKey=modelListKey(client.value);const result=await run({kind:'models',client:client.value,refresh:true,key:keyInput.value},false);if(result){keyInput.value='';lists.set(listKey,{models:result.data.models,time:new Date().toLocaleTimeString(),warning:Boolean(result.data.warning&&['cache','config'].includes(result.data.source)),source:result.data.source||'model-list result',fetchedAt:result.data.fetched_at});fill();}});guidance.append(details);} + }; + client.onchange=()=>{fill();endpointGuidance();saveChoices();if(model.value)guidance.append(node('p','The endpoint changed. Check that it supports the retained model ID before saving.','warning-note'));};model.oninput=()=>{fieldError(model,'');saveChoices();};fill();endpointGuidance(); + mutation(actions(row),'Refresh suggestions',async()=>{const listKey=modelListKey(client.value);listStatus.textContent='Loading suggestions…';const result=await run({kind:'models',client:client.value,refresh:true,key:null},false);if(result){lists.set(listKey,{models:result.data.models,time:new Date().toLocaleTimeString(),warning:Boolean(result.data.warning&&['cache','config'].includes(result.data.source)),source:result.data.source||'model-list result',fetchedAt:result.data.fetched_at});fill();}else listStatus.textContent='Suggestions unavailable. Retry or enter a model ID manually.';}); + if(!choice.model)row.append(node('p','New role: choose its endpoint and model before saving.','warning-note')); + if(chosen==='random')mutation(actions(row),'Remove model',()=>{saveChoices();if(draft.choices.length<=2){notify('Random routing needs at least two models.','warning');return;}draft.choices.splice(index,1);draft.byAlgorithm.set(chosen,draft.choices.map(c=>({...c})));draw();}); + roles.append(row); + });draft.loadedAlgorithm=chosen;draft.byAlgorithm.set(chosen,draft.choices.map(c=>({...c}))); + if(chosen==='random')mutation(roles,'Add model',()=>{saveChoices();draft.choices.push({client:snapshot.clients[0]?.name||'',model:''});draft.byAlgorithm.set(chosen,draft.choices.map(c=>({...c})));draw();}); + updateDirty(); + }catch(error){if(c.isConnected&¤t===revision){editorState.textContent=`Could not load the editor: ${error}`;button(roles,'Retry editor',draw);blocked(save,true);}} + finally{roles.setAttribute('aria-busy','false');} + }; + algorithm.onchange=()=>{saveChoices();draft.algorithm=algorithm.value;draft.choices=draft.byAlgorithm.get(draft.algorithm)?.map(c=>({...c}))||[];draw();};updateDirty();enqueue(draw); +} +document.querySelectorAll('[data-page]').forEach(b=>b.onclick=()=>navigate(b.dataset.page)); +document.querySelector('#refresh').onclick=refresh; +document.querySelector('#dismiss-notification').onclick=dismissNotification; +document.querySelector('#dismiss-notification').onmousedown=event=>event.preventDefault(); +window.matchMedia?.('(prefers-color-scheme: dark)').addEventListener('change',()=>{if(page==='overview'&&snapshot){const saved=capturePageState();render();restorePageState(saved);}}); +window.addEventListener?.('switchyard-notice',event=>notify(String(event.detail),'warning')); +window.addEventListener?.('switchyard-page',event=>{ + const next=typeof event.detail==='string'?event.detail:event.detail?.page; + if(!Object.hasOwn(pageNames,next))return; + if(next==='overview')Object.assign(analyticsView,{period:'all',group:'model',search:'',model:typeof event.detail==='object'?event.detail.model||null:null}); + navigate(next);if(!snapshot)refresh(); +}); +async function poll() { + if(!document.querySelector('[role="dialog"]')&&!busy&&document.activeElement?.tagName!=='SELECT')await refresh(false); + setTimeout(poll,(snapshot?.refresh_seconds||30)*1000); +} +content.append(node('p','Loading Switchyard…','empty-state')); +refresh().finally(()=>setTimeout(poll,(snapshot?.refresh_seconds||30)*1000)); diff --git a/crates/switchyard-desktop/ui/index.html b/crates/switchyard-desktop/ui/index.html new file mode 100644 index 0000000000..f73713368c --- /dev/null +++ b/crates/switchyard-desktop/ui/index.html @@ -0,0 +1,50 @@ + + + + + + + + Switchyard + + + + + + + +
+
+

Overview

+
Loading…
+
+ + + +
+
+ + diff --git a/crates/switchyard-desktop/ui/vendor/LICENSE.echarts b/crates/switchyard-desktop/ui/vendor/LICENSE.echarts new file mode 100644 index 0000000000..c633765305 --- /dev/null +++ b/crates/switchyard-desktop/ui/vendor/LICENSE.echarts @@ -0,0 +1,222 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + + + +======================================================================== +Apache ECharts Subcomponents: + +The Apache ECharts project contains subcomponents with separate copyright +notices and license terms. Your use of the source code for these +subcomponents is also subject to the terms and conditions of the following +licenses. + +BSD 3-Clause (d3.js): +The following files embed [d3.js](https://github.com/d3/d3) BSD 3-Clause: + `/src/chart/treemap/treemapLayout.ts`, + `/src/chart/tree/layoutHelper.ts`, + `/src/chart/graph/forceHelper.ts`, + `/src/util/number.ts` +See `/licenses/LICENSE-d3` for details of the license. diff --git a/crates/switchyard-desktop/ui/vendor/NOTICE.echarts b/crates/switchyard-desktop/ui/vendor/NOTICE.echarts new file mode 100644 index 0000000000..c6a6e5e43b --- /dev/null +++ b/crates/switchyard-desktop/ui/vendor/NOTICE.echarts @@ -0,0 +1,5 @@ +Apache ECharts +Copyright 2017-2026 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (https://www.apache.org/). \ No newline at end of file diff --git a/crates/switchyard-desktop/ui/vendor/echarts.min.js b/crates/switchyard-desktop/ui/vendor/echarts.min.js new file mode 100644 index 0000000000..3b8ed4bcd1 --- /dev/null +++ b/crates/switchyard-desktop/ui/vendor/echarts.min.js @@ -0,0 +1,45 @@ + +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).echarts={})}(this,function(t){"use strict"; +/*! ***************************************************************************** + Copyright (c) Microsoft Corporation. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + PERFORMANCE OF THIS SOFTWARE. + ***************************************************************************** */var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[n]=e[n])},e(t,n)};function n(t,n){if("function"!=typeof n&&null!==n)throw new TypeError("Class extends value "+String(n)+" is not a constructor or null");function i(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(i.prototype=n.prototype,new i)}var i=function(){this.firefox=!1,this.ie=!1,this.edge=!1,this.newEdge=!1,this.weChat=!1},r=new function(){this.browser=new i,this.node=!1,this.wxa=!1,this.worker=!1,this.svgSupported=!1,this.touchEventsSupported=!1,this.pointerEventsSupported=!1,this.domSupported=!1,this.transformSupported=!1,this.transform3dSupported=!1,this.hasGlobalWindow="undefined"!=typeof window};"object"==typeof wx&&"function"==typeof wx.getSystemInfoSync?(r.wxa=!0,r.touchEventsSupported=!0):"undefined"==typeof document&&"undefined"!=typeof self?r.worker=!0:!r.hasGlobalWindow||"Deno"in window||"undefined"!=typeof navigator&&"string"==typeof navigator.userAgent&&navigator.userAgent.indexOf("Node.js")>-1?(r.node=!0,r.svgSupported=!0):function(t,e){var n=e.browser,i=t.match(/Firefox\/([\d.]+)/),r=t.match(/MSIE\s([\d.]+)/)||t.match(/Trident\/.+?rv:(([\d.]+))/),o=t.match(/Edge?\/([\d.]+)/),a=/micromessenger/i.test(t);i&&(n.firefox=!0,n.version=i[1]);r&&(n.ie=!0,n.version=r[1]);o&&(n.edge=!0,n.version=o[1],n.newEdge=+o[1].split(".")[0]>18);a&&(n.weChat=!0);e.svgSupported="undefined"!=typeof SVGRect,e.touchEventsSupported="ontouchstart"in window&&!n.ie&&!n.edge,e.pointerEventsSupported="onpointerdown"in window&&(n.edge||n.ie&&+n.version>=11);var s=e.domSupported="undefined"!=typeof document;if(s){var l=document.documentElement.style;e.transform3dSupported=(n.ie&&"transition"in l||n.edge||"WebKitCSSMatrix"in window&&"m11"in new WebKitCSSMatrix||"MozPerspective"in l)&&!("OTransition"in l),e.transformSupported=e.transform3dSupported||n.ie&&+n.version>=9}}(navigator.userAgent,r);var o="sans-serif",a="12px "+o;var s,l,u=function(t){var e={};if("undefined"==typeof JSON)return e;for(var n=0;n=0)o=r*t.length;else for(var h=0;h=M&&(S=0),S++}function T(){for(var t=[],e=0;e>1)%2;a.style.cssText=["position: absolute","visibility: hidden","padding: 0","margin: 0","border-width: 0","user-select: none","width:0","height:0",i[s]+":0",r[l]+":0",i[1-s]+":auto",r[1-l]+":auto",""].join("!important;"),t.appendChild(a),n.push(a)}return e.clearMarkers=function(){B(n,function(t){t.parentNode&&t.parentNode.removeChild(t)})},n}(e,a),l=function(t,e,n){for(var i=n?"invTrans":"trans",r=e[i],o=e.srcCoords,a=[],s=[],l=!0,u=0;u<4;u++){var c=t[u].getBoundingClientRect(),h=2*u,d=c.left,p=c.top;a.push(d,p),l=l&&o&&d===o[h]&&p===o[h+1],s.push(t[u].offsetLeft,t[u].offsetTop)}return l&&r?r:(e.srcCoords=a,e[i]=n?te(s,a):te(a,s))}(s,a,o);if(l)return l(t,n,i),!0}return!1}function re(t){return"CANVAS"===t.nodeName.toUpperCase()}var oe=/([&<>"'])/g,ae={"&":"&","<":"<",">":">",'"':""","'":"'"};function se(t){return null==t?"":(t+"").replace(oe,function(t,e){return ae[e]})}var le=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ue=[],ce=r.browser.firefox&&+r.browser.version.split(".")[0]<39;function he(t,e,n,i){return n=n||{},i?de(t,e,n):ce&&null!=e.layerX&&e.layerX!==e.offsetX?(n.zrX=e.layerX,n.zrY=e.layerY):null!=e.offsetX?(n.zrX=e.offsetX,n.zrY=e.offsetY):de(t,e,n),n}function de(t,e,n){if(r.domSupported&&t.getBoundingClientRect){var i=e.clientX,o=e.clientY;if(re(t)){var a=t.getBoundingClientRect();return n.zrX=i-a.left,void(n.zrY=o-a.top)}if(ie(ue,t,i,o))return n.zrX=ue[0],void(n.zrY=ue[1])}n.zrX=n.zrY=0}function pe(t){return t||window.event}function fe(t,e,n){if(null!=(e=pe(e)).zrX)return e;var i=e.type;if(i&&i.indexOf("touch")>=0){var r="touchend"!==i?e.targetTouches[0]:e.changedTouches[0];r&&he(t,r,e,n)}else{he(t,e,e,n);var o=function(t){var e=t.wheelDelta;if(e)return e;var n=t.deltaX,i=t.deltaY;if(null==n||null==i)return e;return 3*(0!==i?Math.abs(i):Math.abs(n))*(i>0?-1:i<0?1:n>0?-1:1)}(e);e.zrDelta=o?o/120:-(e.detail||0)/3}var a=e.button;return null==e.which&&void 0!==a&&le.test(e.type)&&(e.which=1&a?1:2&a?3:4&a?2:0),e}function ge(t,e,n,i){t.addEventListener(e,n,i)}function ve(t,e,n,i){t.removeEventListener(e,n,i)}var ye=function(t){t.preventDefault(),t.stopPropagation(),t.cancelBubble=!0};function me(t){return 2===t.which||3===t.which}var xe=function(){function t(){this._track=[]}return t.prototype.recognize=function(t,e,n){return this._doTrack(t,e,n),this._recognize(t)},t.prototype.clear=function(){return this._track.length=0,this},t.prototype._doTrack=function(t,e,n){var i=t.touches;if(i){for(var r={points:[],touches:[],target:e,event:t},o=0,a=i.length;o1&&r&&r.length>1){var a=_e(r)/_e(o);!isFinite(a)&&(a=1),e.pinchScale=a;var s=[((i=r)[0][0]+i[1][0])/2,(i[0][1]+i[1][1])/2];return e.pinchX=s[0],e.pinchY=s[1],{type:"pinch",target:t[0].target,event:e}}}}};function we(){return[1,0,0,1,0,0]}function Se(t){return t[0]=1,t[1]=0,t[2]=0,t[3]=1,t[4]=0,t[5]=0,t}function Me(t,e){return t[0]=e[0],t[1]=e[1],t[2]=e[2],t[3]=e[3],t[4]=e[4],t[5]=e[5],t}function Ie(t,e,n){var i=e[0]*n[0]+e[2]*n[1],r=e[1]*n[0]+e[3]*n[1],o=e[0]*n[2]+e[2]*n[3],a=e[1]*n[2]+e[3]*n[3],s=e[0]*n[4]+e[2]*n[5]+e[4],l=e[1]*n[4]+e[3]*n[5]+e[5];return t[0]=i,t[1]=r,t[2]=o,t[3]=a,t[4]=s,t[5]=l,t}function Te(t,e,n){return t[0]=e[0],t[1]=e[1],t[2]=e[2],t[3]=e[3],t[4]=e[4]+n[0],t[5]=e[5]+n[1],t}function Ce(t,e,n,i){void 0===i&&(i=[0,0]);var r=e[0],o=e[2],a=e[4],s=e[1],l=e[3],u=e[5],c=Math.sin(n),h=Math.cos(n);return t[0]=r*h+s*c,t[1]=-r*c+s*h,t[2]=o*h+l*c,t[3]=-o*c+h*l,t[4]=h*(a-i[0])+c*(u-i[1])+i[0],t[5]=h*(u-i[1])-c*(a-i[0])+i[1],t}function De(t,e,n){var i=n[0],r=n[1];return t[0]=e[0]*i,t[1]=e[1]*r,t[2]=e[2]*i,t[3]=e[3]*r,t[4]=e[4]*i,t[5]=e[5]*r,t}function Ae(t,e){var n=e[0],i=e[2],r=e[4],o=e[1],a=e[3],s=e[5],l=n*a-o*i;return l?(l=1/l,t[0]=a*l,t[1]=-o*l,t[2]=-i*l,t[3]=n*l,t[4]=(i*s-a*r)*l,t[5]=(o*r-n*s)*l,t):null}function ke(t){var e=[1,0,0,1,0,0];return Me(e,t),e}var Le=Object.freeze({__proto__:null,create:we,identity:Se,copy:Me,mul:Ie,translate:Te,rotate:Ce,scale:De,invert:Ae,clone:ke}),Pe=function(){function t(t,e){this.x=t||0,this.y=e||0}return t.prototype.copy=function(t){return this.x=t.x,this.y=t.y,this},t.prototype.clone=function(){return new t(this.x,this.y)},t.prototype.set=function(t,e){return this.x=t,this.y=e,this},t.prototype.equal=function(t){return t.x===this.x&&t.y===this.y},t.prototype.add=function(t){return this.x+=t.x,this.y+=t.y,this},t.prototype.scale=function(t){this.x*=t,this.y*=t},t.prototype.scaleAndAdd=function(t,e){this.x+=t.x*e,this.y+=t.y*e},t.prototype.sub=function(t){return this.x-=t.x,this.y-=t.y,this},t.prototype.dot=function(t){return this.x*t.x+this.y*t.y},t.prototype.len=function(){return Math.sqrt(this.x*this.x+this.y*this.y)},t.prototype.lenSquare=function(){return this.x*this.x+this.y*this.y},t.prototype.normalize=function(){var t=this.len();return this.x/=t,this.y/=t,this},t.prototype.distance=function(t){var e=this.x-t.x,n=this.y-t.y;return Math.sqrt(e*e+n*n)},t.prototype.distanceSquare=function(t){var e=this.x-t.x,n=this.y-t.y;return e*e+n*n},t.prototype.negate=function(){return this.x=-this.x,this.y=-this.y,this},t.prototype.transform=function(t){if(t){var e=this.x,n=this.y;return this.x=t[0]*e+t[2]*n+t[4],this.y=t[1]*e+t[3]*n+t[5],this}},t.prototype.toArray=function(t){return t[0]=this.x,t[1]=this.y,t},t.prototype.fromArray=function(t){this.x=t[0],this.y=t[1]},t.set=function(t,e,n){t.x=e,t.y=n},t.copy=function(t,e){t.x=e.x,t.y=e.y},t.len=function(t){return Math.sqrt(t.x*t.x+t.y*t.y)},t.lenSquare=function(t){return t.x*t.x+t.y*t.y},t.dot=function(t,e){return t.x*e.x+t.y*e.y},t.add=function(t,e,n){t.x=e.x+n.x,t.y=e.y+n.y},t.sub=function(t,e,n){t.x=e.x-n.x,t.y=e.y-n.y},t.scale=function(t,e,n){t.x=e.x*n,t.y=e.y*n},t.scaleAndAdd=function(t,e,n,i){t.x=e.x+n.x*i,t.y=e.y+n.y*i},t.lerp=function(t,e,n,i){var r=1-i;t.x=r*e.x+i*n.x,t.y=r*e.y+i*n.y},t}(),Oe=Math.min,Re=Math.max,Ne=Math.abs,Ee=["x","y"],ze=["width","height"],Be=new Pe,Ve=new Pe,Ge=new Pe,Fe=new Pe,He=rn(),We=He.minTv,Ue=He.maxTv,Ye=[0,0],Xe=function(){function t(t,e,n,i){je(this,t,e,n,i)}return t.set=function(t,e,n,i,r){return i<0&&(e+=i,i=-i),r<0&&(n+=r,r=-r),t.x=e,t.y=n,t.width=i,t.height=r,t},t.prototype.union=function(t){var e=Oe(t.x,this.x),n=Oe(t.y,this.y);isFinite(this.x)&&isFinite(this.width)?this.width=Re(t.x+t.width,this.x+this.width)-e:this.width=t.width,isFinite(this.y)&&isFinite(this.height)?this.height=Re(t.y+t.height,this.y+this.height)-n:this.height=t.height,this.x=e,this.y=n},t.prototype.applyTransform=function(e){t.applyTransform(this,this,e)},t.prototype.calculateTransform=function(t){return Ke([1,0,0,1,0,0],this,t)},t.prototype.intersect=function(e,n,i){return t.intersect(this,e,n,i)},t.intersect=function(e,n,i,r){i&&Pe.set(i,0,0);var o=r&&r.outIntersectRect||null,a=r&&r.clamp;if(o&&(o.x=o.y=o.width=o.height=NaN),!e||!n)return!1;e instanceof t||(e=je(Qe,e.x,e.y,e.width,e.height)),n instanceof t||(n=je(tn,n.x,n.y,n.width,n.height));var s=!!i;He.reset(r,s);var l=He.touchThreshold,u=e.x+l,c=e.x+e.width-l,h=e.y+l,d=e.y+e.height-l,p=n.x+l,f=n.x+n.width-l,g=n.y+l,v=n.y+n.height-l;if(u>c||h>d||p>f||g>v)return!1;var y=!(c=t.x&&e<=t.x+t.width&&n>=t.y&&n<=t.y+t.height},t.prototype.contain=function(e,n){return t.contain(this,e,n)},t.prototype.clone=function(){return new t(this.x,this.y,this.width,this.height)},t.prototype.copy=function(t){qe(this,t)},t.prototype.plain=function(){return{x:this.x,y:this.y,width:this.width,height:this.height}},t.prototype.isFinite=function(){return isFinite(this.x)&&isFinite(this.y)&&isFinite(this.width)&&isFinite(this.height)},t.prototype.isZero=function(){return 0===this.width||0===this.height},t.create=function(e){return new t(e?e.x:0,e?e.y:0,e?e.width:0,e?e.height:0)},t.copy=function(t,e){return t.x=e.x,t.y=e.y,t.width=e.width,t.height=e.height,t},t.applyTransform=function(t,e,n){if(n){if(n[1]<1e-5&&n[1]>-1e-5&&n[2]<1e-5&&n[2]>-1e-5){var i=n[0],r=n[3],o=n[4],a=n[5];return t.x=e.x*i+o,t.y=e.y*r+a,t.width=e.width*i,t.height=e.height*r,t.width<0&&(t.x+=t.width,t.width=-t.width),void(t.height<0&&(t.y+=t.height,t.height=-t.height))}Be.x=Ge.x=e.x,Be.y=Fe.y=e.y,Ve.x=Fe.x=e.x+e.width,Ve.y=Ge.y=e.y+e.height,Be.transform(n),Fe.transform(n),Ve.transform(n),Ge.transform(n),t.x=Oe(Be.x,Ve.x,Ge.x,Fe.x),t.y=Oe(Be.y,Ve.y,Ge.y,Fe.y);var s=Re(Be.x,Ve.x,Ge.x,Fe.x),l=Re(Be.y,Ve.y,Ge.y,Fe.y);t.width=s-t.x,t.height=l-t.y}else t!==e&&qe(t,e)},t.calculateTransform=function(t,e,n){var i=n.width/e.width,r=n.height/e.height;return Te(t=Se(t||[]),t,kt(en,-e.x,-e.y)),De(t,t,kt(en,i,r)),Te(t,t,kt(en,n.x,n.y)),t},t}(),Ze=Xe.create,je=Xe.set,qe=Xe.copy,Ke=Xe.calculateTransform,$e=Xe.applyTransform,Je=Xe.contain,Qe=new Xe(0,0,0,0),tn=new Xe(0,0,0,0),en=[];function nn(t,e,n,i,r,o,a,s){var l=Ne(e-n),u=Ne(i-t),c=Oe(l,u),h=Ee[r],d=Ee[1-r],p=ze[r];e=u||!He.bidirectional)&&(We[h]=-u,We[d]=0,He.useDir&&He.calcDirMTV())))}function rn(){var t=0,e=new Pe,n=new Pe,i={minTv:new Pe,maxTv:new Pe,useDir:!1,dirMinTv:new Pe,touchThreshold:0,bidirectional:!0,negativeSize:!1,reset:function(r,o){i.touchThreshold=0,r&&null!=r.touchThreshold&&(i.touchThreshold=Re(0,r.touchThreshold)),i.negativeSize=!1,o&&(i.minTv.set(1/0,1/0),i.maxTv.set(0,0),i.useDir=!1,r&&null!=r.direction&&(i.useDir=!0,i.dirMinTv.copy(i.minTv),n.copy(i.minTv),t=r.direction,i.bidirectional=null==r.bidirectional||!!r.bidirectional,i.bidirectional||e.set(Math.cos(t),Math.sin(t))))},calcDirMTV:function(){var o=i.minTv,a=i.dirMinTv,s=o.y*o.y+o.x*o.x,l=Math.sin(t),u=Math.cos(t),c=l*o.y+u*o.x;r(c)?r(o.x)&&r(o.y)&&a.set(0,0):(n.x=s*u/c,n.y=s*l/c,r(n.x)&&r(n.y)?a.set(0,0):(i.bidirectional||e.dot(n)>0)&&n.len()=0;u--){var c=i[u];c===n||c.ignore||c.ignoreCoarsePointer||c.parent&&c.parent.ignoreCoarsePointer||(cn.copy(c.getBoundingRect()),c.transform&&cn.applyTransform(c.transform),cn.intersect(l)&&o.push(c))}if(o.length)for(var h=Math.PI/12,d=2*Math.PI,p=0;p=0;o--){var a=t[o],s=void 0;if(a!==r&&!a.ignore&&(s=dn(a,n,i))&&(!e.topTarget&&(e.topTarget=a),s!==on)){e.target=a;break}}}function fn(t,e,n){var i=t.painter;return e<0||e>i.getWidth()||n<0||n>i.getHeight()}B(["click","mousedown","mouseup","mousewheel","dblclick","contextmenu"],function(t){hn.prototype[t]=function(e){var n,i,r=e.zrX,o=e.zrY,a=fn(this,r,o);if("mouseup"===t&&a||(i=(n=this.findHover(r,o)).target),"mousedown"===t)this._downEl=i,this._downPoint=[e.zrX,e.zrY],this._upEl=i;else if("mouseup"===t)this._upEl=i;else if("click"===t){if(this._downEl!==this._upEl||!this._downPoint||Ft(this._downPoint,[e.zrX,e.zrY])>4)return;this._downPoint=null}this.dispatchToElement(n,t,e)}});function gn(t,e,n,i){var r=e+1;if(r===n)return 1;if(i(t[r++],t[e])<0){for(;r=0;)r++;return r-e}function vn(t,e,n,i,r){for(i===e&&i++;i>>1])<0?l=o:s=o+1;var u=i-s;switch(u){case 3:t[s+3]=t[s+2];case 2:t[s+2]=t[s+1];case 1:t[s+1]=t[s];break;default:for(;u>0;)t[s+u]=t[s+u-1],u--}t[s]=a}}function yn(t,e,n,i,r,o){var a=0,s=0,l=1;if(o(t,e[n+r])>0){for(s=i-r;l0;)a=l,(l=1+(l<<1))<=0&&(l=s);l>s&&(l=s),a+=r,l+=r}else{for(s=r+1;ls&&(l=s);var u=a;a=r-l,l=r-u}for(a++;a>>1);o(t,e[n+c])>0?a=c+1:l=c}return l}function mn(t,e,n,i,r,o){var a=0,s=0,l=1;if(o(t,e[n+r])<0){for(s=r+1;ls&&(l=s);var u=a;a=r-l,l=r-u}else{for(s=i-r;l=0;)a=l,(l=1+(l<<1))<=0&&(l=s);l>s&&(l=s),a+=r,l+=r}for(a++;a>>1);o(t,e[n+c])<0?l=c:a=c+1}return l}function xn(t,e){var n,i,r=7,o=0,a=[];function s(s){var l=n[s],u=i[s],c=n[s+1],h=i[s+1];i[s]=u+h,s===o-3&&(n[s+1]=n[s+2],i[s+1]=i[s+2]),o--;var d=mn(t[c],t,l,u,0,e);l+=d,0!==(u-=d)&&0!==(h=yn(t[l+u-1],t,c,h,h-1,e))&&(u<=h?function(n,i,o,s){var l=0;for(l=0;l=7||p>=7);if(f)break;g<0&&(g=0),g+=2}if((r=g)<1&&(r=1),1===i){for(l=0;l=0;l--)t[p+l]=t[d+l];return void(t[h]=a[c])}var f=r;for(;;){var g=0,v=0,y=!1;do{if(e(a[c],t[u])<0){if(t[h--]=t[u--],g++,v=0,0===--i){y=!0;break}}else if(t[h--]=a[c--],v++,g=0,1===--s){y=!0;break}}while((g|v)=0;l--)t[p+l]=t[d+l];if(0===i){y=!0;break}}if(t[h--]=a[c--],1===--s){y=!0;break}if(0!==(v=s-yn(t[u],a,0,s,s-1,e))){for(s-=v,p=(h-=v)+1,d=(c-=v)+1,l=0;l=7||v>=7);if(y)break;f<0&&(f=0),f+=2}(r=f)<1&&(r=1);if(1===s){for(p=(h-=i)+1,d=(u-=i)+1,l=i-1;l>=0;l--)t[p+l]=t[d+l];t[h]=a[c]}else{if(0===s)throw new Error;for(d=h-(s-1),l=0;l1;){var t=o-2;if(t>=1&&i[t-1]<=i[t]+i[t+1]||t>=2&&i[t-2]<=i[t]+i[t-1])i[t-1]i[t+1])break;s(t)}},forceMergeRuns:function(){for(;o>1;){var t=o-2;t>0&&i[t-1]=32;)e|=1&t,t>>=1;return t+e}(r);do{if((o=gn(t,n,i,e))s&&(l=s),vn(t,n,n+l,n+o,e),o=l}a.pushRun(n,o),a.mergeRuns(),r-=o,n+=o}while(0!==r);a.forceMergeRuns()}}}var bn=!1;function wn(){bn||(bn=!0,console.warn("z / z2 / zlevel of displayable is invalid, which may cause unexpected errors"))}function Sn(t,e){return t.zlevel===e.zlevel?t.z===e.z?t.z2-e.z2:t.z-e.z:t.zlevel-e.zlevel}var Mn=function(){function t(){this._roots=[],this._displayList=[],this._displayListLen=0,this.displayableSortFunc=Sn}return t.prototype.traverse=function(t,e){for(var n=0;n=0&&this._roots.splice(i,1)}},t.prototype.delAllRoots=function(){this._roots=[],this._displayList=[],this._displayListLen=0},t.prototype.getRoots=function(){return this._roots},t.prototype.dispose=function(){this._displayList=null,this._roots=null},t}(),In=r.hasGlobalWindow&&(window.requestAnimationFrame&&window.requestAnimationFrame.bind(window)||window.msRequestAnimationFrame&&window.msRequestAnimationFrame.bind(window)||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame)||function(t){return setTimeout(t,16)},Tn={linear:function(t){return t},quadraticIn:function(t){return t*t},quadraticOut:function(t){return t*(2-t)},quadraticInOut:function(t){return(t*=2)<1?.5*t*t:-.5*(--t*(t-2)-1)},cubicIn:function(t){return t*t*t},cubicOut:function(t){return--t*t*t+1},cubicInOut:function(t){return(t*=2)<1?.5*t*t*t:.5*((t-=2)*t*t+2)},quarticIn:function(t){return t*t*t*t},quarticOut:function(t){return 1- --t*t*t*t},quarticInOut:function(t){return(t*=2)<1?.5*t*t*t*t:-.5*((t-=2)*t*t*t-2)},quinticIn:function(t){return t*t*t*t*t},quinticOut:function(t){return--t*t*t*t*t+1},quinticInOut:function(t){return(t*=2)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2)},sinusoidalIn:function(t){return 1-Math.cos(t*Math.PI/2)},sinusoidalOut:function(t){return Math.sin(t*Math.PI/2)},sinusoidalInOut:function(t){return.5*(1-Math.cos(Math.PI*t))},exponentialIn:function(t){return 0===t?0:Math.pow(1024,t-1)},exponentialOut:function(t){return 1===t?1:1-Math.pow(2,-10*t)},exponentialInOut:function(t){return 0===t?0:1===t?1:(t*=2)<1?.5*Math.pow(1024,t-1):.5*(2-Math.pow(2,-10*(t-1)))},circularIn:function(t){return 1-Math.sqrt(1-t*t)},circularOut:function(t){return Math.sqrt(1- --t*t)},circularInOut:function(t){return(t*=2)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)},elasticIn:function(t){var e,n=.1;return 0===t?0:1===t?1:(!n||n<1?(n=1,e=.1):e=.4*Math.asin(1/n)/(2*Math.PI),-n*Math.pow(2,10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/.4))},elasticOut:function(t){var e,n=.1;return 0===t?0:1===t?1:(!n||n<1?(n=1,e=.1):e=.4*Math.asin(1/n)/(2*Math.PI),n*Math.pow(2,-10*t)*Math.sin((t-e)*(2*Math.PI)/.4)+1)},elasticInOut:function(t){var e,n=.1,i=.4;return 0===t?0:1===t?1:(!n||n<1?(n=1,e=.1):e=i*Math.asin(1/n)/(2*Math.PI),(t*=2)<1?n*Math.pow(2,10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/i)*-.5:n*Math.pow(2,-10*(t-=1))*Math.sin((t-e)*(2*Math.PI)/i)*.5+1)},backIn:function(t){var e=1.70158;return t*t*((e+1)*t-e)},backOut:function(t){var e=1.70158;return--t*t*((e+1)*t+e)+1},backInOut:function(t){var e=2.5949095;return(t*=2)<1?t*t*((e+1)*t-e)*.5:.5*((t-=2)*t*((e+1)*t+e)+2)},bounceIn:function(t){return 1-Tn.bounceOut(1-t)},bounceOut:function(t){return t<1/2.75?7.5625*t*t:t<2/2.75?7.5625*(t-=1.5/2.75)*t+.75:t<2.5/2.75?7.5625*(t-=2.25/2.75)*t+.9375:7.5625*(t-=2.625/2.75)*t+.984375},bounceInOut:function(t){return t<.5?.5*Tn.bounceIn(2*t):.5*Tn.bounceOut(2*t-1)+.5}},Cn=Math.pow,Dn=Math.sqrt,An=1e-8,kn=1e-4,Ln=Dn(3),Pn=1/3,On=Ct(),Rn=Ct(),Nn=Ct();function En(t){return t>-1e-8&&tAn||t<-1e-8}function Bn(t,e,n,i,r){var o=1-r;return o*o*(o*t+3*r*e)+r*r*(r*i+3*o*n)}function Vn(t,e,n,i,r){var o=1-r;return 3*(((e-t)*o+2*(n-e)*r)*o+(i-n)*r*r)}function Gn(t,e,n,i,r,o){var a=i+3*(e-n)-t,s=3*(n-2*e+t),l=3*(e-t),u=t-r,c=s*s-3*a*l,h=s*l-9*a*u,d=l*l-3*s*u,p=0;if(En(c)&&En(h)){if(En(s))o[0]=0;else(M=-l/s)>=0&&M<=1&&(o[p++]=M)}else{var f=h*h-4*c*d;if(En(f)){var g=h/c,v=-g/2;(M=-s/a+g)>=0&&M<=1&&(o[p++]=M),v>=0&&v<=1&&(o[p++]=v)}else if(f>0){var y=Dn(f),m=c*s+1.5*a*(-h+y),x=c*s+1.5*a*(-h-y);(M=(-s-((m=m<0?-Cn(-m,Pn):Cn(m,Pn))+(x=x<0?-Cn(-x,Pn):Cn(x,Pn))))/(3*a))>=0&&M<=1&&(o[p++]=M)}else{var _=(2*c*s-3*a*h)/(2*Dn(c*c*c)),b=Math.acos(_)/3,w=Dn(c),S=Math.cos(b),M=(-s-2*w*S)/(3*a),I=(v=(-s+w*(S+Ln*Math.sin(b)))/(3*a),(-s+w*(S-Ln*Math.sin(b)))/(3*a));M>=0&&M<=1&&(o[p++]=M),v>=0&&v<=1&&(o[p++]=v),I>=0&&I<=1&&(o[p++]=I)}}return p}function Fn(t,e,n,i,r){var o=6*n-12*e+6*t,a=9*e+3*i-3*t-9*n,s=3*e-3*t,l=0;if(En(a)){if(zn(o))(c=-s/o)>=0&&c<=1&&(r[l++]=c)}else{var u=o*o-4*a*s;if(En(u))r[0]=-o/(2*a);else if(u>0){var c,h=Dn(u),d=(-o-h)/(2*a);(c=(-o+h)/(2*a))>=0&&c<=1&&(r[l++]=c),d>=0&&d<=1&&(r[l++]=d)}}return l}function Hn(t,e,n,i,r,o){var a=(e-t)*r+t,s=(n-e)*r+e,l=(i-n)*r+n,u=(s-a)*r+a,c=(l-s)*r+s,h=(c-u)*r+u;o[0]=t,o[1]=a,o[2]=u,o[3]=h,o[4]=h,o[5]=c,o[6]=l,o[7]=i}function Wn(t,e,n,i,r,o,a,s,l,u,c){var h,d,p,f,g,v=.005,y=1/0;On[0]=l,On[1]=u;for(var m=0;m<1;m+=.05)Rn[0]=Bn(t,n,r,a,m),Rn[1]=Bn(e,i,o,s,m),(f=Wt(On,Rn))=0&&f=0&&v=1?1:Gn(0,i,o,1,t,s)&&Bn(0,r,a,1,s[0])}}}var Qn=function(){function t(t){this._inited=!1,this._startTime=0,this._pausedTime=0,this._paused=!1,this._life=t.life||1e3,this._delay=t.delay||0,this.loop=t.loop||!1,this.onframe=t.onframe||St,this.ondestroy=t.ondestroy||St,this.onrestart=t.onrestart||St,t.easing&&this.setEasing(t.easing)}return t.prototype.step=function(t,e){if(this._inited||(this._startTime=t+this._delay,this._inited=!0),!this._paused){var n=this._life,i=t-this._startTime-this._pausedTime,r=i/n;r<0&&(r=0),r=Math.min(r,1);var o=this.easingFunc,a=o?o(r):r;if(this.onframe(a),1===r){if(!this.loop)return!0;var s=i%n;this._startTime=t-s,this._pausedTime=0,this.onrestart()}return!1}this._pausedTime+=e},t.prototype.pause=function(){this._paused=!0},t.prototype.resume=function(){this._paused=!1},t.prototype.setEasing=function(t){this.easing=t,this.easingFunc=Z(t)?t:Tn[t]||Jn(t)},t}(),ti=function(t){this.value=t},ei=function(){function t(){this._len=0}return t.prototype.insert=function(t){var e=new ti(t);return this.insertEntry(e),e},t.prototype.insertEntry=function(t){this.head?(this.tail.next=t,t.prev=this.tail,t.next=null,this.tail=t):this.head=this.tail=t,this._len++},t.prototype.remove=function(t){var e=t.prev,n=t.next;e?e.next=n:this.head=n,n?n.prev=e:this.tail=e,t.next=t.prev=null,this._len--},t.prototype.len=function(){return this._len},t.prototype.clear=function(){this.head=this.tail=null,this._len=0},t}(),ni=function(){function t(t){this._list=new ei,this._maxSize=10,this._map={},this._maxSize=t}return t.prototype.put=function(t,e){var n=this._list,i=this._map,r=null;if(null==i[t]){var o=n.len(),a=this._lastRemovedEntry;if(o>=this._maxSize&&o>0){var s=n.head;n.remove(s),delete i[s.key],r=s.value,this._lastRemovedEntry=s}a?a.value=e:a=new ti(e),a.key=t,n.insertEntry(a),i[t]=a}return r},t.prototype.get=function(t){var e=this._map[t],n=this._list;if(null!=e)return e!==n.tail&&(n.remove(e),n.insertEntry(e)),e.value},t.prototype.clear=function(){this._list.clear(),this._map={}},t.prototype.len=function(){return this._list.len()},t}(),ii={transparent:[0,0,0,0],aliceblue:[240,248,255,1],antiquewhite:[250,235,215,1],aqua:[0,255,255,1],aquamarine:[127,255,212,1],azure:[240,255,255,1],beige:[245,245,220,1],bisque:[255,228,196,1],black:[0,0,0,1],blanchedalmond:[255,235,205,1],blue:[0,0,255,1],blueviolet:[138,43,226,1],brown:[165,42,42,1],burlywood:[222,184,135,1],cadetblue:[95,158,160,1],chartreuse:[127,255,0,1],chocolate:[210,105,30,1],coral:[255,127,80,1],cornflowerblue:[100,149,237,1],cornsilk:[255,248,220,1],crimson:[220,20,60,1],cyan:[0,255,255,1],darkblue:[0,0,139,1],darkcyan:[0,139,139,1],darkgoldenrod:[184,134,11,1],darkgray:[169,169,169,1],darkgreen:[0,100,0,1],darkgrey:[169,169,169,1],darkkhaki:[189,183,107,1],darkmagenta:[139,0,139,1],darkolivegreen:[85,107,47,1],darkorange:[255,140,0,1],darkorchid:[153,50,204,1],darkred:[139,0,0,1],darksalmon:[233,150,122,1],darkseagreen:[143,188,143,1],darkslateblue:[72,61,139,1],darkslategray:[47,79,79,1],darkslategrey:[47,79,79,1],darkturquoise:[0,206,209,1],darkviolet:[148,0,211,1],deeppink:[255,20,147,1],deepskyblue:[0,191,255,1],dimgray:[105,105,105,1],dimgrey:[105,105,105,1],dodgerblue:[30,144,255,1],firebrick:[178,34,34,1],floralwhite:[255,250,240,1],forestgreen:[34,139,34,1],fuchsia:[255,0,255,1],gainsboro:[220,220,220,1],ghostwhite:[248,248,255,1],gold:[255,215,0,1],goldenrod:[218,165,32,1],gray:[128,128,128,1],green:[0,128,0,1],greenyellow:[173,255,47,1],grey:[128,128,128,1],honeydew:[240,255,240,1],hotpink:[255,105,180,1],indianred:[205,92,92,1],indigo:[75,0,130,1],ivory:[255,255,240,1],khaki:[240,230,140,1],lavender:[230,230,250,1],lavenderblush:[255,240,245,1],lawngreen:[124,252,0,1],lemonchiffon:[255,250,205,1],lightblue:[173,216,230,1],lightcoral:[240,128,128,1],lightcyan:[224,255,255,1],lightgoldenrodyellow:[250,250,210,1],lightgray:[211,211,211,1],lightgreen:[144,238,144,1],lightgrey:[211,211,211,1],lightpink:[255,182,193,1],lightsalmon:[255,160,122,1],lightseagreen:[32,178,170,1],lightskyblue:[135,206,250,1],lightslategray:[119,136,153,1],lightslategrey:[119,136,153,1],lightsteelblue:[176,196,222,1],lightyellow:[255,255,224,1],lime:[0,255,0,1],limegreen:[50,205,50,1],linen:[250,240,230,1],magenta:[255,0,255,1],maroon:[128,0,0,1],mediumaquamarine:[102,205,170,1],mediumblue:[0,0,205,1],mediumorchid:[186,85,211,1],mediumpurple:[147,112,219,1],mediumseagreen:[60,179,113,1],mediumslateblue:[123,104,238,1],mediumspringgreen:[0,250,154,1],mediumturquoise:[72,209,204,1],mediumvioletred:[199,21,133,1],midnightblue:[25,25,112,1],mintcream:[245,255,250,1],mistyrose:[255,228,225,1],moccasin:[255,228,181,1],navajowhite:[255,222,173,1],navy:[0,0,128,1],oldlace:[253,245,230,1],olive:[128,128,0,1],olivedrab:[107,142,35,1],orange:[255,165,0,1],orangered:[255,69,0,1],orchid:[218,112,214,1],palegoldenrod:[238,232,170,1],palegreen:[152,251,152,1],paleturquoise:[175,238,238,1],palevioletred:[219,112,147,1],papayawhip:[255,239,213,1],peachpuff:[255,218,185,1],peru:[205,133,63,1],pink:[255,192,203,1],plum:[221,160,221,1],powderblue:[176,224,230,1],purple:[128,0,128,1],red:[255,0,0,1],rosybrown:[188,143,143,1],royalblue:[65,105,225,1],saddlebrown:[139,69,19,1],salmon:[250,128,114,1],sandybrown:[244,164,96,1],seagreen:[46,139,87,1],seashell:[255,245,238,1],sienna:[160,82,45,1],silver:[192,192,192,1],skyblue:[135,206,235,1],slateblue:[106,90,205,1],slategray:[112,128,144,1],slategrey:[112,128,144,1],snow:[255,250,250,1],springgreen:[0,255,127,1],steelblue:[70,130,180,1],tan:[210,180,140,1],teal:[0,128,128,1],thistle:[216,191,216,1],tomato:[255,99,71,1],turquoise:[64,224,208,1],violet:[238,130,238,1],wheat:[245,222,179,1],white:[255,255,255,1],whitesmoke:[245,245,245,1],yellow:[255,255,0,1],yellowgreen:[154,205,50,1]};function ri(t){return(t=Math.round(t))<0?0:t>255?255:t}function oi(t){return t<0?0:t>1?1:t}function ai(t){var e=t;return e.length&&"%"===e.charAt(e.length-1)?ri(parseFloat(e)/100*255):ri(parseInt(e,10))}function si(t){var e=t;return e.length&&"%"===e.charAt(e.length-1)?oi(parseFloat(e)/100):oi(parseFloat(e))}function li(t,e,n){return n<0?n+=1:n>1&&(n-=1),6*n<1?t+(e-t)*n*6:2*n<1?e:3*n<2?t+(e-t)*(2/3-n)*6:t}function ui(t,e,n){return t+(e-t)*n}function ci(t,e,n,i,r){return t[0]=e,t[1]=n,t[2]=i,t[3]=r,t}function hi(t,e){return t[0]=e[0],t[1]=e[1],t[2]=e[2],t[3]=e[3],t}var di=new ni(20),pi=null;function fi(t,e){pi&&hi(pi,e),pi=di.put(t,pi||e.slice())}function gi(t,e){if(t){e=e||[];var n=di.get(t);if(n)return hi(e,n);var i=(t+="").replace(/ /g,"").toLowerCase();if(i in ii)return hi(e,ii[i]),fi(t,e),e;var r,o=i.length;if("#"===i.charAt(0))return 4===o||5===o?(r=parseInt(i.slice(1,4),16))>=0&&r<=4095?(ci(e,(3840&r)>>4|(3840&r)>>8,240&r|(240&r)>>4,15&r|(15&r)<<4,5===o?parseInt(i.slice(4),16)/15:1),fi(t,e),e):void ci(e,0,0,0,1):7===o||9===o?(r=parseInt(i.slice(1,7),16))>=0&&r<=16777215?(ci(e,(16711680&r)>>16,(65280&r)>>8,255&r,9===o?parseInt(i.slice(7),16)/255:1),fi(t,e),e):void ci(e,0,0,0,1):void 0;var a=i.indexOf("("),s=i.indexOf(")");if(-1!==a&&s+1===o){var l=i.substr(0,a),u=i.substr(a+1,s-(a+1)).split(","),c=1;switch(l){case"rgba":if(4!==u.length)return 3===u.length?ci(e,+u[0],+u[1],+u[2],1):ci(e,0,0,0,1);c=si(u.pop());case"rgb":return u.length>=3?(ci(e,ai(u[0]),ai(u[1]),ai(u[2]),3===u.length?c:si(u[3])),fi(t,e),e):void ci(e,0,0,0,1);case"hsla":return 4!==u.length?void ci(e,0,0,0,1):(u[3]=si(u[3]),vi(u,e),fi(t,e),e);case"hsl":return 3!==u.length?void ci(e,0,0,0,1):(vi(u,e),fi(t,e),e);default:return}}ci(e,0,0,0,1)}}function vi(t,e){var n=(parseFloat(t[0])%360+360)%360/360,i=si(t[1]),r=si(t[2]),o=r<=.5?r*(i+1):r+i-r*i,a=2*r-o;return ci(e=e||[],ri(255*li(a,o,n+1/3)),ri(255*li(a,o,n)),ri(255*li(a,o,n-1/3)),1),4===t.length&&(e[3]=t[3]),e}function yi(t,e){var n=gi(t);if(n){for(var i=0;i<3;i++)n[i]=e<0?n[i]*(1-e)|0:(255-n[i])*e+n[i]|0,n[i]>255?n[i]=255:n[i]<0&&(n[i]=0);return Mi(n,4===n.length?"rgba":"rgb")}}function mi(t,e,n){if(e&&e.length&&t>=0&&t<=1){n=n||[];var i=t*(e.length-1),r=Math.floor(i),o=Math.ceil(i),a=e[r],s=e[o],l=i-r;return n[0]=ri(ui(a[0],s[0],l)),n[1]=ri(ui(a[1],s[1],l)),n[2]=ri(ui(a[2],s[2],l)),n[3]=oi(ui(a[3],s[3],l)),n}}var xi=mi;function _i(t,e,n){if(e&&e.length&&t>=0&&t<=1){var i=t*(e.length-1),r=Math.floor(i),o=Math.ceil(i),a=gi(e[r]),s=gi(e[o]),l=i-r,u=Mi([ri(ui(a[0],s[0],l)),ri(ui(a[1],s[1],l)),ri(ui(a[2],s[2],l)),oi(ui(a[3],s[3],l))],"rgba");return n?{color:u,leftIndex:r,rightIndex:o,value:i}:u}}var bi=_i;function wi(t,e,n,i){var r=gi(t);if(t)return r=function(t){if(t){var e,n,i=t[0]/255,r=t[1]/255,o=t[2]/255,a=Math.min(i,r,o),s=Math.max(i,r,o),l=s-a,u=(s+a)/2;if(0===l)e=0,n=0;else{n=u<.5?l/(s+a):l/(2-s-a);var c=((s-i)/6+l/2)/l,h=((s-r)/6+l/2)/l,d=((s-o)/6+l/2)/l;i===s?e=d-h:r===s?e=1/3+c-d:o===s&&(e=2/3+h-c),e<0&&(e+=1),e>1&&(e-=1)}var p=[360*e,n,u];return null!=t[3]&&p.push(t[3]),p}}(r),null!=e&&(r[0]=function(t){return(t=Math.round(t))<0?0:t>360?360:t}(Z(e)?e(r[0]):e)),null!=n&&(r[1]=si(Z(n)?n(r[1]):n)),null!=i&&(r[2]=si(Z(i)?i(r[2]):i)),Mi(vi(r),"rgba")}function Si(t,e){var n=gi(t);if(n&&null!=e)return n[3]=oi(e),Mi(n,"rgba")}function Mi(t,e){if(t&&t.length){var n=t[0]+","+t[1]+","+t[2];return"rgba"!==e&&"hsva"!==e&&"hsla"!==e||(n+=","+t[3]),e+"("+n+")"}}function Ii(t,e){var n=gi(t);return n?(.299*n[0]+.587*n[1]+.114*n[2])*n[3]/255+(1-n[3])*e:0}var Ti=new ni(100);function Ci(t){if(j(t)){var e=Ti.get(t);return e||(e=yi(t,-.1),Ti.put(t,e)),e}if(et(t)){var n=k({},t);return n.colorStops=V(t.colorStops,function(t){return{offset:t.offset,color:yi(t.color,-.1)}}),n}return t}var Di=Object.freeze({__proto__:null,parseCssInt:ai,parseCssFloat:si,parse:gi,lift:yi,toHex:function(t){var e=gi(t);if(e)return((1<<24)+(e[0]<<16)+(e[1]<<8)+ +e[2]).toString(16).slice(1)},fastLerp:mi,fastMapToColor:xi,lerp:_i,mapToColor:bi,modifyHSL:wi,modifyAlpha:Si,stringify:Mi,lum:Ii,random:function(){return Mi([Math.round(255*Math.random()),Math.round(255*Math.random()),Math.round(255*Math.random())],"rgb")},liftColor:Ci}),Ai=Math.round;function ki(t){var e;if(t&&"transparent"!==t){if("string"==typeof t&&t.indexOf("rgba")>-1){var n=gi(t);n&&(t="rgb("+n[0]+","+n[1]+","+n[2]+")",e=n[3])}}else t="none";return{color:t,opacity:null==e?1:e}}var Li=1e-4;function Pi(t){return t-1e-4}function Oi(t){return Ai(1e3*t)/1e3}function Ri(t){return Ai(1e4*t)/1e4}var Ni={left:"start",right:"end",center:"middle",middle:"middle"};function Ei(t){return t&&!!t.image}function zi(t){return Ei(t)||function(t){return t&&!!t.svgElement}(t)}function Bi(t){return"linear"===t.type}function Vi(t){return"radial"===t.type}function Gi(t){return t&&("linear"===t.type||"radial"===t.type)}function Fi(t){return"url(#"+t+")"}function Hi(t){var e=t.getGlobalScale(),n=Math.max(e[0],e[1]);return Math.max(Math.ceil(Math.log(n)/Math.log(10)),1)}function Wi(t){var e=t.x||0,n=t.y||0,i=(t.rotation||0)*Mt,r=at(t.scaleX,1),o=at(t.scaleY,1),a=t.skewX||0,s=t.skewY||0,l=[];return(e||n)&&l.push("translate("+e+"px,"+n+"px)"),i&&l.push("rotate("+i+")"),1===r&&1===o||l.push("scale("+r+","+o+")"),(a||s)&&l.push("skew("+Ai(a*Mt)+"deg, "+Ai(s*Mt)+"deg)"),l.join(" ")}var Ui="undefined"!=typeof Buffer&&"function"==typeof Buffer.from?function(t){return Buffer.from(t).toString("base64")}:"function"==typeof btoa&&"function"==typeof unescape&&"function"==typeof encodeURIComponent?function(t){return btoa(unescape(encodeURIComponent(t)))}:function(t){return null},Yi=Array.prototype.slice;function Xi(t,e,n){return(e-t)*n+t}function Zi(t,e,n,i){for(var r=e.length,o=0;oi?e:t,o=Math.min(n,i),a=r[o-1]||{color:[0,0,0,0],offset:0},s=o;sa)i.length=a;else for(var s=o;s=1},t.prototype.getAdditiveTrack=function(){return this._additiveTrack},t.prototype.addKeyframe=function(t,e,n){this._needsSort=!0;var i=this.keyframes,r=i.length,o=!1,a=6,s=e;if(z(e)){var l=function(t){return z(t&&t[0])?2:1}(e);a=l,(1===l&&!K(e[0])||2===l&&!K(e[0][0]))&&(o=!0)}else if(K(e)&&!rt(e))a=0;else if(j(e))if(isNaN(+e)){var u=gi(e);u&&(s=u,a=3)}else a=0;else if(et(e)){var c=k({},s);c.colorStops=V(e.colorStops,function(t){return{offset:t.offset,color:gi(t.color)}}),Bi(e)?a=4:Vi(e)&&(a=5),s=c}0===r?this.valType=a:a===this.valType&&6!==a||(o=!0),this.discrete=this.discrete||o;var h={time:t,value:s,rawValue:e,percent:0};return n&&(h.easing=n,h.easingFunc=Z(n)?n:Tn[n]||Jn(n)),i.push(h),h},t.prototype.prepare=function(t,e){var n=this.keyframes;this._needsSort&&n.sort(function(t,e){return t.time-e.time});for(var i=this.valType,r=n.length,o=n[r-1],a=this.discrete,s=er(i),l=tr(i),u=0;u=0&&!(l[n].percent<=e);n--);n=p(n,u-2)}else{for(n=d;ne);n++);n=p(n-1,u-2)}r=l[n+1],i=l[n]}if(i&&r){this._lastFr=n,this._lastFrP=e;var f=r.percent-i.percent,g=0===f?1:p((e-i.percent)/f,1);r.easingFunc&&(g=r.easingFunc(g));var v=o?this._additiveValue:h?nr:t[c];if(!er(s)&&!h||v||(v=this._additiveValue=[]),this.discrete)t[c]=g<1?i.rawValue:r.rawValue;else if(er(s))1===s?Zi(v,i[a],r[a],g):function(t,e,n,i){for(var r=e.length,o=r&&e[0].length,a=0;a0&&s.addKeyframe(0,Ji(l),i),this._trackKeys.push(a)}s.addKeyframe(t,Ji(e[a]),i)}return this._maxTime=Math.max(this._maxTime,t),this},t.prototype.pause=function(){this._clip.pause(),this._paused=!0},t.prototype.resume=function(){this._clip.resume(),this._paused=!1},t.prototype.isPaused=function(){return!!this._paused},t.prototype.duration=function(t){return this._maxTime=t,this._force=!0,this},t.prototype._doneCallback=function(){this._setTracksFinished(),this._clip=null;var t=this._doneCbs;if(t)for(var e=t.length,n=0;n0)){this._started=1;for(var e=this,n=[],i=this._maxTime||0,r=0;r1){var a=o.pop();r.addKeyframe(a.time,t[i]),r.prepare(this._maxTime,r.getAdditiveTrack())}}}},t}();function or(){return(new Date).getTime()}var ar,sr,lr=function(t){function e(e){var n=t.call(this)||this;return n._running=!1,n._time=0,n._pausedTime=0,n._pauseStart=0,n._paused=!1,e=e||{},n.stage=e.stage||{},n}return n(e,t),e.prototype.addClip=function(t){t.animation&&this.removeClip(t),this._head?(this._tail.next=t,t.prev=this._tail,t.next=null,this._tail=t):this._head=this._tail=t,t.animation=this},e.prototype.addAnimator=function(t){t.animation=this;var e=t.getClip();e&&this.addClip(e)},e.prototype.removeClip=function(t){if(t.animation){var e=t.prev,n=t.next;e?e.next=n:this._head=n,n?n.prev=e:this._tail=e,t.next=t.prev=t.animation=null}},e.prototype.removeAnimator=function(t){var e=t.getClip();e&&this.removeClip(e),t.animation=null},e.prototype.update=function(t){for(var e=or()-this._pausedTime,n=e-this._time,i=this._head;i;){var r=i.next;i.step(e,n)?(i.ondestroy(),this.removeClip(i),i=r):i=r}this._time=e,t||(this.trigger("frame",n),this.stage.update&&this.stage.update())},e.prototype._startLoop=function(){var t=this;this._running=!0,In(function e(){t._running&&(In(e),!t._paused&&t.update())})},e.prototype.start=function(){this._running||(this._time=or(),this._pausedTime=0,this._startLoop())},e.prototype.stop=function(){this._running=!1},e.prototype.pause=function(){this._paused||(this._pauseStart=or(),this._paused=!0)},e.prototype.resume=function(){this._paused&&(this._pausedTime+=or()-this._pauseStart,this._paused=!1)},e.prototype.clear=function(){for(var t=this._head;t;){var e=t.next;t.prev=t.next=t.animation=null,t=e}this._head=this._tail=null},e.prototype.isFinished=function(){return null==this._head},e.prototype.animate=function(t,e){e=e||{},this.start();var n=new rr(t,e.loop);return this.addAnimator(n),n},e}($t),ur=r.domSupported,cr=(sr={pointerdown:1,pointerup:1,pointermove:1,pointerout:1},{mouse:ar=["click","dblclick","mousewheel","wheel","mouseout","mouseup","mousedown","mousemove","contextmenu"],touch:["touchstart","touchend","touchmove"],pointer:V(ar,function(t){var e=t.replace("mouse","pointer");return sr.hasOwnProperty(e)?e:t})}),hr=["mousemove","mouseup"],dr=["pointermove","pointerup"],pr=!1;function fr(t){var e=t.pointerType;return"pen"===e||"touch"===e}function gr(t){t&&(t.zrByTouch=!0)}function vr(t,e){for(var n=e,i=!1;n&&9!==n.nodeType&&!(i=n.domBelongToZr||n!==e&&n===t.painterRoot);)n=n.parentNode;return i}var yr=function(t,e){this.stopPropagation=St,this.stopImmediatePropagation=St,this.preventDefault=St,this.type=e.type,this.target=this.currentTarget=t.dom,this.pointerType=e.pointerType,this.clientX=e.clientX,this.clientY=e.clientY},mr={mousedown:function(t){t=fe(this.dom,t),this.__mayPointerCapture=[t.zrX,t.zrY],this.trigger("mousedown",t)},mousemove:function(t){t=fe(this.dom,t);var e=this.__mayPointerCapture;!e||t.zrX===e[0]&&t.zrY===e[1]||this.__togglePointerCapture(!0),this.trigger("mousemove",t)},mouseup:function(t){t=fe(this.dom,t),this.__togglePointerCapture(!1),this.trigger("mouseup",t)},mouseout:function(t){vr(this,(t=fe(this.dom,t)).toElement||t.relatedTarget)||(this.__pointerCapturing&&(t.zrEventControl="no_globalout"),this.trigger("mouseout",t))},wheel:function(t){pr=!0,t=fe(this.dom,t),this.trigger("mousewheel",t)},mousewheel:function(t){pr||(t=fe(this.dom,t),this.trigger("mousewheel",t))},touchstart:function(t){gr(t=fe(this.dom,t)),this.__lastTouchMoment=new Date,this.handler.processGesture(t,"start"),mr.mousemove.call(this,t),mr.mousedown.call(this,t)},touchmove:function(t){gr(t=fe(this.dom,t)),this.handler.processGesture(t,"change"),mr.mousemove.call(this,t)},touchend:function(t){gr(t=fe(this.dom,t)),this.handler.processGesture(t,"end"),mr.mouseup.call(this,t),+new Date-+this.__lastTouchMoment<300&&mr.click.call(this,t)},pointerdown:function(t){mr.mousedown.call(this,t)},pointermove:function(t){fr(t)||mr.mousemove.call(this,t)},pointerup:function(t){mr.mouseup.call(this,t)},pointerout:function(t){fr(t)||mr.mouseout.call(this,t)}};B(["click","dblclick","contextmenu"],function(t){mr[t]=function(e){e=fe(this.dom,e),this.trigger(t,e)}});var xr={pointermove:function(t){fr(t)||xr.mousemove.call(this,t)},pointerup:function(t){xr.mouseup.call(this,t)},mousemove:function(t){this.trigger("mousemove",t)},mouseup:function(t){var e=this.__pointerCapturing;this.__togglePointerCapture(!1),this.trigger("mouseup",t),e&&(t.zrEventControl="only_globalout",this.trigger("mouseout",t))}};function _r(t,e){var n=e.domHandlers;r.pointerEventsSupported?B(cr.pointer,function(i){wr(e,i,function(e){n[i].call(t,e)})}):(r.touchEventsSupported&&B(cr.touch,function(i){wr(e,i,function(r){n[i].call(t,r),function(t){t.touching=!0,null!=t.touchTimer&&(clearTimeout(t.touchTimer),t.touchTimer=null),t.touchTimer=setTimeout(function(){t.touching=!1,t.touchTimer=null},700)}(e)})}),B(cr.mouse,function(i){wr(e,i,function(r){r=pe(r),e.touching||n[i].call(t,r)})}))}function br(t,e){function n(n){wr(e,n,function(i){i=pe(i),vr(t,i.target)||(i=function(t,e){return fe(t.dom,new yr(t,e),!0)}(t,i),e.domHandlers[n].call(t,i))},{capture:!0})}r.pointerEventsSupported?B(dr,n):r.touchEventsSupported||B(hr,n)}function wr(t,e,n,i){t.mounted[e]=n,t.listenerOpts[e]=i,ge(t.domTarget,e,n,i)}function Sr(t){var e=t.mounted;for(var n in e)e.hasOwnProperty(n)&&ve(t.domTarget,n,e[n],t.listenerOpts[n]);t.mounted={}}var Mr=function(t,e){this.mounted={},this.listenerOpts={},this.touching=!1,this.domTarget=t,this.domHandlers=e},Ir=function(t){function e(e,n){var i=t.call(this)||this;return i.__pointerCapturing=!1,i.dom=e,i.painterRoot=n,i._localHandlerScope=new Mr(e,mr),ur&&(i._globalHandlerScope=new Mr(document,xr)),_r(i,i._localHandlerScope),i}return n(e,t),e.prototype.dispose=function(){Sr(this._localHandlerScope),ur&&Sr(this._globalHandlerScope)},e.prototype.setCursor=function(t){this.dom.style&&(this.dom.style.cursor=t||"default")},e.prototype.__togglePointerCapture=function(t){if(this.__mayPointerCapture=null,ur&&+this.__pointerCapturing^+t){this.__pointerCapturing=t;var e=this._globalHandlerScope;t?br(this,e):Sr(e)}},e}($t),Tr=1;r.hasGlobalWindow&&(Tr=Math.max(window.devicePixelRatio||window.screen&&window.screen.deviceXDPI/window.screen.logicalXDPI||1,1));var Cr=Tr,Dr="#333",Ar="#ccc",kr=Se,Lr=5e-5;function Pr(t){return t>Lr||t<-5e-5}var Or=[],Rr=[],Nr=[1,0,0,1,0,0],Er=Math.abs,zr=function(){function t(){}var e;return t.prototype.getLocalTransform=function(t){return Br(this,t)},t.prototype.setPosition=function(t){this.x=t[0],this.y=t[1]},t.prototype.setScale=function(t){this.scaleX=t[0],this.scaleY=t[1]},t.prototype.setSkew=function(t){this.skewX=t[0],this.skewY=t[1]},t.prototype.setOrigin=function(t){this.originX=t[0],this.originY=t[1]},t.prototype.needLocalTransform=function(){return Pr(this.rotation)||Pr(this.x)||Pr(this.y)||Pr(this.scaleX-1)||Pr(this.scaleY-1)||Pr(this.skewX)||Pr(this.skewY)},t.prototype.updateTransform=function(){var t=this.parent&&this.parent.transform,e=this.needLocalTransform(),n=this.transform;e||t?(n=n||[1,0,0,1,0,0],e?this.getLocalTransform(n):kr(n),t&&(e?Ie(n,t,n):Me(n,t)),this.transform=n,this._resolveGlobalScaleRatio(n),this.invTransform=this.invTransform||[1,0,0,1,0,0],Ae(this.invTransform,n)):n&&(kr(n),this.invTransform=null)},t.prototype._resolveGlobalScaleRatio=function(t){var e=this.globalScaleRatio;if(null!=e&&1!==e){this.getGlobalScale(Or);var n=Or[0]<0?-1:1,i=Or[1]<0?-1:1,r=((Or[0]-n)*e+n)/Or[0]||0,o=((Or[1]-i)*e+i)/Or[1]||0;t[0]*=r,t[1]*=r,t[2]*=o,t[3]*=o}},t.prototype.getComputedTransform=function(){for(var t=this,e=[];t;)e.push(t),t=t.parent;for(;t=e.pop();)t.updateTransform();return this.transform},t.prototype.setLocalTransform=function(t){if(t){var e=t[0]*t[0]+t[1]*t[1],n=t[2]*t[2]+t[3]*t[3],i=Math.atan2(t[1],t[0]),r=Math.PI/2+i-Math.atan2(t[3],t[2]);n=Math.sqrt(n)*Math.cos(r),e=Math.sqrt(e),this.skewX=r,this.skewY=0,this.rotation=-i,this.x=+t[4],this.y=+t[5],this.scaleX=e,this.scaleY=n,this.originX=0,this.originY=0}},t.prototype.decomposeTransform=function(){if(this.transform){var t=this.parent,e=this.transform;t&&t.transform&&(t.invTransform=t.invTransform||[1,0,0,1,0,0],Ie(Rr,t.invTransform,e),e=Rr);var n=this.originX,i=this.originY;(n||i)&&(Nr[4]=n,Nr[5]=i,Ie(Rr,e,Nr),Rr[4]-=n,Rr[5]-=i,e=Rr),this.setLocalTransform(e)}},t.prototype.getGlobalScale=function(t){var e=this.transform;return t=t||[],e?(t[0]=Math.sqrt(e[0]*e[0]+e[1]*e[1]),t[1]=Math.sqrt(e[2]*e[2]+e[3]*e[3]),e[0]<0&&(t[0]=-t[0]),e[3]<0&&(t[1]=-t[1]),t):(t[0]=1,t[1]=1,t)},t.prototype.transformCoordToLocal=function(t,e){var n=[t,e],i=this.invTransform;return i&&Yt(n,n,i),n},t.prototype.transformCoordToGlobal=function(t,e){var n=[t,e],i=this.transform;return i&&Yt(n,n,i),n},t.prototype.getLineScale=function(){var t=this.transform;return t&&Er(t[0]-1)>1e-10&&Er(t[3]-1)>1e-10?Math.sqrt(Er(t[0]*t[3]-t[2]*t[1])):1},t.prototype.copyTransform=function(t){Hr(this,t)},t.getLocalTransform=function(t,e){e=e||[];var n=t.originX||0,i=t.originY||0,r=t.scaleX,o=t.scaleY,a=t.anchorX,s=t.anchorY,l=t.rotation||0,u=t.x,c=t.y,h=t.skewX?Math.tan(t.skewX):0,d=t.skewY?Math.tan(-t.skewY):0;if(n||i||a||s){var p=n+a,f=i+s;e[4]=-p*r-h*f*o,e[5]=-f*o-d*p*r}else e[4]=e[5]=0;return e[0]=r,e[3]=o,e[1]=d*r,e[2]=h*o,l&&Ce(e,e,l),e[4]+=n+u,e[5]+=i+c,e},t.initDefaultProps=((e=t.prototype).scaleX=e.scaleY=e.globalScaleRatio=1,void(e.x=e.y=e.originX=e.originY=e.skewX=e.skewY=e.rotation=e.anchorX=e.anchorY=0)),t}(),Br=zr.getLocalTransform;function Vr(){return new zr}var Gr,Fr=["x","y","originX","originY","anchorX","anchorY","rotation","scaleX","scaleY","skewX","skewY"];function Hr(t,e){return L(t,e,Fr)}function Wr(t){Gr||(Gr=new ni(100)),t=t||a;var e=Gr.get(t);return e||(e={font:t,strWidthCache:new ni(500),asciiWidthMap:null,asciiWidthMapTried:!1,stWideCharWidth:c.measureText("国",t).width,asciiCharWidth:c.measureText("a",t).width},Gr.put(t,e)),e}var Ur=0,Yr=5;function Xr(t,e){return t.asciiWidthMapTried||(t.asciiWidthMap=function(t){if(!(Ur>=Yr)){t=t||a;for(var e=[],n=+new Date,i=0;i<=127;i++)e[i]=c.measureText(String.fromCharCode(i),t).width;var r=+new Date-n;return r>16?Ur=Yr:r>2&&Ur++,e}}(t.font),t.asciiWidthMapTried=!0),0<=e&&e<=127?null!=t.asciiWidthMap?t.asciiWidthMap[e]:t.asciiCharWidth:t.stWideCharWidth}function Zr(t,e){var n=t.strWidthCache,i=n.get(e);return null==i&&(i=c.measureText(e,t.font).width,n.put(e,i)),i}function jr(t,e,n,i){var r=Zr(Wr(e),t),o=Jr(e),a=Kr(0,r,n),s=$r(0,o,i);return new Xe(a,s,r,o)}function qr(t,e,n,i){var r=((t||"")+"").split("\n");if(1===r.length)return jr(r[0],e,n,i);for(var o=new Xe(0,0,0,0),a=0;a=0?parseFloat(t)/100*e:parseFloat(t):t}function to(t,e,n){var i=e.position||"inside",r=null!=e.distance?e.distance:5,o=n.height,a=n.width,s=o/2,l=n.x,u=n.y,c="left",h="top";if(i instanceof Array)l+=Qr(i[0],n.width),u+=Qr(i[1],n.height),c=null,h=null;else switch(i){case"left":l-=r,u+=s,c="right",h="middle";break;case"right":l+=r+a,u+=s,h="middle";break;case"top":l+=a/2,u-=r,c="center",h="bottom";break;case"bottom":l+=a/2,u+=o+r,c="center";break;case"inside":l+=a/2,u+=s,c="center",h="middle";break;case"insideLeft":l+=r,u+=s,h="middle";break;case"insideRight":l+=a-r,u+=s,c="right",h="middle";break;case"insideTop":l+=a/2,u+=r,c="center";break;case"insideBottom":l+=a/2,u+=o-r,c="center",h="bottom";break;case"insideTopLeft":l+=r,u+=r;break;case"insideTopRight":l+=a-r,u+=r,c="right";break;case"insideBottomLeft":l+=r,u+=o-r,h="bottom";break;case"insideBottomRight":l+=a-r,u+=o-r,c="right",h="bottom"}return(t=t||{}).x=l,t.y=u,t.align=c,t.verticalAlign=h,t}var eo="__zr_normal__",no=Fr.concat(["ignore"]),io=G(Fr,function(t,e){return t[e]=!0,t},{ignore:!1}),ro={},oo=new Xe(0,0,0,0),ao=[],so=function(){function t(t){this.id=I(),this.animators=[],this.currentStates=[],this.states={},this._init(t)}return t.prototype._init=function(t){this.attr(t)},t.prototype.drift=function(t,e,n){switch(this.draggable){case"horizontal":e=0;break;case"vertical":t=0}var i=this.transform;i||(i=this.transform=[1,0,0,1,0,0]),i[4]+=t,i[5]+=e,this.decomposeTransform(),this.markRedraw()},t.prototype.beforeUpdate=function(){},t.prototype.afterUpdate=function(){},t.prototype.update=function(){this.updateTransform(),this.__dirty&&this.updateInnerText()},t.prototype.updateInnerText=function(t){var e=this._textContent;if(e&&(!e.ignore||t)){this.textConfig||(this.textConfig={});var n=this.textConfig,i=n.local,r=e.innerTransformable,o=void 0,a=void 0,s=!1;r.parent=i?this:null;var l=!1;r.copyTransform(e);var u=null!=n.position,c=n.autoOverflowArea,h=void 0;if((c||u)&&(h=oo,n.layoutRect?h.copy(n.layoutRect):h.copy(this.getBoundingRect()),i||h.applyTransform(this.transform)),u){this.calculateTextPosition?this.calculateTextPosition(ro,n,h):to(ro,n,h),r.x=ro.x,r.y=ro.y,o=ro.align,a=ro.verticalAlign;var d=n.origin;if(d&&null!=n.rotation){var p=void 0,f=void 0;"center"===d?(p=.5*h.width,f=.5*h.height):(p=Qr(d[0],h.width),f=Qr(d[1],h.height)),l=!0,r.originX=-r.x+p+(i?0:h.x),r.originY=-r.y+f+(i?0:h.y)}}null!=n.rotation&&(r.rotation=n.rotation);var g=n.offset;g&&(r.x+=g[0],r.y+=g[1],l||(r.originX=-g[0],r.originY=-g[1]));var v=this._innerTextDefaultStyle||(this._innerTextDefaultStyle={});if(c){var y=v.overflowRect=v.overflowRect||new Xe(0,0,0,0);r.getLocalTransform(ao),Ae(ao,ao),Xe.copy(y,h),y.applyTransform(ao)}else v.overflowRect=null;var m=void 0,x=void 0,_=void 0;(null==n.inside?"string"==typeof n.position&&n.position.indexOf("inside")>=0:n.inside)&&this.canBeInsideText()?(m=n.insideFill,x=n.insideStroke,null!=m&&"auto"!==m||(m=this.getInsideTextFill()),null!=x&&"auto"!==x||(x=this.getInsideTextStroke(m),_=!0)):(m=n.outsideFill,x=n.outsideStroke,null!=m&&"auto"!==m||(m=this.getOutsideFill()),null!=x&&"auto"!==x||(x=this.getOutsideStroke(m),_=!0)),(m=m||"#000")===v.fill&&x===v.stroke&&_===v.autoStroke&&o===v.align&&a===v.verticalAlign||(s=!0,v.fill=m,v.stroke=x,v.autoStroke=_,v.align=o,v.verticalAlign=a,e.setDefaultTextStyle(v)),e.__dirty|=1,s&&e.dirtyStyle(!0)}},t.prototype.canBeInsideText=function(){return!0},t.prototype.getInsideTextFill=function(){return"#fff"},t.prototype.getInsideTextStroke=function(t){return"#000"},t.prototype.getOutsideFill=function(){return this.__zr&&this.__zr.isDarkMode()?Ar:Dr},t.prototype.getOutsideStroke=function(t){var e=this.__zr&&this.__zr.getBackgroundColor(),n="string"==typeof e&&gi(e);n||(n=[255,255,255,1]);for(var i=n[3],r=this.__zr.isDarkMode(),o=0;o<3;o++)n[o]=n[o]*i+(r?0:255)*(1-i);return n[3]=1,Mi(n,"rgba")},t.prototype.traverse=function(t,e){},t.prototype.attrKV=function(t,e){"textConfig"===t?this.setTextConfig(e):"textContent"===t?this.setTextContent(e):"clipPath"===t?this.setClipPath(e):"extra"===t?(this.extra=this.extra||{},k(this.extra,e)):this[t]=e},t.prototype.hide=function(){this.ignore=!0,this.markRedraw()},t.prototype.show=function(){this.ignore=!1,this.markRedraw()},t.prototype.attr=function(t,e){if("string"==typeof t)this.attrKV(t,e);else if($(t))for(var n=W(t),i=0;i0},t.prototype.getState=function(t){return this.states[t]},t.prototype.ensureState=function(t){var e=this.states;return e[t]||(e[t]={}),e[t]},t.prototype.clearStates=function(t){this.useState(eo,!1,t)},t.prototype.useState=function(t,e,n,i){var r=t===eo;if(this.hasState()||!r){var o=this.currentStates,a=this.stateTransition;if(!(R(o,t)>=0)||!e&&1!==o.length){var s;if(this.stateProxy&&!r&&(s=this.stateProxy(t)),s||(s=this.states&&this.states[t]),s||r){r||this.saveCurrentToNormalState(s);var l=this._textContent,u=po(this,l,s,i);u&&!this.__inHover&&(this.__inHover=u),this._applyStateObj(t,s,this._normalState,e,go(this,n,a),a);var c=this._textGuide;return l&&l.useState(t,e,n,!!u),c&&c.useState(t,e,n,!!u),r?(this.currentStates=[],this._normalState={}):e?this.currentStates.push(t):this.currentStates=[t],this._updateAnimationTargets(),this.markRedraw(),!u&&this.__inHover&&(this.__inHover=0,this.__dirty&=-2),s}T("State "+t+" not exists.")}}},t.prototype.useStates=function(t,e,n){if(t.length){var i=[],r=this.currentStates,o=t.length,a=o===r.length;if(a)for(var s=0;s=0){var n=this.currentStates.slice();n.splice(e,1),this.useStates(n)}},t.prototype.replaceState=function(t,e,n){var i=this.currentStates.slice(),r=R(i,t),o=R(i,e)>=0;r>=0?o?i.splice(r,1):i[r]=e:n&&!o&&i.push(e),this.useStates(i)},t.prototype.toggleState=function(t,e){e?this.useState(t,!0):this.removeState(t)},t.prototype._mergeStates=function(t){for(var e,n={},i=0;i=0&&e.splice(n,1)}),this.animators.push(t),n&&n.animation.addAnimator(t),n&&n.wakeUp()},t.prototype.updateDuringAnimation=function(t){this.markRedraw()},t.prototype.stopAnimation=function(t,e){for(var n=this.animators,i=n.length,r=[],o=0;o0&&n.during&&o[0].during(function(t,e){n.during(e)});for(var d=0;d0||r.force&&!a.length){var w,S=void 0,M=void 0,I=void 0;if(s){M={},d&&(S={});for(_=0;_0}E(so,$t),E(so,zr);var vo=function(t){function e(e){var n=t.call(this)||this;return n.isGroup=!0,n._children=[],n.attr(e),n}return n(e,t),e.prototype.childrenRef=function(){return this._children},e.prototype.children=function(){return this._children.slice()},e.prototype.childAt=function(t){return this._children[t]},e.prototype.childOfName=function(t){for(var e=this._children,n=0;n=0&&(n.splice(i,0,t),this._doAdd(t))}return this},e.prototype.replace=function(t,e){var n=R(this._children,t);return n>=0&&this.replaceAt(e,n),this},e.prototype.replaceAt=function(t,e){var n=this._children,i=n[e];if(t&&t!==this&&t.parent!==this&&t!==i){n[e]=t,i.parent=null;var r=this.__zr;r&&i.removeSelfFromZr(r),this._doAdd(t)}return this},e.prototype._doAdd=function(t){t.parent&&t.parent.remove(t),t.parent=this;var e=this.__zr;e&&e!==t.__zr&&t.addSelfToZr(e),e&&e.refresh()},e.prototype.remove=function(t){var e=this.__zr,n=this._children,i=R(n,t);return i<0||(n.splice(i,1),t.parent=null,e&&t.removeSelfFromZr(e),e&&e.refresh()),this},e.prototype.removeAll=function(){for(var t=this._children,e=this.__zr,n=0;n0&&(this._stillFrameAccum++,this._stillFrameAccum>this._sleepAfterStill&&this.animation.stop())},t.prototype.setSleepAfterStill=function(t){this._sleepAfterStill=t},t.prototype.wakeUp=function(){this._disposed||(this.animation.start(),this._stillFrameAccum=0)},t.prototype.refreshHover=function(){this._needsRefreshHover=!0},t.prototype.refreshHoverImmediately=function(){this._disposed||this._refresh({animUpdate:!1,refresh:!1,refreshHover:!0})},t.prototype.resize=function(t){this._disposed||(t=t||{},this.painter.resize(t.width,t.height),this.handler.resize())},t.prototype.clearAnimation=function(){this._disposed||this.animation.clear()},t.prototype.getWidth=function(){if(!this._disposed)return this.painter.getWidth()},t.prototype.getHeight=function(){if(!this._disposed)return this.painter.getHeight()},t.prototype.setCursorStyle=function(t){this._disposed||this.handler.setCursorStyle(t)},t.prototype.findHover=function(t,e){if(!this._disposed)return this.handler.findHover(t,e)},t.prototype.on=function(t,e,n){return this._disposed||this.handler.on(t,e,n),this},t.prototype.off=function(t,e){this._disposed||this.handler.off(t,e)},t.prototype.trigger=function(t,e){this._disposed||this.handler.trigger(t,e)},t.prototype.clear=function(){if(!this._disposed){for(var t=this.storage.getRoots(),e=0;e0){if(t<=r)return a;if(t>=o)return s}else{if(t>=r)return a;if(t<=o)return s}else{if(t===r)return a;if(t===o)return s}return(t-r)/l*u+a}var Vo=function(t,e,n){switch(t){case"center":case"middle":t="50%";break;case"left":case"top":t="0%";break;case"right":case"bottom":t="100%"}return Go(t,e,n)};function Go(t,e,n){return j(t)?Fo(t)?parseFloat(t)/100*e+(n||0):parseFloat(t):null==t?NaN:+t}function Fo(t){return!!(e=t,e.replace(/^\s+|\s+$/g,"")).match(/%$/);var e}function Ho(t,e,n){return isNaN(e)?n?""+t:+t:(e=Co(Do(0,e),20),t=(+t).toFixed(e),n?t:+t)}function Wo(t){return t.sort(function(t,e){return t-e}),t}function Uo(t){if(t=+t,isNaN(t))return 0;if(t>1e-14)for(var e=1,n=0;n<15;n++,e*=10)if(ko(t*e)/e===t)return n;return Yo(t)}function Yo(t){var e=t.toString().toLowerCase(),n=e.indexOf("e"),i=n>0?+e.slice(n+1):0,r=n>0?n:e.length,o=e.indexOf(".");return Do(0,(o<0?0:r-1-o)-i)}function Xo(t,e,n){var i=Ao(t[1]-t[0]);if(!isFinite(i)||0===i)return NaN;var r=Ro(2*Ao(n||1)*Ao(i))/No,o=Ro(Ao(e))/No,a=Do(0,Po(-r+o));return isFinite(a)||(a=NaN),a}function Zo(t,e){var n=G(t,function(t,e){return t+(isNaN(e)?0:e)},0);if(0===n)return[];for(var i=Oo(10,e),r=V(t,function(t){return(isNaN(t)?0:t)/n*i*100}),o=100*i,a=V(r,function(t){return Lo(t)}),s=G(a,function(t,e){return t+e},0),l=V(r,function(t,e){return t-a[e]});su&&(u=l[h],c=h);++a[c],l[c]=0,++s}return V(a,function(t){return t/i})}function jo(t,e){var n=Do(Uo(t),Uo(e)),i=t+e;return n>20?i:Ho(i,n)}var qo=Oo(2,53)-1;function Ko(t){var e=2*Eo;return(t%e+e)%e}function $o(t){return t>-1e-4&&t=10&&e++,e}function na(t,e){var n=ea(t),i=Oo(10,n),r=t/i;return Ho(t=(2===e?1:e?r<1.5?1:r<2.5?2:r<4?3:r<7?5:10:r<1?1:r<2?2:r<3?3:r<5?5:10)*i,-n)}function ia(t,e){var n=(t.length-1)*e+1,i=Lo(n),r=+t[i-1],o=n-i;return o?r+o*(t[i]-r):r}function ra(t){t.sort(function(t,e){return s(t,e,0)?-1:1});for(var e=-1/0,n=1,i=0;i0?t.length:0,this.item=null,this.key=NaN,this},t.prototype.next=function(){return(this._step>0?this._idx=this._end)&&(this.item=this._list[this._idx],this.key=this._idx=this._idx+this._step,!0)},t}();function Ua(t){t.option=t.parentModel=t.ecModel=null}function Ya(){return[1/0,-1/0]}function Xa(t,e){qa(e)&&(et[1]&&(t[1]=e))}function Za(t,e){qa(e)&&et[1]&&(t[1]=e)}function qa(t){return null!=t&&isFinite(t)}function Ka(t,e){return qa(t)&&qa(e)&&t<=e}function $a(t){Ka(t[0],t[1])&&t[0]>t[1]&&(t[0]=t[1])}function Ja(){var t="__ec_once_"+Qa++;return function(e,n){wt(e,t)||(e[t]=1,n())}}var Qa=sa();function ts(t,e,n){var i=mt(),r=0;B(t,function(o){var a=e(o);var s=i.get(a)||0;n&&n(o,s),s||n||(t[r++]=o),i.set(a,s+1)}),n||(t.length=r)}function es(t){return t.value+""}function ns(t){return t+""}function is(t,e){return at(e,!0)?t.seriesIndex+2:0}function rs(t,e,n){var i=t.getData().count();return{progressiveRender:n.progressiveEnabled&&e.incrementalPrepareRender&&i>=n.threshold,large:t.get("large")&&i>=t.get("largeThreshold"),modDataCount:"mod"===t.get("progressiveChunkMode")?t.getData().count():null}}function os(t,e){return{seriesType:t,overallReset:e}}function as(t){return{overallReset:t}}var ss="___EC__COMPONENT__CONTAINER___",ls="___EC__EXTENDED_CLASS___";function us(t){var e={main:"",sub:""};if(t){var n=t.split(".");e.main=n[0]||"",e.sub=n[1]||""}return e}function cs(t,e){t.$constructor=t,t.extend=function(t){var e,i,r=this;return Z(i=r)&&/^class\s/.test(Function.prototype.toString.call(i))?e=function(t){function e(){return t.apply(this,arguments)||this}return n(e,t),e}(r):(e=function(){(t.$constructor||r).apply(this,arguments)},N(e,this)),k(e.prototype,t),e[ls]=!0,e.extend=this.extend,e.superCall=ps,e.superApply=fs,e.superClass=r,e}}function hs(t,e){t.extend=e.extend}var ds=Math.round(10*Math.random());function ps(t,e){for(var n=[],i=2;i=0||r&&R(r,s)<0)){var l=n.getShallow(s,e);null!=l&&(o[t[a][0]]=l)}}return o}}var ys=vs([["fill","color"],["shadowBlur"],["shadowOffsetX"],["shadowOffsetY"],["opacity"],["shadowColor"]]),ms=function(){function t(){}return t.prototype.getAreaStyle=function(t,e){return ys(this,t,e)},t}(),xs=new ni(50);function _s(t){if("string"==typeof t){var e=xs.get(t);return e&&e.image}return t}function bs(t,e,n,i,r){if(t){if("string"==typeof t){if(e&&e.__zrImageSrc===t||!n)return e;var o=xs.get(t),a={hostEl:n,cb:i,cbPayload:r};return o?!Ss(e=o.image)&&o.pending.push(a):((e=c.loadImage(t,ws,ws)).__zrImageSrc=t,xs.put(t,e.__cachedImgObj={image:e,pending:[a]})),e}return t}return e}function ws(){var t=this.__cachedImgObj;this.onload=this.onerror=this.__cachedImgObj=null;for(var e=0;e=s;u++)l-=s;var c=Zr(a,n);return c>l&&(n="",c=0),l=t-c,r.ellipsis=n,r.ellipsisWidth=c,r.contentWidth=l,r.containerWidth=t,r}function Cs(t,e,n){var i=n.containerWidth,r=n.contentWidth,o=n.fontMeasureInfo;if(!i)return t.textLine="",void(t.isTruncated=!1);var a=Zr(o,e);if(a<=i)return t.textLine=e,void(t.isTruncated=!1);for(var s=0;;s++){if(a<=r||s>=n.maxIterations){e+=n.ellipsis;break}var l=0===s?Ds(e,r,o):a>0?Math.floor(e.length*r/a):0;a=Zr(o,e=e.substr(0,l))}""===e&&(e=n.placeholder),t.textLine=e,t.isTruncated=!0}function Ds(t,e,n){for(var i=0,r=0,o=t.length;r0&&f+i.accumWidth>i.width&&(o=e.split("\n"),h=!0),i.accumWidth=f}else{var g=Ns(e,c,i.width,i.breakAll,i.accumWidth);i.accumWidth=g.accumWidth+p,a=g.linesWidths,o=g.lines}}o||(o=e.split("\n"));for(var v=Wr(c),y=0;y=32&&e<=591||e>=880&&e<=4351||e>=4608&&e<=5119||e>=7680&&e<=8303}(t)||!!Os[t]}function Ns(t,e,n,i,r){for(var o=[],a=[],s="",l="",u=0,c=0,h=Wr(e),d=0;dn:r+c+f>n)?c?(s||l)&&(g?(s||(s=l,l="",c=u=0),o.push(s),a.push(c-u),l+=p,s="",c=u+=f):(l&&(s+=l,l="",u=0),o.push(s),a.push(c),s=p,c=f)):g?(o.push(l),a.push(u),l=p,u=f):(o.push(p),a.push(f)):(c+=f,g?(l+=p,u+=f):(l&&(s+=l,l="",u=0),s+=p))}else l&&(s+=l,c+=u),o.push(s),a.push(c),s="",l="",u=0,c=0}return l&&(s+=l),s&&(o.push(s),a.push(c)),1===o.length&&(c+=r),{accumWidth:c,lines:o,linesWidths:a}}function Es(t,e,n,i,r,o){if(t.baseX=n,t.baseY=i,t.outerWidth=t.outerHeight=null,e){var a=2*e.width,s=2*e.height;Xe.set(zs,Kr(n,a,r),$r(i,s,o),a,s),Xe.intersect(e,zs,null,Bs);var l=Bs.outIntersectRect;t.outerWidth=l.width,t.outerHeight=l.height,t.baseX=Kr(l.x,l.width,r,!0),t.baseY=$r(l.y,l.height,o,!0)}}var zs=new Xe(0,0,0,0),Bs={outIntersectRect:{},clamp:!0};function Vs(t){return null!=t?t+="":t=""}function Gs(t,e,n,i){var r=new Xe(Kr(t.x||0,e,t.textAlign),$r(t.y||0,n,t.textBaseline),e,n),o=null!=i?i:Fs(t)?t.lineWidth:0;return o>0&&(r.x-=o/2,r.y-=o/2,r.width+=o,r.height+=o),r}function Fs(t){var e=t.stroke;return null!=e&&"none"!==e&&t.lineWidth>0}var Hs="__zr_style_"+Math.round(10*Math.random()),Ws={shadowBlur:0,shadowOffsetX:0,shadowOffsetY:0,shadowColor:"#000",opacity:1,blend:"source-over"},Us={style:{shadowBlur:!0,shadowOffsetX:!0,shadowOffsetY:!0,shadowColor:!0,opacity:!0}};Ws[Hs]=!0;var Ys=["z","z2","invisible"],Xs=["invisible"],Zs=function(t){function e(e){return t.call(this,e)||this}var i;return n(e,t),e.prototype._init=function(e){for(var n=W(e),i=0;i1e-4)return s[0]=t-n,s[1]=e-i,l[0]=t+n,void(l[1]=e+i);if(el[0]=Qs(r)*n+t,el[1]=Js(r)*i+e,nl[0]=Qs(o)*n+t,nl[1]=Js(o)*i+e,u(s,el,nl),c(l,el,nl),(r%=tl)<0&&(r+=tl),(o%=tl)<0&&(o+=tl),r>o&&!a?o+=tl:rr&&(il[0]=Qs(p)*n+t,il[1]=Js(p)*i+e,u(s,il,s),c(l,il,l))}var hl={M:1,L:2,C:3,Q:4,A:5,Z:6,R:7},dl=[],pl=[],fl=[],gl=[],vl=[],yl=[],ml=Math.min,xl=Math.max,_l=Math.cos,bl=Math.sin,wl=Math.abs,Sl=Math.PI,Ml=2*Sl,Il="undefined"!=typeof Float32Array,Tl=[];function Cl(t){return Math.round(t/Sl*1e8)/1e8%2*Sl}function Dl(t,e){var n=Cl(t[0]);n<0&&(n+=Ml);var i=n-t[0],r=t[1];r+=i,!e&&r-n>=Ml?r=n+Ml:e&&n-r>=Ml?r=n-Ml:!e&&n>r?r=n+(Ml-Cl(n-r)):e&&n0&&(this._ux=wl(n/Cr/t)||0,this._uy=wl(n/Cr/e)||0)},t.prototype.setDPR=function(t){this.dpr=t},t.prototype.setContext=function(t){this._ctx=t},t.prototype.getContext=function(){return this._ctx},t.prototype.beginPath=function(){return this._ctx&&this._ctx.beginPath(),this.reset(),this},t.prototype.reset=function(){this._saveData&&(this._len=0),this._pathSegLen&&(this._pathSegLen=null,this._pathLen=0),this._version++},t.prototype.moveTo=function(t,e){return this._drawPendingPt(),this.addData(hl.M,t,e),this._ctx&&this._ctx.moveTo(t,e),this._x0=t,this._y0=e,this._xi=t,this._yi=e,this},t.prototype.lineTo=function(t,e){var n=wl(t-this._xi),i=wl(e-this._yi),r=n>this._ux||i>this._uy;if(this.addData(hl.L,t,e),this._ctx&&r&&this._ctx.lineTo(t,e),r)this._xi=t,this._yi=e,this._pendingPtDist=0;else{var o=n*n+i*i;o>this._pendingPtDist&&(this._pendingPtX=t,this._pendingPtY=e,this._pendingPtDist=o)}return this},t.prototype.bezierCurveTo=function(t,e,n,i,r,o){return this._drawPendingPt(),this.addData(hl.C,t,e,n,i,r,o),this._ctx&&this._ctx.bezierCurveTo(t,e,n,i,r,o),this._xi=r,this._yi=o,this},t.prototype.quadraticCurveTo=function(t,e,n,i){return this._drawPendingPt(),this.addData(hl.Q,t,e,n,i),this._ctx&&this._ctx.quadraticCurveTo(t,e,n,i),this._xi=n,this._yi=i,this},t.prototype.arc=function(t,e,n,i,r,o){this._drawPendingPt(),Tl[0]=i,Tl[1]=r,Dl(Tl,o),i=Tl[0];var a=(r=Tl[1])-i;return this.addData(hl.A,t,e,n,n,i,a,0,o?0:1),this._ctx&&this._ctx.arc(t,e,n,i,r,o),this._xi=_l(r)*n+t,this._yi=bl(r)*n+e,this},t.prototype.arcTo=function(t,e,n,i,r){return this._drawPendingPt(),this._ctx&&this._ctx.arcTo(t,e,n,i,r),this},t.prototype.rect=function(t,e,n,i){return this._drawPendingPt(),this._ctx&&this._ctx.rect(t,e,n,i),this.addData(hl.R,t,e,n,i),this},t.prototype.closePath=function(){this._drawPendingPt(),this.addData(hl.Z);var t=this._ctx,e=this._x0,n=this._y0;return t&&t.closePath(),this._xi=e,this._yi=n,this},t.prototype.fill=function(t){t&&t.fill(),this.toStatic()},t.prototype.stroke=function(t){t&&t.stroke(),this.toStatic()},t.prototype.len=function(){return this._len},t.prototype.setData=function(t){if(this._saveData){var e=t.length;this.data&&this.data.length===e||!Il||(this.data=new Float32Array(e));for(var n=0;n0&&o))for(var a=0;au.length&&(this._expandData(),u=this.data);for(var c=0;c0&&(this._ctx&&this._ctx.lineTo(this._pendingPtX,this._pendingPtY),this._pendingPtDist=0)},t.prototype._expandData=function(){if(!(this.data instanceof Array)){for(var t=[],e=0;e11&&(this.data=new Float32Array(t)))}},t.prototype.getBoundingRect=function(){fl[0]=fl[1]=vl[0]=vl[1]=Number.MAX_VALUE,gl[0]=gl[1]=yl[0]=yl[1]=-Number.MAX_VALUE;var t,e=this.data,n=0,i=0,r=0,o=0;for(t=0;tn||wl(v)>i||h===e-1)&&(f=Math.sqrt(A*A+v*v),r=g,o=x);break;case hl.C:var y=t[h++],m=t[h++],x=(g=t[h++],t[h++]),_=t[h++],b=t[h++];f=Un(r,o,y,m,g,x,_,b,10),r=_,o=b;break;case hl.Q:f=Kn(r,o,y=t[h++],m=t[h++],g=t[h++],x=t[h++],10),r=g,o=x;break;case hl.A:var w=t[h++],S=t[h++],M=t[h++],I=t[h++],T=t[h++],C=t[h++],D=C+T;h+=1,p&&(a=_l(T)*M+w,s=bl(T)*I+S),f=xl(M,I)*ml(Ml,Math.abs(C)),r=_l(D)*M+w,o=bl(D)*I+S;break;case hl.R:a=r=t[h++],s=o=t[h++],f=2*t[h++]+2*t[h++];break;case hl.Z:var A=a-r;v=s-o;f=Math.sqrt(A*A+v*v),r=a,o=s}f>=0&&(l[c++]=f,u+=f)}return this._pathLen=u,u},t.prototype.rebuildPath=function(t,e){var n,i,r,o,a,s,l,u,c,h,d=this.data,p=this._ux,f=this._uy,g=this._len,v=e<1,y=0,m=0,x=0;if(!v||(this._pathSegLen||this._calculateLength(),l=this._pathSegLen,u=e*this._pathLen))t:for(var _=0;_0&&(t.lineTo(c,h),x=0),b){case hl.M:n=r=d[_++],i=o=d[_++],t.moveTo(r,o);break;case hl.L:a=d[_++],s=d[_++];var S=wl(a-r),M=wl(s-o);if(S>p||M>f){if(v){if(y+(j=l[m++])>u){var I=(u-y)/j;t.lineTo(r*(1-I)+a*I,o*(1-I)+s*I);break t}y+=j}t.lineTo(a,s),r=a,o=s,x=0}else{var T=S*S+M*M;T>x&&(c=a,h=s,x=T)}break;case hl.C:var C=d[_++],D=d[_++],A=d[_++],k=d[_++],L=d[_++],P=d[_++];if(v){if(y+(j=l[m++])>u){Hn(r,C,A,L,I=(u-y)/j,dl),Hn(o,D,k,P,I,pl),t.bezierCurveTo(dl[1],pl[1],dl[2],pl[2],dl[3],pl[3]);break t}y+=j}t.bezierCurveTo(C,D,A,k,L,P),r=L,o=P;break;case hl.Q:C=d[_++],D=d[_++],A=d[_++],k=d[_++];if(v){if(y+(j=l[m++])>u){jn(r,C,A,I=(u-y)/j,dl),jn(o,D,k,I,pl),t.quadraticCurveTo(dl[1],pl[1],dl[2],pl[2]);break t}y+=j}t.quadraticCurveTo(C,D,A,k),r=A,o=k;break;case hl.A:var O=d[_++],R=d[_++],N=d[_++],E=d[_++],z=d[_++],B=d[_++],V=d[_++],G=!d[_++],F=N>E?N:E,H=wl(N-E)>.001,W=z+B,U=!1;if(v)y+(j=l[m++])>u&&(W=z+B*(u-y)/j,U=!0),y+=j;if(H&&t.ellipse?t.ellipse(O,R,N,E,V,z,W,G):t.arc(O,R,F,z,W,G),U)break t;w&&(n=_l(z)*N+O,i=bl(z)*E+R),r=_l(W)*N+O,o=bl(W)*E+R;break;case hl.R:n=r=d[_],i=o=d[_+1],a=d[_++],s=d[_++];var Y=d[_++],X=d[_++];if(v){if(y+(j=l[m++])>u){var Z=u-y;t.moveTo(a,s),t.lineTo(a+ml(Z,Y),s),(Z-=Y)>0&&t.lineTo(a+Y,s+ml(Z,X)),(Z-=X)>0&&t.lineTo(a+xl(Y-Z,0),s+X),(Z-=Y)>0&&t.lineTo(a,s+xl(X-Z,0));break t}y+=j}t.rect(a,s,Y,X);break;case hl.Z:if(v){var j;if(y+(j=l[m++])>u){I=(u-y)/j;t.lineTo(r*(1-I)+n*I,o*(1-I)+i*I);break t}y+=j}t.closePath(),r=n,o=i}}},t.prototype.clone=function(){var e=new t,n=this.data;return e.data=n.slice?n.slice():Array.prototype.slice.call(n),e._len=this._len,e},t.prototype.canSave=function(){return!!this._saveData},t.CMD=hl,t.initDefaultProps=((e=t.prototype)._saveData=!0,e._ux=0,e._uy=0,e._pendingPtDist=0,void(e._version=0)),t}();function kl(t,e,n,i,r,o,a){if(0===r)return!1;var s=r,l=0;if(a>e+s&&a>i+s||at+s&&o>n+s||oe+h&&c>i+h&&c>o+h&&c>s+h||ct+h&&u>n+h&&u>r+h&&u>a+h||ue+u&&l>i+u&&l>o+u||lt+u&&s>n+u&&s>r+u||sn||c+ur&&(r+=Nl);var d=Math.atan2(l,s);return d<0&&(d+=Nl),d>=i&&d<=r||d+Nl>=i&&d+Nl<=r}function zl(t,e,n,i,r,o){if(o>e&&o>i||or?s:0}var Bl=Al.CMD,Vl=2*Math.PI;var Gl=[-1,-1,-1],Fl=[-1,-1];function Hl(){var t=Fl[0];Fl[0]=Fl[1],Fl[1]=t}function Wl(t,e,n,i,r,o,a,s,l,u){if(u>e&&u>i&&u>o&&u>s||u1&&Hl(),p=Bn(e,i,o,s,Fl[0]),d>1&&(f=Bn(e,i,o,s,Fl[1]))),2===d?ve&&s>i&&s>o||s=0&&c<=1&&(r[l++]=c);else{var u=a*a-4*o*s;if(En(u))(c=-a/(2*o))>=0&&c<=1&&(r[l++]=c);else if(u>0){var c,h=Dn(u),d=(-a-h)/(2*o);(c=(-a+h)/(2*o))>=0&&c<=1&&(r[l++]=c),d>=0&&d<=1&&(r[l++]=d)}}return l}(e,i,o,s,Gl);if(0===l)return 0;var u=Zn(e,i,o);if(u>=0&&u<=1){for(var c=0,h=Yn(e,i,o,u),d=0;dn||s<-n)return 0;var l=Math.sqrt(n*n-s*s);Gl[0]=-l,Gl[1]=l;var u=Math.abs(i-r);if(u<1e-4)return 0;if(u>=Vl-1e-4){i=0,r=Vl;var c=o?1:-1;return a>=Gl[0]+t&&a<=Gl[1]+t?c:0}if(i>r){var h=i;i=r,r=h}i<0&&(i+=Vl,r+=Vl);for(var d=0,p=0;p<2;p++){var f=Gl[p];if(f+t>a){var g=Math.atan2(s,f);c=o?1:-1;g<0&&(g=Vl+g),(g>=i&&g<=r||g+Vl>=i&&g+Vl<=r)&&(g>Math.PI/2&&g<1.5*Math.PI&&(c=-c),d+=c)}}return d}function Xl(t,e,n,i,r){for(var o,a,s,l,u=t.data,c=t.len(),h=0,d=0,p=0,f=0,g=0,v=0;v1&&(n||(h+=zl(d,p,f,g,i,r))),m&&(f=d=u[v],g=p=u[v+1]),y){case Bl.M:d=f=u[v++],p=g=u[v++];break;case Bl.L:if(n){if(kl(d,p,u[v],u[v+1],e,i,r))return!0}else h+=zl(d,p,u[v],u[v+1],i,r)||0;d=u[v++],p=u[v++];break;case Bl.C:if(n){if(Ll(d,p,u[v++],u[v++],u[v++],u[v++],u[v],u[v+1],e,i,r))return!0}else h+=Wl(d,p,u[v++],u[v++],u[v++],u[v++],u[v],u[v+1],i,r)||0;d=u[v++],p=u[v++];break;case Bl.Q:if(n){if(Pl(d,p,u[v++],u[v++],u[v],u[v+1],e,i,r))return!0}else h+=Ul(d,p,u[v++],u[v++],u[v],u[v+1],i,r)||0;d=u[v++],p=u[v++];break;case Bl.A:var x=u[v++],_=u[v++],b=u[v++],w=u[v++],S=u[v++],M=u[v++];v+=1;var I=!!(1-u[v++]);o=Math.cos(S)*b+x,a=Math.sin(S)*w+_,m?(f=o,g=a):h+=zl(d,p,o,a,i,r);var T=(i-x)*w/b+x;if(n){if(El(x,_,w,S,S+M,I,e,T,r))return!0}else h+=Yl(x,_,w,S,S+M,I,T,r);d=Math.cos(S+M)*b+x,p=Math.sin(S+M)*w+_;break;case Bl.R:if(f=d=u[v++],g=p=u[v++],o=f+u[v++],a=g+u[v++],n){if(kl(f,g,o,g,e,i,r)||kl(o,g,o,a,e,i,r)||kl(o,a,f,a,e,i,r)||kl(f,a,f,g,e,i,r))return!0}else h+=zl(o,g,o,a,i,r),h+=zl(f,a,f,g,i,r);break;case Bl.Z:if(n){if(kl(d,p,f,g,e,i,r))return!0}else h+=zl(d,p,f,g,i,r);d=f,p=g}}return n||(s=p,l=g,Math.abs(s-l)<1e-4)||(h+=zl(d,p,f,g,i,r)||0),0!==h}var Zl=P({fill:"#000",stroke:null,strokePercent:1,fillOpacity:1,strokeOpacity:1,lineDashOffset:0,lineWidth:1,lineCap:"butt",miterLimit:10,strokeNoScale:!1,strokeFirst:!1},Ws),jl={style:P({fill:!0,stroke:!0,strokePercent:!0,fillOpacity:!0,strokeOpacity:!0,lineDashOffset:!0,lineWidth:!0,miterLimit:!0},Us.style)},ql=Fr.concat(["invisible","culling","z","z2","zlevel","parent"]),Kl=function(t){function e(e){return t.call(this,e)||this}var i;return n(e,t),e.prototype.update=function(){var n=this;t.prototype.update.call(this);var i=this.style;if(i.decal){var r=this._decalEl=this._decalEl||new e;r.buildPath===e.prototype.buildPath&&(r.buildPath=function(t){n.buildPath(t,n.shape)}),r.silent=!0;var o=r.style;for(var a in i)o[a]!==i[a]&&(o[a]=i[a]);o.fill=i.fill?i.decal:null,o.decal=null,o.shadowColor=null,i.strokeFirst&&(o.stroke=null);for(var s=0;s.5?Dr:e>.2?"#eee":Ar}if(t)return Ar}return Dr},e.prototype.getInsideTextStroke=function(t){var e=this.style.fill;if(j(e)){var n=this.__zr;if(!(!n||!n.isDarkMode())===Ii(t,0)<.4)return e}},e.prototype.buildPath=function(t,e,n){},e.prototype.pathUpdated=function(){this.__dirty&=-5},e.prototype.getUpdatedPathProxy=function(t){return!this.path&&this.createPathProxy(),this.path.beginPath(),this.buildPath(this.path,this.shape,t),this.path},e.prototype.createPathProxy=function(){this.path=new Al(!1)},e.prototype.hasStroke=function(){var t=this.style,e=t.stroke;return!(null==e||"none"===e||!(t.lineWidth>0))},e.prototype.hasFill=function(){var t=this.style.fill;return null!=t&&"none"!==t},e.prototype.getBoundingRect=function(){var t=this._rect,e=this.style,n=!t;if(n){var i=!1;this.path||(i=!0,this.createPathProxy());var r=this.path;(i||4&this.__dirty)&&(r.beginPath(),this.buildPath(r,this.shape,!1),this.pathUpdated()),t=r.getBoundingRect()}if(this._rect=t,this.hasStroke()&&this.path&&this.path.len()>0){var o=this._rectStroke||(this._rectStroke=t.clone());if(this.__dirty||n){o.copy(t);var a=e.strokeNoScale?this.getLineScale():1,s=e.lineWidth;if(!this.hasFill()){var l=this.strokeContainThreshold;s=Math.max(s,null==l?4:l)}a>1e-10&&(o.width+=s/a,o.height+=s/a,o.x-=s/a/2,o.y-=s/a/2)}return o}return t},e.prototype.contain=function(t,e){var n=this.transformCoordToLocal(t,e),i=this.getBoundingRect(),r=this.style;if(t=n[0],e=n[1],i.contain(t,e)){var o=this.path;if(this.hasStroke()){var a=r.lineWidth,s=r.strokeNoScale?this.getLineScale():1;if(s>1e-10&&(this.hasFill()||(a=Math.max(a,this.strokeContainThreshold)),function(t,e,n,i){return Xl(t,e,!0,n,i)}(o,a/s,t,e)))return!0}if(this.hasFill())return function(t,e,n){return Xl(t,0,!1,e,n)}(o,t,e)}return!1},e.prototype.dirtyShape=function(){this.__dirty|=4,this._rect&&(this._rect=null),this._decalEl&&this._decalEl.dirtyShape(),this.markRedraw()},e.prototype.dirty=function(){this.dirtyStyle(),this.dirtyShape()},e.prototype.animateShape=function(t){return this.animate("shape",t)},e.prototype.updateDuringAnimation=function(t){"style"===t?this.dirtyStyle():"shape"===t?this.dirtyShape():this.markRedraw()},e.prototype.attrKV=function(e,n){"shape"===e?this.setShape(n):t.prototype.attrKV.call(this,e,n)},e.prototype.setShape=function(t,e){var n=this.shape;return n||(n=this.shape={}),"string"==typeof t?n[t]=e:k(n,t),this.dirtyShape(),this},e.prototype.shapeChanged=function(){return!!(4&this.__dirty)},e.prototype.createStyle=function(t){return _t(Zl,t)},e.prototype._innerSaveToNormal=function(e){t.prototype._innerSaveToNormal.call(this,e);var n=this._normalState;e.shape&&!n.shape&&(n.shape=k({},this.shape))},e.prototype._applyStateObj=function(e,n,i,r,o,a){if(t.prototype._applyStateObj.call(this,e,n,i,r,o,a),1!==this.__inHover){var s,l=!(n&&r);if(n&&n.shape?o?r?s=n.shape:(s=k({},i.shape),k(s,n.shape)):(s=k({},r?this.shape:i.shape),k(s,n.shape)):l&&(s=i.shape),s)if(o){this.shape=k({},this.shape);for(var u={},c=W(s),h=0;hu&&(n*=u/(a=n+i),i*=u/a),r+o>u&&(r*=u/(a=r+o),o*=u/a),i+r>c&&(i*=c/(a=i+r),r*=c/a),n+o>c&&(n*=c/(a=n+o),o*=c/a),t.moveTo(s+n,l),t.lineTo(s+u-i,l),0!==i&&t.arc(s+u-i,l+i,i,-Math.PI/2,0),t.lineTo(s+u,l+c-r),0!==r&&t.arc(s+u-r,l+c-r,r,0,Math.PI/2),t.lineTo(s+o,l+c),0!==o&&t.arc(s+o,l+c-o,o,Math.PI/2,Math.PI),t.lineTo(s,l+n),0!==n&&t.arc(s+n,l+n,n,Math.PI,1.5*Math.PI),t.closePath()}(t,e):t.rect(n,i,r,o)},e.prototype.isZeroArea=function(){return!this.shape.width||!this.shape.height},e}(Kl);lu.prototype.type="rect";var uu={fill:"#000"},cu={},hu={style:P({fill:!0,stroke:!0,fillOpacity:!0,strokeOpacity:!0,lineWidth:!0,fontSize:!0,lineHeight:!0,width:!0,height:!0,textShadowColor:!0,textShadowBlur:!0,textShadowOffsetX:!0,textShadowOffsetY:!0,backgroundColor:!0,padding:!0,borderColor:!0,borderWidth:!0,borderRadius:!0},Us.style)},du=function(t){function e(e){var n=t.call(this)||this;return n.type="text",n._children=[],n._defaultStyle=uu,n.attr(e),n}return n(e,t),e.prototype.childrenRef=function(){return this._children},e.prototype.update=function(){t.prototype.update.call(this),this.styleChanged()&&this._updateSubTexts();for(var e=0;ey&&p){var x=Math.floor(y/d);f=f||v.length>x,m=(v=v.slice(0,x)).length*d}if(r&&c&&null!=g)for(var _=Ts(g,u,e.ellipsis,{minChar:e.truncateMinChar,placeholder:e.placeholder}),b={},w=0;w0,T=0;Tg&&Ps(o,a.substring(g,v),e,f),Ps(o,d[2],e,f,d[1]),g=Ms.lastIndex}gh){var R=o.lines.length;D>0?(I.tokens=I.tokens.slice(0,D),S(I,C,T),o.lines=o.lines.slice(0,M+1)):o.lines=o.lines.slice(0,M),o.isTruncated=o.isTruncated||o.lines.length=0&&"right"===(C=x[T]).align;)this._placeToken(C,t,b,f,I,"right",v),w-=C.width,I-=C.width,T--;for(M+=(s-(M-p)-(g-I)-w)/2;S<=T;)C=x[S],this._placeToken(C,t,b,f,M+C.width/2,"center",v),M+=C.width,S++;f+=b}},e.prototype._placeToken=function(t,e,n,i,r,o,s){var l=e.rich[t.styleName]||{};l.text=t.text;var u=t.verticalAlign,c=i+n/2;"top"===u?c=i+t.height/2:"bottom"===u&&(c=i+n-t.height/2),!t.isLineHolder&&Mu(l)&&this._renderBackground(l,e,"right"===o?r-t.width:"center"===o?r-t.width/2:r,c-t.height/2,t.width,t.height);var h=!!l.backgroundColor,d=t.textPadding;d&&(r=wu(r,o,d),c-=t.height/2-d[0]-t.innerHeight/2);var p=this._getOrCreateChild(Jl),f=p.createStyle();p.useStyle(f);var g=this._defaultStyle,v=!1,y=0,m=!1,x=bu("fill"in l?l.fill:"fill"in e?e.fill:(v=!0,g.fill)),_=_u("stroke"in l?l.stroke:"stroke"in e?e.stroke:h||s||g.autoStroke&&!v?null:(y=2,m=!0,g.stroke)),b=l.textShadowBlur>0||e.textShadowBlur>0;f.text=t.text,f.x=r,f.y=c,b&&(f.shadowBlur=l.textShadowBlur||e.textShadowBlur||0,f.shadowColor=l.textShadowColor||e.textShadowColor||"transparent",f.shadowOffsetX=l.textShadowOffsetX||e.textShadowOffsetX||0,f.shadowOffsetY=l.textShadowOffsetY||e.textShadowOffsetY||0),f.textAlign=o,f.textBaseline="middle",f.font=t.font||a,f.opacity=st(l.opacity,e.opacity,1),yu(f,l),_&&(f.lineWidth=st(l.lineWidth,e.lineWidth,y),f.lineDash=at(l.lineDash,e.lineDash),f.lineDashOffset=e.lineDashOffset||0,f.stroke=_),x&&(f.fill=x),p.setBoundingRect(Gs(f,t.contentWidth,t.contentHeight,m?0:null))},e.prototype._renderBackground=function(t,e,n,i,r,o){var a,s,l,u=t.backgroundColor,c=t.borderWidth,h=t.borderColor,d=u&&u.image,p=u&&!d,f=t.borderRadius,g=this;if(p||t.lineHeight||c&&h){(a=this._getOrCreateChild(lu)).useStyle(a.createStyle()),a.style.fill=null;var v=a.shape;v.x=n,v.y=i,v.width=r,v.height=o,v.r=f,a.dirtyShape()}if(p)(l=a.style).fill=u||null,l.fillOpacity=at(t.fillOpacity,1);else if(d){(s=this._getOrCreateChild(eu)).onload=function(){g.dirtyStyle()};var y=s.style;y.image=u.image,y.x=n,y.y=i,y.width=r,y.height=o}c&&h&&((l=a.style).lineWidth=c,l.stroke=h,l.strokeOpacity=at(t.strokeOpacity,1),l.lineDash=t.borderDash,l.lineDashOffset=t.borderDashOffset||0,a.strokeContainThreshold=0,a.hasFill()&&a.hasStroke()&&(l.strokeFirst=!0,l.lineWidth*=2));var m=(a||s).style;m.shadowBlur=t.shadowBlur||0,m.shadowColor=t.shadowColor||"transparent",m.shadowOffsetX=t.shadowOffsetX||0,m.shadowOffsetY=t.shadowOffsetY||0,m.opacity=st(t.opacity,e.opacity,1)},e.makeFont=function(t){var e="";return mu(t)&&(e=[t.fontStyle,t.fontWeight,vu(t.fontSize),t.fontFamily||"sans-serif"].join(" ")),e&&ht(e)||t.textFont||t.font},e}(Zs),pu={left:!0,right:1,center:1},fu={top:1,bottom:1,middle:1},gu=["fontStyle","fontWeight","fontSize","fontFamily"];function vu(t){return"string"!=typeof t||-1===t.indexOf("px")&&-1===t.indexOf("rem")&&-1===t.indexOf("em")?isNaN(+t)?"12px":t+"px":t}function yu(t,e){for(var n=0;n=0,o=!1;if(t instanceof Kl){var a=Uu(t),s=r&&a.selectFill||a.normalFill,l=r&&a.selectStroke||a.normalStroke;if(ec(s)||ec(l)){var u=(i=i||{}).style||{};"inherit"===u.fill?(o=!0,i=k({},i),(u=k({},u)).fill=s):!ec(u.fill)&&ec(s)?(o=!0,i=k({},i),(u=k({},u)).fill=Ci(s)):!ec(u.stroke)&&ec(l)&&(o||(i=k({},i),u=k({},u)),u.stroke=Ci(l)),i.style=u}}if(i&&null==i.z2){o||(i=k({},i));var c=t.z2EmphasisLift;i.z2=t.z2+(null!=c?c:ju)}return i}(this,0,e,n);if("blur"===t)return function(t,e,n){var i=R(t.currentStates,e)>=0,r=t.style.opacity,o=i?null:function(t,e,n,i){for(var r=t.style,o={},a=0;a0){var o={dataIndex:r,seriesIndex:t.seriesIndex};null!=i&&(o.dataType=i),e.push(o)}})}),e}function Ac(t,e,n){Nc(t,!0),cc(t,pc),Lc(t,e,n)}function kc(t,e,n,i){i?function(t){Nc(t,!1)}(t):Ac(t,e,n)}function Lc(t,e,n){var i=Iu(t);null!=e?(i.focus=e,i.blurScope=n):i.focus&&(i.focus=null)}var Pc=["emphasis","blur","select"],Oc={itemStyle:"getItemStyle",lineStyle:"getLineStyle",areaStyle:"getAreaStyle"};function Rc(t,e,n,i){n=n||"itemStyle";for(var r=0;r1&&(a*=Uc(f),s*=Uc(f));var g=(r===o?-1:1)*Uc((a*a*(s*s)-a*a*(p*p)-s*s*(d*d))/(a*a*(p*p)+s*s*(d*d)))||0,v=g*a*p/s,y=g*-s*d/a,m=(t+n)/2+Xc(h)*v-Yc(h)*y,x=(e+i)/2+Yc(h)*v+Xc(h)*y,_=Kc([1,0],[(d-v)/a,(p-y)/s]),b=[(d-v)/a,(p-y)/s],w=[(-1*d-v)/a,(-1*p-y)/s],S=Kc(b,w);if(qc(b,w)<=-1&&(S=Zc),qc(b,w)>=1&&(S=0),S<0){var M=Math.round(S/Zc*1e6)/1e6;S=2*Zc+M%2*Zc}c.addData(u,m,x,a,s,_,S,h,o)}var Jc=/([mlvhzcqtsa])([^mlvhzcqtsa]*)/gi,Qc=/-?([0-9]*\.)?[0-9]+([eE]-?[0-9]+)?/g;var th=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.applyTransform=function(t){},e}(Kl);function eh(t){return null!=t.setData}function nh(t,e){var n=function(t){var e=new Al;if(!t)return e;var n,i=0,r=0,o=i,a=r,s=Al.CMD,l=t.match(Jc);if(!l)return e;for(var u=0;uk*k+L*L&&(M=T,I=C),{cx:M,cy:I,x0:-c,y0:-h,x1:M*(r/b-1),y1:I*(r/b-1)}}function bh(t,e){var n,i=yh(e.r,0),r=yh(e.r0||0,0),o=i>0;if(o||r>0){if(o||(i=r,r=0),r>i){var a=i;i=r,r=a}var s=e.startAngle,l=e.endAngle;if(!isNaN(s)&&!isNaN(l)){var u=e.cx,c=e.cy,h=!!e.clockwise,d=gh(l-s),p=d>ch&&d%ch;if(p>xh&&(d=p),i>xh)if(d>ch-xh)t.moveTo(u+i*dh(s),c+i*hh(s)),t.arc(u,c,i,s,l,!h),r>xh&&(t.moveTo(u+r*dh(l),c+r*hh(l)),t.arc(u,c,r,l,s,h));else{var f=void 0,g=void 0,v=void 0,y=void 0,m=void 0,x=void 0,_=void 0,b=void 0,w=void 0,S=void 0,M=void 0,I=void 0,T=void 0,C=void 0,D=void 0,A=void 0,k=i*dh(s),L=i*hh(s),P=r*dh(l),O=r*hh(l),R=d>xh;if(R){var N=e.cornerRadius;N&&(n=function(t){var e;if(X(t)){var n=t.length;if(!n)return t;e=1===n?[t[0],t[0],0,0]:2===n?[t[0],t[0],t[1],t[1]]:3===n?t.concat(t[2]):t}else e=[t,t,t,t];return e}(N),f=n[0],g=n[1],v=n[2],y=n[3]);var E=gh(i-r)/2;if(m=mh(E,v),x=mh(E,y),_=mh(E,f),b=mh(E,g),M=w=yh(m,x),I=S=yh(_,b),(w>xh||S>xh)&&(T=i*dh(l),C=i*hh(l),D=r*dh(s),A=r*hh(s),dxh){var U=mh(v,M),Y=mh(y,M),Z=_h(D,A,k,L,i,U,h),j=_h(T,C,P,O,i,Y,h);t.moveTo(u+Z.cx+Z.x0,c+Z.cy+Z.y0),M0&&t.arc(u+Z.cx,c+Z.cy,U,fh(Z.y0,Z.x0),fh(Z.y1,Z.x1),!h),t.arc(u,c,i,fh(Z.cy+Z.y1,Z.cx+Z.x1),fh(j.cy+j.y1,j.cx+j.x1),!h),Y>0&&t.arc(u+j.cx,c+j.cy,Y,fh(j.y1,j.x1),fh(j.y0,j.x0),!h))}else t.moveTo(u+k,c+L),t.arc(u,c,i,s,l,!h);else t.moveTo(u+k,c+L);if(r>xh&&R)if(I>xh){U=mh(f,I),Z=_h(P,O,T,C,r,-(Y=mh(g,I)),h),j=_h(k,L,D,A,r,-U,h);t.lineTo(u+Z.cx+Z.x0,c+Z.cy+Z.y0),I0&&t.arc(u+Z.cx,c+Z.cy,Y,fh(Z.y0,Z.x0),fh(Z.y1,Z.x1),!h),t.arc(u,c,r,fh(Z.cy+Z.y1,Z.cx+Z.x1),fh(j.cy+j.y1,j.cx+j.x1),h),U>0&&t.arc(u+j.cx,c+j.cy,U,fh(j.y1,j.x1),fh(j.y0,j.x0),!h))}else t.lineTo(u+P,c+O),t.arc(u,c,r,l,s,h);else t.lineTo(u+P,c+O)}else t.moveTo(u,c);t.closePath()}}}var wh=function(){this.cx=0,this.cy=0,this.r0=0,this.r=0,this.startAngle=0,this.endAngle=2*Math.PI,this.clockwise=!0,this.cornerRadius=0},Sh=function(t){function e(e){return t.call(this,e)||this}return n(e,t),e.prototype.getDefaultShape=function(){return new wh},e.prototype.buildPath=function(t,e){bh(t,e)},e.prototype.isZeroArea=function(){return this.shape.startAngle===this.shape.endAngle||this.shape.r===this.shape.r0},e}(Kl);Sh.prototype.type="sector";var Mh=function(){this.cx=0,this.cy=0,this.r=0,this.r0=0},Ih=function(t){function e(e){return t.call(this,e)||this}return n(e,t),e.prototype.getDefaultShape=function(){return new Mh},e.prototype.buildPath=function(t,e){var n=e.cx,i=e.cy,r=2*Math.PI;t.moveTo(n+e.r,i),t.arc(n,i,e.r,0,r,!1),t.moveTo(n+e.r0,i),t.arc(n,i,e.r0,0,r,!0)},e}(Kl);function Th(t,e,n){var i=e.smooth,r=e.points;if(r&&r.length>=2){if(i){var o=function(t,e,n,i){var r,o,a,s,l=[],u=[],c=[],h=[];if(i){a=[1/0,1/0],s=[-1/0,-1/0];for(var d=0,p=t.length;djh[1]){if(r=!1,qh.negativeSize||n)return r;var s=Xh(jh[0]-Zh[1]),l=Xh(Zh[0]-jh[1]);Uh(s,l)>$h.len()&&(s=l||!qh.bidirectional)&&(Pe.scale(Kh,a,-l*i),qh.useDir&&qh.calcDirMTV()))}}return r},t.prototype._getProjMinMaxOnAxis=function(t,e,n){for(var i=this._axes[t],r=this._origin,o=e[0].dot(i)+r[t],a=o,s=o,l=1;l0){var h={duration:c.duration,delay:c.delay||0,easing:c.easing,done:o,force:!!o||!!a,setToFinal:!u,scope:t,during:a};l?e.animateFrom(n,h):e.animateTo(n,h)}else e.stopAnimation(),!l&&e.attr(n),a&&a(1),o&&o()}function rd(t,e,n,i,r,o){id("update",t,e,n,i,r,o)}function od(t,e,n,i,r,o){id("enter",t,e,n,i,r,o)}function ad(t){if(!t.__zr)return!0;for(var e=0;eAo(o[1])?o[0]>0?"right":"left":o[1]>0?"bottom":"top"}function Ad(t){return!t.isGroup}function kd(t,e,n){if(t&&e){var i,r=(i={},t.traverse(function(t){Ad(t)&&t.anid&&(i[t.anid]=t)}),i);e.traverse(function(t){if(Ad(t)&&t.anid){var e=r[t.anid];if(e){var i=o(t);t.attr(o(e)),rd(t,i,n,Iu(t).dataIndex)}}})}function o(t){var e={x:t.x,y:t.y,rotation:t.rotation};return function(t){return null!=t.shape}(t)&&(e.shape=C(t.shape)),e}}function Ld(t,e){return V(t,function(t){var n=t[0];n=Do(n,e.x),n=Co(n,e.x+e.width);var i=t[1];return i=Do(i,e.y),[n,i=Co(i,e.y+e.height)]})}function Pd(t,e){var n=Do(t.x,e.x),i=Co(t.x+t.width,e.x+e.width),r=Do(t.y,e.y),o=Co(t.y+t.height,e.y+e.height);if(i>=n&&o>=r)return{x:n,y:r,width:i-n,height:o-r}}function Od(t,e,n){var i=k({rectHover:!0},e),r=i.style={strokeNoScale:!0};if(n=n||{x:-1,y:-1,width:2,height:2},t)return 0===t.indexOf("image://")?(r.image=t.slice(8),P(r,n),new eu(i)):xd(t.replace("path://",""),i,n,"center")}function Rd(t,e,n,i,r){for(var o=0,a=r[r.length-1];o=-1e-6)return!1;var f=t-r,g=e-o,v=Ed(f,g,u,c)/p;if(v<0||v>1)return!1;var y=Ed(f,g,h,d)/p;return!(y<0||y>1)}function Ed(t,e,n,i){return t*i-n*e}function zd(t,e,n,i,r){return null==e||(K(e)?Bd[0]=Bd[1]=Bd[2]=Bd[3]=e:(Bd[0]=e[0],Bd[1]=e[1],Bd[2]=e[2],Bd[3]=e[3]),i&&(Bd[0]=Do(0,Bd[0]),Bd[1]=Do(0,Bd[1]),Bd[2]=Do(0,Bd[2]),Bd[3]=Do(0,Bd[3])),n&&(Bd[0]=-Bd[0],Bd[1]=-Bd[1],Bd[2]=-Bd[2],Bd[3]=-Bd[3]),Vd(t,Bd,"x","width",3,1,r&&r[0]||0),Vd(t,Bd,"y","height",0,2,r&&r[1]||0)),t}var Bd=[0,0,0,0];function Vd(t,e,n,i,r,o,a){var s=e[o]+e[r],l=t[i];t[i]+=s,a=Do(0,Co(a,l)),t[i]=0?-e[r]:e[o]>=0?l+e[o]:Ao(s)>1e-8?(l-a)*e[r]/s:0):t[n]-=e[r]}function Gd(t){var e=t.itemTooltipOption,n=t.componentModel,i=t.itemName,r=j(e)?{formatter:e}:e,o=n.mainType,a=n.componentIndex,s={componentType:o,name:i,$vars:["name"]};s[o+"Index"]=a;var l=t.formatterParamsExtra;l&&B(W(l),function(t){wt(s,t)||(s[t]=l[t],s.$vars.push(t))});var u=Iu(t.el);u.componentMainType=o,u.componentIndex=a,u.tooltipConfig={name:i,option:P({content:i,encodeHTMLContent:!0,formatterParams:s},r)}}function Fd(t,e){var n;t.isGroup&&(n=e(t)),n||t.traverse(e)}function Hd(t,e){if(t)if(X(t))for(var n=0;ne&&(e=i),ie&&(n=e=0),{min:n,max:e}}function qd(t,e,n){Kd(t,e,n,-1/0)}function Kd(t,e,n,i){if(t.ignoreModelZ)return i;var r=t.getTextContent(),o=t.getTextGuideLine();if(t.isGroup)for(var a=t.childrenRef(),s=0;s-1?Rp:Ep;function Gp(t,e){t=t.toUpperCase(),Bp[t]=new kp(e),zp[t]=e}function Fp(t){return Bp[t]}Gp(Np,{time:{month:["January","February","March","April","May","June","July","August","September","October","November","December"],monthAbbr:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayOfWeek:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayOfWeekAbbr:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"]},legend:{selector:{all:"All",inverse:"Inv"}},toolbox:{brush:{title:{rect:"Box Select",polygon:"Lasso Select",lineX:"Horizontally Select",lineY:"Vertically Select",keep:"Keep Selections",clear:"Clear Selections"}},dataView:{title:"Data View",lang:["Data View","Close","Refresh"]},dataZoom:{title:{zoom:"Zoom",back:"Zoom Reset"}},magicType:{title:{line:"Switch to Line Chart",bar:"Switch to Bar Chart",stack:"Stack",tiled:"Tile"}},restore:{title:"Restore"},saveAsImage:{title:"Save as Image",lang:["Right Click to Save Image"]}},series:{typeNames:{pie:"Pie chart",bar:"Bar chart",line:"Line chart",scatter:"Scatter plot",effectScatter:"Ripple scatter plot",radar:"Radar chart",tree:"Tree",treemap:"Treemap",boxplot:"Boxplot",candlestick:"Candlestick",k:"K line chart",heatmap:"Heat map",map:"Map",parallel:"Parallel coordinate map",lines:"Line graph",graph:"Relationship graph",sankey:"Sankey diagram",funnel:"Funnel chart",gauge:"Gauge",pictorialBar:"Pictorial bar",themeRiver:"Theme River Map",sunburst:"Sunburst",custom:"Custom chart",chart:"Chart"}},aria:{general:{withTitle:'This is a chart about "{title}"',withoutTitle:"This is a chart"},series:{single:{prefix:"",withName:" with type {seriesType} named {seriesName}.",withoutName:" with type {seriesType}."},multiple:{prefix:". It consists of {seriesCount} series count.",withName:" The {seriesId} series is a {seriesType} representing {seriesName}.",withoutName:" The {seriesId} series is a {seriesType}.",separator:{middle:"",end:""}}},data:{allData:"The data is as follows: ",partialData:"The first {displayCnt} items are: ",withName:"the data for {name} is {value}",withoutName:"{value}",separator:{middle:", ",end:". "}}}}),Gp(Rp,{time:{month:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],monthAbbr:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],dayOfWeek:["星期日","星期一","星期二","星期三","星期四","星期五","星期六"],dayOfWeekAbbr:["日","一","二","三","四","五","六"]},legend:{selector:{all:"全选",inverse:"反选"}},toolbox:{brush:{title:{rect:"矩形选择",polygon:"圈选",lineX:"横向选择",lineY:"纵向选择",keep:"保持选择",clear:"清除选择"}},dataView:{title:"数据视图",lang:["数据视图","关闭","刷新"]},dataZoom:{title:{zoom:"区域缩放",back:"区域缩放还原"}},magicType:{title:{line:"切换为折线图",bar:"切换为柱状图",stack:"切换为堆叠",tiled:"切换为平铺"}},restore:{title:"还原"},saveAsImage:{title:"保存为图片",lang:["右键另存为图片"]}},series:{typeNames:{pie:"饼图",bar:"柱状图",line:"折线图",scatter:"散点图",effectScatter:"涟漪散点图",radar:"雷达图",tree:"树图",treemap:"矩形树图",boxplot:"箱型图",candlestick:"K线图",k:"K线图",heatmap:"热力图",map:"地图",parallel:"平行坐标图",lines:"线图",graph:"关系图",sankey:"桑基图",funnel:"漏斗图",gauge:"仪表盘图",pictorialBar:"象形柱图",themeRiver:"主题河流图",sunburst:"旭日图",custom:"自定义图表",chart:"图表"}},aria:{general:{withTitle:"这是一个关于“{title}”的图表。",withoutTitle:"这是一个图表,"},series:{single:{prefix:"",withName:"图表类型是{seriesType},表示{seriesName}。",withoutName:"图表类型是{seriesType}。"},multiple:{prefix:"它由{seriesCount}个图表系列组成。",withName:"第{seriesId}个系列是一个表示{seriesName}的{seriesType},",withoutName:"第{seriesId}个系列是一个{seriesType},",separator:{middle:";",end:"。"}}},data:{allData:"其数据是——",partialData:"其中,前{displayCnt}项是——",withName:"{name}的数据是{value}",withoutName:"{value}",separator:{middle:",",end:""}}}});var Hp=null;function Wp(){return Hp}function Up(t,e){var n=Wp(),i=e.breakOption,r=e.breakParsed;return!r&&n&&(r=n.parseAxisBreakOption(i,t)),r}function Yp(t){var e=t.brk;return e?e.breaks:[]}function Xp(t){var e=t.brk;return!!e&&e.hasBreaks()}var Zp=1e3,jp=6e4,qp=36e5,Kp=864e5,$p=31536e6,Jp={year:/({yyyy}|{yy})/,month:/({MMMM}|{MMM}|{MM}|{M})/,day:/({dd}|{d})/,hour:/({HH}|{H}|{hh}|{h})/,minute:/({mm}|{m})/,second:/({ss}|{s})/,millisecond:/({SSS}|{S})/},Qp={year:"{yyyy}",month:"{MMM}",day:"{d}",hour:"{HH}:{mm}",minute:"{HH}:{mm}",second:"{HH}:{mm}:{ss}",millisecond:"{HH}:{mm}:{ss} {SSS}"},tf="{yyyy}-{MM}-{dd}",ef={year:"{yyyy}",month:"{yyyy}-{MM}",day:tf,hour:tf+" "+Qp.hour,minute:tf+" "+Qp.minute,second:tf+" "+Qp.second,millisecond:"{yyyy}-{MM}-{dd} {HH}:{mm}:{ss} {SSS}"},nf=["year","month","day","hour","minute","second","millisecond"],rf=["year","half-year","quarter","month","week","half-week","day","half-day","quarter-day","hour","minute","second","millisecond"];function of(t){return j(t)||Z(t)?t:function(t){t=t||{};var e={},n=!0;return B(nf,function(e){n&&(n=null==t[e])}),B(nf,function(i,r){var o=t[i];e[i]={};for(var a=null,s=r;s>=0;s--){var l=nf[s],u=$(o)&&!X(o)?o[l]:o,c=void 0;X(u)?a=(c=u.slice())[0]||"":j(u)?c=[a=u]:(null==a?a=Qp[i]:Jp[l].test(a)||(a=e[l][l][0]+" "+a),c=[a],n&&(c[1]="{primary|"+a+"}")),e[i][l]=c}}),e}(t)}function af(t,e){return"0000".substr(0,e-(t+="").length)+t}function sf(t){switch(t){case"half-year":case"quarter":return"month";case"week":case"half-week":return"day";case"half-day":case"quarter-day":return"hour";default:return t}}function lf(t){return t===sf(t)}function uf(t,e,n,i){var r=Qo(t),o=r[df(n)](),a=r[pf(n)]()+1,s=Math.floor((a-1)/3)+1,l=r[ff(n)](),u=r["get"+(n?"UTC":"")+"Day"](),c=r[gf(n)](),h=(c-1)%12+1,d=r[vf(n)](),p=r[yf(n)](),f=r[mf(n)](),g=c>=12?"pm":"am",v=g.toUpperCase(),y=(i instanceof kp?i:Fp(i||Vp)||Bp[Ep]).getModel("time"),m=y.get("month"),x=y.get("monthAbbr"),_=y.get("dayOfWeek"),b=y.get("dayOfWeekAbbr");return(e||"").replace(/{a}/g,g+"").replace(/{A}/g,v+"").replace(/{yyyy}/g,o+"").replace(/{yy}/g,af(o%100+"",2)).replace(/{Q}/g,s+"").replace(/{MMMM}/g,m[a-1]).replace(/{MMM}/g,x[a-1]).replace(/{MM}/g,af(a,2)).replace(/{M}/g,a+"").replace(/{dd}/g,af(l,2)).replace(/{d}/g,l+"").replace(/{eeee}/g,_[u]).replace(/{ee}/g,b[u]).replace(/{e}/g,u+"").replace(/{HH}/g,af(c,2)).replace(/{H}/g,c+"").replace(/{hh}/g,af(h+"",2)).replace(/{h}/g,h+"").replace(/{mm}/g,af(d,2)).replace(/{m}/g,d+"").replace(/{ss}/g,af(p,2)).replace(/{s}/g,p+"").replace(/{SSS}/g,af(f,3)).replace(/{S}/g,f+"")}function cf(t,e){var n=Qo(t),i=n[pf(e)]()+1,r=n[ff(e)](),o=n[gf(e)](),a=n[vf(e)](),s=n[yf(e)](),l=0===n[mf(e)](),u=l&&0===s,c=u&&0===a,h=c&&0===o,d=h&&1===r;return d&&1===i?"year":d?"month":h?"day":c?"hour":u?"minute":l?"second":"millisecond"}function hf(t,e,n){switch(e){case"year":t[_f(n)](0);case"month":t[bf(n)](1);case"day":t[wf(n)](0);case"hour":t[Sf(n)](0);case"minute":t[Mf(n)](0);case"second":t[If(n)](0)}return t}function df(t){return t?"getUTCFullYear":"getFullYear"}function pf(t){return t?"getUTCMonth":"getMonth"}function ff(t){return t?"getUTCDate":"getDate"}function gf(t){return t?"getUTCHours":"getHours"}function vf(t){return t?"getUTCMinutes":"getMinutes"}function yf(t){return t?"getUTCSeconds":"getSeconds"}function mf(t){return t?"getUTCMilliseconds":"getMilliseconds"}function xf(t){return t?"setUTCFullYear":"setFullYear"}function _f(t){return t?"setUTCMonth":"setMonth"}function bf(t){return t?"setUTCDate":"setDate"}function wf(t){return t?"setUTCHours":"setHours"}function Sf(t){return t?"setUTCMinutes":"setMinutes"}function Mf(t){return t?"setUTCSeconds":"setSeconds"}function If(t){return t?"setUTCMilliseconds":"setMilliseconds"}function Tf(t){if(!aa(t))return j(t)?t:"-";var e=(t+"").split(".");return e[0].replace(/(\d{1,3})(?=(?:\d{3})+(?!\d))/g,"$1,")+(e.length>1?"."+e[1]:"")}function Cf(t,e){return t=(t||"").toLowerCase().replace(/-(.)/g,function(t,e){return e.toUpperCase()}),e&&t&&(t=t.charAt(0).toUpperCase()+t.slice(1)),t}var Df=ut;function Af(t,e,n){function i(t){return t&&ht(t)?t:"-"}function r(t){return ca(t)}var o="time"===e,a=t instanceof Date;if(o||a){var s=o?Qo(t):t;if(!isNaN(+s))return uf(s,"{yyyy}-{MM}-{dd} {HH}:{mm}:{ss}",n);if(a)return"-"}if("ordinal"===e)return q(t)?i(t):K(t)&&r(t)?t+"":"-";var l=oa(t);return r(l)?Tf(l):q(t)?i(t):"boolean"==typeof t?t+"":"-"}var kf=["a","b","c","d","e","f","g"],Lf=function(t,e){return"{"+t+(null==e?"":e)+"}"};function Pf(t,e,n){X(e)||(e=[e]);var i=e.length;if(!i)return"";for(var r=e[0].$vars||[],o=0;o':'':{renderMode:o,content:"{"+(n.markerId||"markerX")+"|} ",style:"subItem"===r?{width:4,height:4,borderRadius:2,backgroundColor:i}:{width:10,height:10,borderRadius:5,backgroundColor:i}}:""}function Nf(t,e){return e=e||"transparent",j(t)?t:$(t)&&t.colorStops&&(t.colorStops[0]||{}).color||e}function Ef(t,e){if("_blank"===e||"blank"===e){var n=window.open();n.opener=null,n.location.href=t}else window.open(t,e)}var zf={},Bf={},Vf=function(){function t(){this._normalMasterList=[],this._nonSeriesBoxMasterList=[]}return t.prototype.create=function(t,e){function n(n,i){var r=[];return B(n,function(n,i){var o=n.create(t,e);r=r.concat(o||[])}),r}this._nonSeriesBoxMasterList=n(zf,!0),this._normalMasterList=n(Bf,!1)},t.prototype.update=function(t,e){B(this._normalMasterList,function(n){n.update&&n.update(t,e)})},t.prototype.getCoordinateSystems=function(){return this._normalMasterList.concat(this._nonSeriesBoxMasterList)},t.register=function(t,e){"matrix"!==t&&"calendar"!==t?Bf[t]=e:zf[t]=e},t.get=function(t){return Bf[t]||zf[t]},t}();var Gf=mt();function Ff(t){var e=t.getShallow("coord",!0),n=1;if(null==e){var i=Gf.get(t.type);i&&i.getCoord2&&(n=2,e=i.getCoord2(t))}return{coord:e,from:n}}function Hf(t,e){var n=t.getShallow("coordinateSystem"),i=t.getShallow("coordinateSystemUsage",!0),r=0;if(n){var o="series"===t.mainType;null==i&&(i=o?"data":"box"),"data"===i?(r=1,o||(r=0)):"box"===i&&(r=2,o||function(t){return!!zf[t]}(n)||(r=0))}return{coordSysType:n,kind:r}}function Wf(t){var e=t.targetModel,n=t.coordSysType,i=t.coordSysProvider,r=t.isDefaultDataCoordSys;t.allowNotFound;var o=Hf(e),a=o.kind,s=o.coordSysType;if(r&&1!==a&&(a=1,s=n),0===a||s!==n)return 0;var l=i(n,e);return l?(1===a?e.coordinateSystem=l:e.boxCoordinateSystem=l,a):0}var Uf=function(t,e){var n=e.getReferringComponents(t,Ea).models[0];return n&&n.coordinateSystem},Yf=B,Xf=["left","right","top","bottom","width","height"],Zf=[["width","left","right"],["height","top","bottom"]];function jf(t,e,n,i,r){var o=0,a=0;null==i&&(i=1/0),null==r&&(r=1/0);var s=0;e.eachChild(function(l,u){var c,h,d=l.getBoundingRect(),p=e.childAt(u+1),f=p&&p.getBoundingRect();if("horizontal"===t){var g=d.width+(f?-f.x+d.x:0);(c=o+g)>i||l.newline?(o=0,c=g,a+=s+n,s=d.height):s=Math.max(s,d.height)}else{var v=d.height+(f?-f.y+d.y:0);(h=a+v)>r||l.newline?(o+=s+n,a=0,h=v,s=d.width):s=Math.max(s,d.width)}l.newline||(l.x=o,l.y=a,l.markRedraw(),"horizontal"===t?o=c+n:a=h+n)})}var qf=jf;Y(jf,"vertical"),Y(jf,"horizontal");function Kf(t,e){return{left:t.getShallow("left",e),top:t.getShallow("top",e),right:t.getShallow("right",e),bottom:t.getShallow("bottom",e),width:t.getShallow("width",e),height:t.getShallow("height",e)}}function $f(t,e){var n=function(t,e){var n,i,r=eg(t,e,{enableLayoutOnlyByCenter:!0}),o=t.getBoxLayoutParams();if(r.type===tg.point)i=r.refPoint,n=Jf(o,{width:e.getWidth(),height:e.getHeight()});else{var a=t.get("center"),s=X(a)?a:[a,a];n=Jf(o,r.refContainer),i=2===r.boxCoordFrom?r.refPoint:[Vo(s[0],n.width)+n.x,Vo(s[1],n.height)+n.y]}return{viewRect:n,center:i}}(t,e),i=n.viewRect,r=n.center,o=t.get("radius");X(o)||(o=[0,o]);var a=Vo(i.width,e.getWidth()),s=Vo(i.height,e.getHeight()),l=Math.min(a,s),u=Vo(o[0],l/2),c=Vo(o[1],l/2);return{cx:r[0],cy:r[1],r0:u,r:c,viewRect:i}}function Jf(t,e,n){n=Df(n||0);var i=e.width,r=e.height,o=Vo(t.left,i),a=Vo(t.top,r),s=Vo(t.right,i),l=Vo(t.bottom,r),u=Vo(t.width,i),c=Vo(t.height,r),h=n[2]+n[0],d=n[1]+n[3],p=t.aspect;switch(isNaN(u)&&(u=i-s-d-o),isNaN(c)&&(c=r-l-h-a),null!=p&&(isNaN(u)&&isNaN(c)&&(p>i/r?u=.8*i:c=.8*r),isNaN(u)&&(u=p*c),isNaN(c)&&(c=u/p)),isNaN(o)&&(o=i-s-u-d),isNaN(a)&&(a=r-l-c-h),t.left||t.right){case"center":o=i/2-u/2-n[3];break;case"right":o=i-u-d}switch(t.top||t.bottom){case"middle":case"center":a=r/2-c/2-n[0];break;case"bottom":a=r-c-h}o=o||0,a=a||0,isNaN(u)&&(u=i-d-o-(s||0)),isNaN(c)&&(c=r-h-a-(l||0));var f=new Xe((e.x||0)+o+n[3],(e.y||0)+a+n[0],u,c);return f.margin=n,f}function Qf(t,e,n){var i=t.getShallow("preserveAspect",!0);if(!i)return e;var r=e.width/e.height;if(Math.abs(Math.atan(n)-Math.atan(r))<1e-9)return e;var o=t.getShallow("preserveAspectAlign",!0),a=t.getShallow("preserveAspectVerticalAlign",!0),s={width:e.width,height:e.height},l="cover"===i;return r>n&&!l||r=2)return o;for(var c=0;c=0;a--)o=D(o,n[a],!0);e.defaultOption=o}return e.defaultOption},e.prototype.getReferringComponents=function(t,e){var n=t+"Index",i=t+"Id";return Ba(this.ecModel,t,{index:this.get(n,!0),id:this.get(i,!0)},e)},e.prototype.getBoxLayoutParams=function(){return Kf(this,!1)},e.prototype.getZLevelKey=function(){return""},e.prototype.setZLevel=function(t){this.option.zlevel=t},e.protoInitialize=((i=e.prototype).type="component",i.id="",i.name="",i.mainType="",i.subType="",void(i.componentIndex=0)),e}(kp);hs(lg,kp),gs(lg),function(t){var e={};t.registerSubTypeDefaulter=function(t,n){var i=us(t);e[i.main]=n},t.determineSubType=function(n,i){var r=i.type;if(!r){var o=us(n).main;t.hasSubTypes(n)&&e[o]&&(r=e[o](i))}return r}}(lg),function(t,e){function n(t,e){return t[e]||(t[e]={predecessor:[],successor:[]}),t[e]}t.topologicalTravel=function(t,i,r,o){if(t.length){var a=function(t){var i={},r=[];return B(t,function(o){var a=n(i,o),s=function(t,e){var n=[];return B(t,function(t){R(e,t)>=0&&n.push(t)}),n}(a.originalDeps=e(o),t);a.entryCount=s.length,0===a.entryCount&&r.push(o),B(s,function(t){R(a.predecessor,t)<0&&a.predecessor.push(t);var e=n(i,t);R(e.successor,t)<0&&e.successor.push(o)})}),{graph:i,noEntryList:r}}(i),s=a.graph,l=a.noEntryList,u={};for(B(t,function(t){u[t]=!0});l.length;){var c=l.pop(),h=s[c],d=!!u[c];d&&(r.call(o,c,h.originalDeps.slice()),delete u[c]),B(h.successor,d?f:p)}B(u,function(){var t="";throw new Error(t)})}function p(t){s[t].entryCount--,0===s[t].entryCount&&l.push(t)}function f(t){u[t]=!0,p(t)}}}(lg,function(t){var e=[];B(lg.getClassesByMainType(t),function(t){e=e.concat(t.dependencies||t.prototype.dependencies||[])}),e=V(e,function(t){return us(t).main}),"dataset"!==t&&R(e,"dataset")<=0&&e.unshift("dataset");return e});var ug={color:{},darkColor:{},size:{}},cg=ug.color={theme:["#5070dd","#b6d634","#505372","#ff994d","#0ca8df","#ffd10a","#fb628b","#785db0","#3fbe95"],neutral00:"#fff",neutral05:"#f4f7fd",neutral10:"#e8ebf0",neutral15:"#dbdee4",neutral20:"#cfd2d7",neutral25:"#c3c5cb",neutral30:"#b7b9be",neutral35:"#aaacb2",neutral40:"#9ea0a5",neutral45:"#929399",neutral50:"#86878c",neutral55:"#797b7f",neutral60:"#6d6e73",neutral65:"#616266",neutral70:"#54555a",neutral75:"#48494d",neutral80:"#3c3c41",neutral85:"#303034",neutral90:"#232328",neutral95:"#17171b",neutral99:"#000",accent05:"#eff1f9",accent10:"#e0e4f2",accent15:"#d0d6ec",accent20:"#c0c9e6",accent25:"#b1bbdf",accent30:"#a1aed9",accent35:"#91a0d3",accent40:"#8292cc",accent45:"#7285c6",accent50:"#6578ba",accent55:"#5c6da9",accent60:"#536298",accent65:"#4a5787",accent70:"#404c76",accent75:"#374165",accent80:"#2e3654",accent85:"#252b43",accent90:"#1b2032",accent95:"#121521",transparent:"rgba(0,0,0,0)",highlight:"rgba(255,231,130,0.8)"};for(var hg in k(cg,{primary:cg.neutral80,secondary:cg.neutral70,tertiary:cg.neutral60,quaternary:cg.neutral50,disabled:cg.neutral20,border:cg.neutral30,borderTint:cg.neutral20,borderShade:cg.neutral40,background:cg.neutral05,backgroundTint:"rgba(234,237,245,0.5)",backgroundTransparent:"rgba(255,255,255,0)",backgroundShade:cg.neutral10,shadow:"rgba(0,0,0,0.2)",shadowTint:"rgba(129,130,136,0.2)",axisLine:cg.neutral70,axisLineTint:cg.neutral40,axisTick:cg.neutral70,axisTickMinor:cg.neutral60,axisLabel:cg.neutral70,axisSplitLine:cg.neutral15,axisMinorSplitLine:cg.neutral05}),cg)if(cg.hasOwnProperty(hg)){var dg=cg[hg];"theme"===hg?ug.darkColor.theme=cg.theme.slice():"highlight"===hg?ug.darkColor.highlight="rgba(255,231,130,0.4)":0===hg.indexOf("accent")?ug.darkColor[hg]=wi(dg,null,function(t){return.5*t},function(t){return Math.min(1,1.3-t)}):ug.darkColor[hg]=wi(dg,null,function(t){return.9*t},function(t){return 1-Math.pow(t,1.5)})}ug.size={xxs:2,xs:5,s:10,m:15,l:20,xl:30,xxl:40,xxxl:50};var pg="";"undefined"!=typeof navigator&&(pg=navigator.platform||"");var fg="rgba(0, 0, 0, 0.2)",gg=ug.color.theme[0],vg=wi(gg,null,null,.9),yg={darkMode:"auto",colorBy:"series",color:ug.color.theme,gradientColor:[vg,gg],aria:{decal:{decals:[{color:fg,dashArrayX:[1,0],dashArrayY:[2,5],symbolSize:1,rotation:Math.PI/6},{color:fg,symbol:"circle",dashArrayX:[[8,8],[0,8,8,0]],dashArrayY:[6,0],symbolSize:.8},{color:fg,dashArrayX:[1,0],dashArrayY:[4,3],rotation:-Math.PI/4},{color:fg,dashArrayX:[[6,6],[0,6,6,0]],dashArrayY:[6,0]},{color:fg,dashArrayX:[[1,0],[1,6]],dashArrayY:[1,0,6,0],rotation:Math.PI/4},{color:fg,symbol:"triangle",dashArrayX:[[9,9],[0,9,9,0]],dashArrayY:[7,2],symbolSize:.75}]}},textStyle:{fontFamily:pg.match(/^Win/)?"Microsoft YaHei":"sans-serif",fontSize:12,fontStyle:"normal",fontWeight:"normal"},blendMode:null,stateAnimation:{duration:300,easing:"cubicOut"},animation:"auto",animationDuration:1e3,animationDurationUpdate:500,animationEasing:"cubicInOut",animationEasingUpdate:"cubicInOut",animationThreshold:2e3,progressiveThreshold:3e3,progressive:400,hoverLayerThreshold:3e3,useUTC:!1},mg=1,xg=2,_g=3,bg=Pa();function wg(t,e,n){var i={},r=Mg(e);if(!r||!t)return i;var o,a,s=[],l=[],u=e.ecModel,c=bg(u).datasetMap,h=r.uid+"_"+n.seriesLayoutBy;B(t=t.slice(),function(e,n){var r=$(e)?e:t[n]={name:e};"ordinal"===r.type&&null==o&&(o=n,a=f(r)),i[r.name]=[]});var d=c.get(h)||c.set(h,{categoryWayDim:a,valueWayDim:0});function p(t,e,n){for(var i=0;ie)return t[i];return t[n-1]}(i,a):n;if((c=c||n)&&c.length){var h=c[l];return r&&(u[r]=h),s.paletteIdx=(l+1)%c.length,h}}var Eg="\0_ec_inner";var zg=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.init=function(t,e,n,i,r,o){i=i||{},this.option=null,this._theme=new kp(i),this._locale=new kp(r),this._optionManager=o},e.prototype.setOption=function(t,e,n){var i=Gg(e);this._optionManager.setOption(t,n,i),this._resetOption(null,i)},e.prototype.resetOption=function(t,e){return this._resetOption(t,Gg(e))},e.prototype._resetOption=function(t,e){var n=!1,i=this._optionManager;if(!t||"recreate"===t){var r=i.mountOption("recreate"===t);0,this.option&&"recreate"!==t?(this.restoreData(),this._mergeOption(r,e)):kg(this,r),n=!0}if("timeline"!==t&&"media"!==t||this.restoreData(),!t||"recreate"===t||"timeline"===t){var o=i.getTimelineOption(this);o&&(n=!0,this._mergeOption(o,e))}if(!t||"recreate"===t||"media"===t){var a=i.getMediaOption(this);a.length&&B(a,function(t){n=!0,this._mergeOption(t,e)},this)}return n},e.prototype.mergeOption=function(t){this._mergeOption(t,null)},e.prototype._mergeOption=function(t,e){var n=this.option,i=this._componentsMap,r=this._componentsCount,o=[],a=mt(),s=e&&e.replaceMergeMainTypeMap;bg(this).datasetMap=mt(),B(t,function(t,e){null!=t&&(lg.hasClass(e)?e&&(o.push(e),a.set(e,!0)):n[e]=null==n[e]?C(t):D(n[e],t,!0))}),s&&s.each(function(t,e){lg.hasClass(e)&&!a.get(e)&&(o.push(e),a.set(e,!0))}),lg.topologicalTravel(o,lg.getAllClassMainTypes(),function(e){var o=function(t,e,n){var i=Cg.get(e);if(!i)return n;var r=i(t);return r?n.concat(r):n}(this,e,_a(t[e])),a=i.get(e),l=a?s&&s.get(e)?"replaceMerge":"normalMerge":"replaceAll",u=Ia(a,o,l);(function(t,e,n){B(t,function(t){var i=t.newOption;$(i)&&(t.keyInfo.mainType=e,t.keyInfo.subType=function(t,e,n,i){return e.type?e.type:n?n.subType:i.determineSubType(t,e)}(e,i,t.existing,n))})})(u,e,lg),n[e]=null,i.set(e,null),r.set(e,0);var c,h=[],d=[],p=0;B(u,function(t,n){var i=t.existing,r=t.newOption;if(r){var o="series"===e,a=lg.getClass(e,t.keyInfo.subType,!o);if(!a)return;if("tooltip"===e){if(c)return void 0;c=!0}if(i&&i.constructor===a)i.name=t.keyInfo.name,i.mergeOption(r,this),i.optionUpdated(r,!1);else{var s=k({componentIndex:n},t.keyInfo);k(i=new a(r,this,this,s),s),t.brandNew&&(i.__requireNewView=!0),i.init(r,this,this),i.optionUpdated(null,!0)}}else i&&(i.mergeOption({},this),i.optionUpdated({},!1));i?(h.push(i.option),d.push(i),p++):(h.push(void 0),d.push(void 0))},this),n[e]=h,i.set(e,d),r.set(e,p),"series"===e&&Dg(this)},this),this._seriesIndices||Dg(this)},e.prototype.getOption=function(){var t=C(this.option);return B(t,function(e,n){if(lg.hasClass(n)){for(var i=_a(e),r=i.length,o=!1,a=r-1;a>=0;a--)i[a]&&!ka(i[a])?o=!0:(i[a]=null,!o&&r--);i.length=r,t[n]=i}}),delete t[Eg],t},e.prototype.setTheme=function(t){this._theme=new kp(t),this._resetOption("recreate",null)},e.prototype.getTheme=function(){return this._theme},e.prototype.getLocaleModel=function(){return this._locale},e.prototype.setUpdatePayload=function(t){this._payload=t},e.prototype.getUpdatePayload=function(){return this._payload},e.prototype.getComponent=function(t,e){var n=this._componentsMap.get(t);if(n){var i=n[e||0];if(i)return i;if(null==e)for(var r=0;r=e:"max"===n?t<=e:t===e})(i[a],t,o)||(r=!1)}}),r}var Ug=B,Yg=$,Xg=["areaStyle","lineStyle","nodeStyle","linkStyle","chordStyle","label","labelLine"];function Zg(t){var e=t&&t.itemStyle;if(e)for(var n=0,i=Xg.length;n0?t[n-1].seriesModel:null)}),function(t){B(t,function(e,n){var i=[],r=[NaN,NaN],o=[e.stackResultDimension,e.stackedOverDimension],a=e.data,s=e.isStackedByIndex,l=e.seriesModel.get("stackStrategy")||"samesign";a.modify(o,function(o,u,c){var h,d,p=a.get(e.stackedDimension,c);if(isNaN(p))return r;s?d=a.getRawIndex(c):h=a.get(e.stackedByDimension,c);for(var f=NaN,g=n-1;g>=0;g--){var v=t[g];if(s||(d=v.data.rawIndexOf(v.stackedByDimension,h)),d>=0){var y=v.data.getByRawIndex(v.stackResultDimension,d);if("all"===l||"positive"===l&&y>0||"negative"===l&&y<0||"samesign"===l&&p>=0&&y>0||"samesign"===l&&p<=0&&y<0){p=jo(p,y),f=y;break}}}return i[0]=p,i[1]=f,i})})}(t))})});var hv,dv,pv,fv,gv,vv,yv=function(t){this.data=t.data||(t.sourceFormat===Ou?{}:[]),this.sourceFormat=t.sourceFormat||Nu,this.seriesLayoutBy=t.seriesLayoutBy||Eu,this.startIndex=t.startIndex||0,this.dimensionsDetectedCount=t.dimensionsDetectedCount,this.metaRawOption=t.metaRawOption;var e=this.dimensionsDefine=t.dimensionsDefine;if(e)for(var n=0;nu&&(u=p)}s[0]=l,s[1]=u}},i=function(){return this._data?this._data.length/this._dimSize:0};function r(t){for(var e=0;e=0&&(s=o.interpolatedValue[l])}return null!=s?s+"":""}):void 0},t.prototype.getRawValue=function(t,e){return zv(this.getData(e),t)},t.prototype.formatTooltip=function(t,e,n){},t}();function Gv(t){var e,n;return $(t)?t.type&&(n=t):e=t,{text:e,frag:n}}function Fv(t){return new Hv(t)}var Hv=function(){function t(t){t=t||{},this._reset=t.reset,this._plan=t.plan,this._count=t.count,this._onDirty=t.onDirty,this._dirty=!0}return t.prototype.perform=function(t){var e,n=this._upstream,i=t&&t.skip;if(this._dirty&&n){var r=this.context;r.data=r.outputData=n.context.outputData}this.__pipeline&&(this.__pipeline.currentTask=this),this._plan&&!i&&(e=this._plan(this.context));var o,a=c(this._modBy),s=this._modDataCount||0,l=c(t&&t.modBy),u=t&&t.modDataCount||0;function c(t){return!(t>=1)&&(t=1),t}a===l&&s===u||(e="reset"),(this._dirty||"reset"===e)&&(this._dirty=!1,o=this._doReset(i)),this._modBy=l,this._modDataCount=u;var h=t&&t.step;if(this._dueEnd=n?n._outputDueEnd:this._count?this._count(this.context):1/0,this._progress){var d=this._dueIndex,p=Math.min(null!=h?this._dueIndex+h:1/0,this._dueEnd);if(!i&&(o||d1&&i>0?s:a}};return o;function a(){return e=t?null:oe},gte:function(t,e){return t>=e}},jv=function(){function t(t,e){if(!K(e)){var n="";0,va(n)}this._opFn=Zv[t],this._rvalFloat=oa(e)}return t.prototype.evaluate=function(t){return K(t)?this._opFn(t,this._rvalFloat):this._opFn(oa(t),this._rvalFloat)},t}(),qv=function(){function t(t,e){var n="desc"===t;this._resultLT=n?1:-1,null==e&&(e=n?"min":"max"),this._incomparable="min"===e?-1/0:1/0}return t.prototype.evaluate=function(t,e){var n=K(t)?t:oa(t),i=K(e)?e:oa(e),r=isNaN(n),o=isNaN(i);if(r&&(n=this._incomparable),o&&(i=this._incomparable),r&&o){var a=j(t),s=j(e);a&&(n=s?t:0),s&&(i=a?e:0)}return ni?-this._resultLT:0},t}(),Kv=function(){function t(t,e){this._rval=e,this._isEQ=t,this._rvalTypeof=typeof e,this._rvalFloat=oa(e)}return t.prototype.evaluate=function(t){var e=t===this._rval;if(!e){var n=typeof t;n===this._rvalTypeof||"number"!==n&&"number"!==this._rvalTypeof||(e=oa(t)===this._rvalFloat)}return this._isEQ?e:!e},t}();function $v(t,e){return"eq"===t||"ne"===t?new Kv("eq"===t,e):wt(Zv,t)?new jv(t,e):null}function Jv(t){var e="",n=-1/0,i=-1/0,r=1/0,o=1/0;return t&&(null!=t.g&&(e+="G"+t.g,n=t.g),null!=t.ge&&(e+="GE"+t.ge,i=t.ge),null!=t.l&&(e+="L"+t.l,r=t.l),null!=t.le&&(e+="LE"+t.le,o=t.le)),{key:e,g:n,ge:i,l:r,le:o}}function Qv(t,e){return e>t.g&&e>=t.ge&&e65535?uy:cy}function gy(t){var e=t.constructor;return e===Array?t.slice():new e(t)}function vy(t,e,n,i,r){var o=py[n||"float"];if(r){var a=t[e],s=a&&a.length;if(s!==i){for(var l=new o(i),u=0;ug[1]&&(g[1]=f)}return this._rawCount=this._count=s,{start:a,end:s}},t.prototype._initDataFromProvider=function(t,e,n){for(var i=this._provider,r=this._chunks,o=this._dimensions,a=o.length,s=this._rawExtent,l=V(o,function(t){return t.property}),u=0;uv[1]&&(v[1]=g)}}!i.persistent&&i.clean&&i.clean(),this._rawCount=this._count=e,this._extent=[]},t.prototype.count=function(){return this._count},t.prototype.get=function(t,e){if(!(e>=0&&e=0&&e=this._rawCount||t<0)return-1;if(!this._indices)return t;var e=this._indices,n=e[t];if(null!=n&&nt))return o;r=o-1}}return-1},t.prototype.getIndices=function(){var t,e=this._indices;if(e){var n=e.constructor,i=this._count;if(n===Array){t=new n(i);for(var r=0;r=u&&x<=c||isNaN(x))&&(a[s++]=p),p++}d=!0}else if(2===r){f=h[i[0]];var v=h[i[1]],y=t[i[1]][0],m=t[i[1]][1];for(g=0;g=u&&x<=c||isNaN(x))&&(_>=y&&_<=m||isNaN(_))&&(a[s++]=p),p++}d=!0}}if(!d)if(1===r)for(g=0;g=u&&x<=c||isNaN(x))&&(a[s++]=b)}else for(g=0;gt[M][1])&&(w=!1)}w&&(a[s++]=e.getRawIndex(g))}return sv[1]&&(v[1]=g)}}}},t.prototype.lttbDownSample=function(t,e){var n,i,r,o=this.clone([t],!0),a=o._chunks[t],s=this.count(),l=0,u=Math.floor(1/e),c=this.getRawIndex(0),h=new(fy(this._rawCount))(Math.min(2*(Math.ceil(s/u)+2),s));h[l++]=c;for(var d=1;dn&&(n=i,r=I)}M>0&&M<_-x&&(h[l++]=Math.min(S,r),r=Math.max(S,r)),h[l++]=r,c=r}return h[l++]=this.getRawIndex(s-1),o._count=l,o._indices=h,o.getRawIndex=this._getRawIdx,o},t.prototype.minmaxDownSample=function(t,e){for(var n=this.clone([t],!0),i=n._chunks,r=Math.floor(1/e),o=i[t],a=this.count(),s=new(fy(this._rawCount))(2*Math.ceil(a/r)),l=0,u=0;ua&&(f=a-u);for(var g=0;gp&&(p=v,d=u+g)}var y=this.getRawIndex(c),m=this.getRawIndex(d);cu-p&&(s=u-p,a.length=s);for(var f=0;fc[1]&&(c[1]=v),h[d++]=y}return r._count=d,r._indices=h,r._updateGetRawIdx(),r},t.prototype.each=function(t,e){if(this._count)for(var n=t.length,i=this._chunks,r=0,o=this.count();rh&&(h=p))}return a[l]=[c,h]},t.prototype.getRawDataItem=function(t){var e=this.getRawIndex(t);if(this._provider.persistent)return this._provider.getItem(e);for(var n=[],i=this._chunks,r=0;r=0?this._indices[t]:-1},t.prototype._updateGetRawIdx=function(){this.getRawIndex=this._indices?this._getRawIdx:this._getRawIdxIdentity},t.internalField=function(){function t(t,e,n,i){return Uv(t[i],this._dimensions[i])}ly={arrayRows:t,objectRows:function(t,e,n,i){return Uv(t[e],this._dimensions[i])},keyedColumns:t,original:function(t,e,n,i){var r=t&&(null==t.value?t:t.value);return Uv(r instanceof Array?r[i]:r,this._dimensions[i])},typedArray:function(t,e,n,i){return t[i]}}}(),t}(),my=function(){function t(t){this._sourceList=[],this._storeList=[],this._upstreamSignList=[],this._versionSignBase=0,this._dirty=!0,this._sourceHost=t}return t.prototype.dirty=function(){this._setLocalSource([],[]),this._storeList=[],this._dirty=!0},t.prototype._setLocalSource=function(t,e){this._sourceList=t,this._upstreamSignList=e,this._versionSignBase++,this._versionSignBase>9e10&&(this._versionSignBase=0)},t.prototype._getVersionSign=function(){return this._sourceHost.uid+"_"+this._versionSignBase},t.prototype.prepareSource=function(){this._isDirty()&&(this._createSource(),this._dirty=!1)},t.prototype._createSource=function(){this._setLocalSource([],[]);var t,e,n=this._sourceHost,i=this._getUpstreamSourceManagers(),r=!!i.length;if(_y(n)){var o=n,a=void 0,s=void 0,l=void 0;if(r){var u=i[0];u.prepareSource(),a=(l=u.getSource()).data,s=l.sourceFormat,e=[u._getVersionSign()]}else s=Q(a=o.get("data",!0))?Ru:ku,e=[];var c=this._getSourceMetaRawOption()||{},h=l&&l.metaRawOption||{},d=at(c.seriesLayoutBy,h.seriesLayoutBy)||null,p=at(c.sourceHeader,h.sourceHeader),f=at(c.dimensions,h.dimensions);t=d!==h.seriesLayoutBy||!!p!=!!h.sourceHeader||f?[xv(a,{seriesLayoutBy:d,sourceHeader:p,dimensions:f},s)]:[]}else{var g=n;if(r){var v=this._applyTransform(i);t=v.sourceList,e=v.upstreamSignList}else{t=[xv(g.get("source",!0),this._getSourceMetaRawOption(),null)],e=[]}}this._setLocalSource(t,e)},t.prototype._applyTransform=function(t){var e,n=this._sourceHost,i=n.get("transform",!0),r=n.get("fromTransformResult",!0);if(null!=r){var o="";1!==t.length&&by(o)}var a,s=[],l=[];return B(t,function(t){t.prepareSource();var e=t.getSource(r||0),n="";null==r||e||by(n),s.push(e),l.push(t._getVersionSign())}),i?e=function(t,e){var n=_a(t),i=n.length,r="";i||va(r);for(var o=0,a=i;o1||n>0&&!t.noHeader;return B(t.blocks,function(t){var n=Ay(t);n>=e&&(e=n+ +(i&&(!n||Cy(t)&&!t.noHeader)))}),e}return 0}function ky(t,e,n,i){var r,o=e.noHeader,a=(r=Ay(e),{html:My[r],richText:Iy[r]}),s=[],l=e.blocks||[];ct(!l||X(l)),l=l||[];var u=t.orderMode;if(e.sortBlocks&&u){l=l.slice();var c={valueAsc:"asc",valueDesc:"desc"};if(wt(c,u)){var h=new qv(c[u],null);l.sort(function(t,e){return h.evaluate(t.sortParam,e.sortParam)})}else"seriesDesc"===u&&l.reverse()}B(l,function(n,r){var o=e.valueFormatter,l=Dy(n)(o?k(k({},t),{valueFormatter:o}):t,n,r>0?a.html:0,i);null!=l&&s.push(l)});var d="richText"===t.renderMode?s.join(a.richText):Oy(i,s.join(""),o?n:a.html);if(o)return d;var p=Af(e.header,"ordinal",t.useUTC),f=Sy(i,t.renderMode).nameStyle,g=wy(i);return"richText"===t.renderMode?Ry(t,p,f)+a.richText+d:Oy(i,'
'+se(p)+"
"+d,n)}function Ly(t,e,n,i){var r=t.renderMode,o=e.noName,a=e.noValue,s=!e.markerType,l=e.name,u=t.useUTC,c=e.valueFormatter||t.valueFormatter||function(t){return V(t=X(t)?t:[t],function(t,e){return Af(t,X(p)?p[e]:p,u)})};if(!o||!a){var h=s?"":t.markupStyleCreator.makeTooltipMarker(e.markerType,e.markerColor||ug.color.secondary,r),d=o?"":Af(l,"ordinal",u),p=e.valueType,f=a?[]:c(e.value,e.rawDataIndex),g=!s||!o,v=!s&&o,y=Sy(i,r),m=y.nameStyle,x=y.valueStyle;return"richText"===r?(s?"":h)+(o?"":Ry(t,d,m))+(a?"":function(t,e,n,i,r){var o=[r],a=i?10:20;return n&&o.push({padding:[0,0,0,a],align:"right"}),t.markupStyleCreator.wrapRichTextStyle(X(e)?e.join(" "):e,o)}(t,f,g,v,x)):Oy(i,(s?"":h)+(o?"":function(t,e,n){return''+se(t)+""}(d,!s,m))+(a?"":function(t,e,n,i){var r=n?"10px":"20px",o=e?"float:right;margin-left:"+r:"";return t=X(t)?t:[t],''+V(t,function(t){return se(t)}).join("  ")+""}(f,g,v,x)),n)}}function Py(t,e,n,i,r,o){if(t)return Dy(t)({useUTC:r,renderMode:n,orderMode:i,markupStyleCreator:e,valueFormatter:t.valueFormatter},t,0,o)}function Oy(t,e,n){return'
'+e+'
'}function Ry(t,e,n){return t.markupStyleCreator.wrapRichTextStyle(e,n)}function Ny(t,e){return Nf(t.getData().getItemVisual(e,"style")[t.visualDrawType])}function Ey(t,e){var n=t.get("padding");return null!=n?n:"richText"===e?[8,10]:10}var zy=function(){function t(){this.richTextStyles={},this._nextStyleNameId=sa()}return t.prototype._generateStyleName=function(){return"__EC_aUTo_"+this._nextStyleNameId++},t.prototype.makeTooltipMarker=function(t,e,n){var i="richText"===n?this._generateStyleName():null,r=Rf({color:e,type:t,renderMode:n,markerId:i});return j(r)?r:(this.richTextStyles[i]=r.style,r.content)},t.prototype.wrapRichTextStyle=function(t,e){var n={};X(e)?B(e,function(t){return k(n,t)}):k(n,e);var i=this._generateStyleName();return this.richTextStyles[i]=n,"{"+i+"|"+t+"}"},t}();function By(t){var e,n,i,r,o=t.series,a=t.dataIndex,s=t.multipleSeries,l=o.getData(),u=l.mapDimensionsAll("defaultedTooltip"),c=u.length,h=o.getRawValue(a),d=X(h),p=Ny(o,a);if(c>1||d&&!c){var f=function(t,e,n,i,r){var o=e.getData(),a=G(t,function(t,e,n){var i=o.getDimensionInfo(n);return t||i&&!1!==i.tooltip&&null!=i.displayName},!1),s=[],l=[],u=[];function c(t,e){var n=o.getDimensionInfo(e);n&&!1!==n.otherDims.tooltip&&(a?u.push(Ty("nameValue",{markerType:"subItem",markerColor:r,name:n.displayName,value:t,valueType:n.type})):(s.push(t),l.push(n.type)))}return i.length?B(i,function(t){c(zv(o,n,t),t)}):B(t,c),{inlineValues:s,inlineValueTypes:l,blocks:u}}(h,o,a,u,p);e=f.inlineValues,n=f.inlineValueTypes,i=f.blocks,r=f.inlineValues[0]}else if(c){var g=l.getDimensionInfo(u[0]);r=e=zv(l,a,u[0]),n=g.type}else r=e=d?h[0]:h;var v=Aa(o),y=v&&o.name||"",m=l.getName(a),x=s?y:m;return Ty("section",{header:y,noHeader:s||!v,sortParam:r,blocks:[Ty("nameValue",{markerType:"item",markerColor:p,name:x,noName:!ht(x),value:e,valueType:n,rawDataIndex:l.getRawIndex(a)})].concat(i||[])})}var Vy=Pa();function Gy(t,e){return t.getName(e)||t.getId(e)}var Fy="__universalTransitionEnabled",Hy=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e._selectedDataIndicesMap={},e}var i;return n(e,t),e.prototype.init=function(t,e,n){this.seriesIndex=this.componentIndex,this.dataTask=Fv({count:Uy,reset:Yy}),this.dataTask.context={model:this},this.mergeDefaultAndTheme(t,n),(Vy(this).sourceManager=new my(this)).prepareSource();var i=this.getInitialData(t,n);Zy(i,this),this.dataTask.context.data=i,Vy(this).dataBeforeProcessed=i,Wy(this),this._initSelectedMapFromData(i)},e.prototype.mergeDefaultAndTheme=function(t,e){var n=ig(this),i=n?og(t):{},r=this.subType;lg.hasClass(r)&&(r+="Series"),D(t,e.getTheme().get(this.subType)),D(t,this.getDefaultOption()),ba(t,"label",["show"]),this.fillDataTextStyle(t.data),n&&rg(t,i,n)},e.prototype.mergeOption=function(t,e){t=D(this.option,t,!0),this.fillDataTextStyle(t.data);var n=ig(this);n&&rg(this.option,t,n);var i=Vy(this).sourceManager;i.dirty(),i.prepareSource();var r=this.getInitialData(t,e);Zy(r,this),this.dataTask.dirty(),this.dataTask.context.data=r,Vy(this).dataBeforeProcessed=r,Wy(this),this._initSelectedMapFromData(r)},e.prototype.fillDataTextStyle=function(t){if(t&&!Q(t))for(var e=["show"],n=0;n=0&&c<0)&&(u=m,c=y,h=0),y===c&&(l[h++]=f))}return l.length=h,l},e.prototype.formatTooltip=function(t,e,n){return By({series:this,dataIndex:t,multipleSeries:e})},e.prototype.isAnimationEnabled=function(){var t=this.ecModel;if(r.node&&(!t||!t.ssr))return!1;var e=this.getShallow("animation");return e&&this.getData().count()>this.getShallow("animationThreshold")&&(e=!1),!!e},e.prototype.restoreData=function(){this.dataTask.dirty()},e.prototype.getColorFromPalette=function(t,e,n){var i=this.ecModel,r=Og.prototype.getColorFromPalette.call(this,t,e,n);return r||(r=i.getColorFromPalette(t,e,n)),r},e.prototype.coordDimToDataDim=function(t){return this.getRawData().mapDimensionsAll(t)},e.prototype.getProgressive=function(){return this.get("progressive")},e.prototype.getProgressiveThreshold=function(){return this.get("progressiveThreshold")},e.prototype.select=function(t,e){this._innerSelect(this.getData(e),t)},e.prototype.unselect=function(t,e){var n=this.option.selectedMap;if(n){var i=this.option.selectedMode,r=this.getData(e);if("series"===i||"all"===n)return this.option.selectedMap={},void(this._selectedDataIndicesMap={});for(var o=0;o=0&&n.push(r)}return n},e.prototype.isSelected=function(t,e){var n=this.option.selectedMap;if(!n)return!1;var i=this.getData(e);return("all"===n||n[Gy(i,t)])&&!i.getItemModel(t).get(["select","disabled"])},e.prototype.isUniversalTransitionEnabled=function(){if(this[Fy])return!0;var t=this.option.universalTransition;return!!t&&(!0===t||t&&t.enabled)},e.prototype._innerSelect=function(t,e){var n,i,r=this.option,o=r.selectedMode,a=e.length;if(o&&a)if("series"===o)r.selectedMap="all";else if("multiple"===o){$(r.selectedMap)||(r.selectedMap={});for(var s=r.selectedMap,l=0;l0&&this._innerSelect(t,e)}},e.registerClass=function(t){return lg.registerClass(t)},e.protoInitialize=((i=e.prototype).type="series.__base__",i.seriesIndex=0,i.ignoreStyleOnData=!1,i.hasSymbolVisual=!1,i.defaultSymbol="circle",i.visualStyleAccessPath="itemStyle",void(i.visualDrawType="fill")),e}(lg);function Wy(t){var e=t.name;Aa(t)||(t.name=function(t){var e=t.getRawData(),n=e.mapDimensionsAll("seriesName"),i=[];return B(n,function(t){var n=e.getDimensionInfo(t);n.displayName&&i.push(n.displayName)}),i.join(" ")}(t)||e)}function Uy(t){return t.model.getRawData().count()}function Yy(t){var e=t.model;return e.setData(e.getRawData().cloneShallow()),Xy}function Xy(t,e){e.outputData&&t.end>e.outputData.count()&&e.model.getRawData().cloneShallow(e.outputData)}function Zy(t,e){B(xt(t.CHANGABLE_METHODS,t.DOWNSAMPLE_METHODS),function(n){t.wrapMethod(n,Y(jy,e))})}function jy(t,e){var n=qy(t);return n&&n.setOutputEnd((e||this).count()),e}function qy(t){var e=(t.ecModel||{}).scheduler,n=e&&e.getPipeline(t.uid);if(n){var i=n.currentTask;if(i){var r=i.agentStubMap;r&&(i=r.get(t.uid))}return i}}E(Hy,Vv),E(Hy,Og),hs(Hy,lg);var Ky=function(){function t(){this.group=new vo,this.uid=Pp("viewComponent")}return t.prototype.init=function(t,e){},t.prototype.render=function(t,e,n,i){},t.prototype.dispose=function(t,e){},t.prototype.updateView=function(t,e,n,i){},t.prototype.updateLayout=function(t,e,n,i){},t.prototype.updateVisual=function(t,e,n,i){},t.prototype.toggleBlurSeries=function(t,e,n){},t.prototype.eachRendered=function(t){var e=this.group;e&&e.traverse(t)},t}();function $y(){var t=Pa();return function(e){var n=t(e),i=e.pipelineContext,r=!!n.large,o=!!n.progressiveRender,a=n.large=!(!i||!i.large),s=n.progressiveRender=!(!i||!i.progressiveRender);return!(r===a&&o===s)&&"reset"}}cs(Ky),gs(Ky);var Jy=Pa(),Qy=$y(),tm=function(){function t(){this.group=new vo,this.uid=Pp("viewChart"),this.renderTask=Fv({plan:im,reset:rm}),this.renderTask.context={view:this}}return t.prototype.init=function(t,e){},t.prototype.render=function(t,e,n,i){0},t.prototype.highlight=function(t,e,n,i){var r=t.getData(i&&i.dataType);r&&nm(r,i,"emphasis")},t.prototype.downplay=function(t,e,n,i){var r=t.getData(i&&i.dataType);r&&nm(r,i,"normal")},t.prototype.remove=function(t,e){this.group.removeAll()},t.prototype.dispose=function(t,e){},t.prototype.updateView=function(t,e,n,i){this.render(t,e,n,i)},t.prototype.updateVisual=function(t,e,n,i){this.render(t,e,n,i)},t.prototype.eachRendered=function(t){Hd(this.group,t)},t.markUpdateMethod=function(t,e){Jy(t).updateMethod=e},t.protoInitialize=void(t.prototype.type="chart"),t}();function em(t,e,n){t&&Ec(t)&&("emphasis"===e?vc:yc)(t,n)}function nm(t,e,n){var i=La(t,e),r=e&&null!=e.highlightKey?function(t){var e=Wu[t];return null==e&&Hu<=32&&(e=Wu[t]=Hu++),e}(e.highlightKey):null;null!=i?B(_a(i),function(e){em(t.getItemGraphicEl(e),n,r)}):t.eachItemGraphicEl(function(t){em(t,n,r)})}function im(t){return Qy(t.model)}function rm(t){var e=t.model,n=t.ecModel,i=t.api,r=t.payload,o=e.pipelineContext.progressiveRender,a=t.view,s=r&&Jy(r).updateMethod,l=o?"incrementalPrepareRender":s&&a[s]?s:"render";return"render"!==l&&a[l](e,n,i,r),om[l]}cs(tm),gs(tm);var om={incrementalPrepareRender:{progress:function(t,e){e.view.incrementalRender(t,e.model,e.ecModel,e.api,e.payload)}},render:{forceFirstProgress:!0,progress:function(t,e){e.view.render(e.model,e.ecModel,e.api,e.payload)}}},am="\0__throttleOriginMethod",sm="\0__throttleRate",lm="\0__throttleType";function um(t,e,n){var i,r,o,a,s,l=0,u=0,c=null;function h(){u=(new Date).getTime(),c=null,t.apply(o,a||[])}e=e||0;var d=function(){for(var t=[],d=0;d=0?h():c=setTimeout(h,-r),l=i};return d.clear=function(){c&&(clearTimeout(c),c=null)},d.debounceNextCall=function(t){s=t},d}function cm(t,e,n,i){var r=t[e];if(r){var o=r[am]||r,a=r[lm];if(r[sm]!==n||a!==i){if(null==n||!i)return t[e]=o;(r=t[e]=um(o,n,"debounce"===i))[am]=o,r[lm]=i,r[sm]=n}return r}}function hm(t,e){var n=t[e];n&&n[am]&&(n.clear&&n.clear(),t[e]=n[am])}var dm=Pa(),pm={itemStyle:vs(Cp,!0),lineStyle:vs(Mp,!0)},fm={lineStyle:"stroke",itemStyle:"fill"};function gm(t,e){var n=t.visualStyleMapper||pm[e];return n||(console.warn("Unknown style type '"+e+"'."),pm.itemStyle)}function vm(t,e){var n=t.visualDrawType||fm[e];return n||(console.warn("Unknown style type '"+e+"'."),"fill")}var ym={createOnAllSeries:!0,performRawSeries:!0,reset:function(t,e){var n=t.getData(),i=t.visualStyleAccessPath||"itemStyle",r=t.getModel(i),o=gm(t,i)(r),a=r.getShallow("decal");a&&(n.setVisual("decal",a),a.dirty=!0);var s=vm(t,i),l=o[s],u=Z(l)?l:null,c="auto"===o.fill||"auto"===o.stroke;if(!o[s]||u||c){var h=t.getColorFromPalette(t.name,null,e.getSeriesCount());o[s]||(o[s]=h,n.setVisual("colorFromPalette",!0)),o.fill="auto"===o.fill||Z(o.fill)?h:o.fill,o.stroke="auto"===o.stroke||Z(o.stroke)?h:o.stroke}if(n.setVisual("style",o),n.setVisual("drawType",s),!e.isSeriesFiltered(t)&&u)return n.setVisual("colorFromPalette",!1),{dataEach:function(e,n){var i=t.getDataParams(n),r=k({},o);r[s]=u(i),e.setItemVisual(n,"style",r)}}}},mm=new kp,xm={createOnAllSeries:!0,reset:function(t,e){if(!t.ignoreStyleOnData){var n=t.getData(),i=t.visualStyleAccessPath||"itemStyle",r=gm(t,i),o=n.getVisual("drawType");return{dataEach:n.hasItemOption?function(t,e){var n=t.getRawDataItem(e);if(n&&n[i]){mm.option=n[i];var a=r(mm);k(t.ensureUniqueItemVisual(e,"style"),a),mm.option.decal&&(t.setItemVisual(e,"decal",mm.option.decal),mm.option.decal.dirty=!0),o in a&&t.setItemVisual(e,"colorFromPalette",!1)}}:null}}}},_m={performRawSeries:!0,overallReset:function(t){var e=mt();t.eachSeries(function(t){if(!t.isColorBySeries()){var n=t.type+"-"+t.getColorBy();dm(t).scope=e.get(n)||e.set(n,{})}}),t.eachSeries(function(t){if(!t.isColorBySeries()){var e=t.getRawData(),n={},i=t.getData(),r=dm(t).scope,o=t.visualStyleAccessPath||"itemStyle",a=vm(t,o);i.each(function(t){var e=i.getRawIndex(t);n[e]=t}),e.each(function(o){var s=n[o];if(i.getItemVisual(s,"colorFromPalette")){var l=i.ensureUniqueItemVisual(s,"style"),u=e.getName(o)||o+"",c=e.count();l[a]=t.getColorFromPalette(u,r,c)}})}})}},bm=Math.PI;var wm=function(){function t(t,e,n,i){this._stageTaskMap=mt(),this.ecInstance=t,this.api=e,n=this._dataProcessorHandlers=n.slice(),i=this._visualHandlers=i.slice(),this._allHandlers=n.concat(i)}return t.prototype.restoreData=function(t,e){t.restoreData(e),this._stageTaskMap.each(function(t){var e=t.overallTask;e&&e.dirty()})},t.prototype.getPerformArgs=function(t,e){if(t.__pipeline){var n=this._pipelineMap.get(t.__pipeline.id),i=n.context,r=!e&&n.progressiveEnabled&&(!i||i.progressiveRender)&&t.__idxInPipeline>n.blockIndex?n.step:null,o=i&&i.modDataCount;return{step:r,modBy:null!=o?Math.ceil(o/r):null,modDataCount:o}}},t.prototype.getPipeline=function(t){return this._pipelineMap.get(t)},t.prototype.updateStreamModes=function(t,e){var n=this._pipelineMap.get(t.uid),i=t.__preparePipelineContext?t.__preparePipelineContext(e,n):rs(t,e,n);t.pipelineContext=n.context=i},t.prototype.restorePipelines=function(t,e){var n=this,i=n._pipelineMap=mt();e.eachSeries(function(e){var r="canvas"===t.painter.type&&e.getProgressive(),o=e.uid;i.set(o,{id:o,head:null,tail:null,threshold:e.getProgressiveThreshold(),progressiveEnabled:r&&!(e.preventIncremental&&e.preventIncremental()),blockIndex:-1,step:Math.round(r||700),count:0}),n._pipe(e,e.dataTask)})},t.prototype.prepareStageTasks=function(){var t=this._stageTaskMap,e=this.api.getModel(),n=this.api;B(this._allHandlers,function(i){var r=t.get(i.uid)||t.set(i.uid,{}),o="";ct(!(i.reset&&i.overallReset),o),i.reset&&this._createSeriesStageTask(i,r,e,n),i.overallReset&&this._createOverallStageTask(i,r,e,n)},this)},t.prototype.prepareView=function(t,e,n,i){var r=t.renderTask,o=r.context;o.model=e,o.ecModel=n,o.api=i,r.__block=!t.incrementalPrepareRender,this._pipe(e,r)},t.prototype.performDataProcessorTasks=function(t,e){this._performStageTasks(this._dataProcessorHandlers,t,e,{block:!0})},t.prototype.performVisualTasks=function(t,e,n){this._performStageTasks(this._visualHandlers,t,e,n)},t.prototype._performStageTasks=function(t,e,n,i){i=i||{};var r=!1,o=this;function a(t,e){return t.setDirty&&(!t.dirtyMap||t.dirtyMap.get(e.__pipeline.id))}B(t,function(t,s){if(!i.visualType||i.visualType===t.visualType){var l=o._stageTaskMap.get(t.uid),u=l.seriesTaskMap,c=l.overallTask;if(c){var h,d=c.agentStubMap;d.each(function(t){a(i,t)&&(t.dirty(),h=!0)}),h&&c.dirty(),o.updatePayload(c,n);var p=o.getPerformArgs(c,i.block);d.each(function(t){t.perform(p)}),c.perform(p)&&(r=!0)}else u&&u.each(function(s,l){a(i,s)&&s.dirty();var u=o.getPerformArgs(s,i.block);u.skip=!t.performRawSeries&&e.isSeriesFiltered(s.context.model),o.updatePayload(s,n),s.perform(u)&&(r=!0)})}}),this.unfinished=r||this.unfinished},t.prototype.performSeriesTasks=function(t){var e;t.eachSeries(function(t){e=t.dataTask.perform()||e}),this.unfinished=e||this.unfinished},t.prototype.plan=function(){this._pipelineMap.each(function(t){var e=t.tail;do{if(e.__block){t.blockIndex=e.__idxInPipeline;break}e=e.getUpstream()}while(e)})},t.prototype.updatePayload=function(t,e){"remain"!==e&&(t.context.payload=e)},t.prototype._createSeriesStageTask=function(t,e,n,i){var r=this,o=e.seriesTaskMap,a=e.seriesTaskMap=mt(),s=t.seriesType,l=t.getTargetSeries;function u(e){var s=e.uid,l=a.set(s,o&&o.get(s)||Fv({plan:Cm,reset:Dm,count:Lm}));l.context={model:e,ecModel:n,api:i,useClearVisual:t.isVisual&&!t.isLayout,plan:t.plan,reset:t.reset,scheduler:r},r._pipe(e,l)}t.createOnAllSeries?n.eachRawSeries(u):s?n.eachRawSeriesByType(s,u):l&&l(n,i).each(u)},t.prototype._createOverallStageTask=function(t,e,n,i){var r=this,o=e.overallTask=e.overallTask||Fv({reset:Sm});o.context={ecModel:n,api:i,overallReset:t.overallReset,scheduler:r};var a=o.agentStubMap,s=o.agentStubMap=mt(),l=t.seriesType,u=t.getTargetSeries,c=t.dirtyOnOverallProgress,h=!1,d="";function p(t){var e=t.uid,n=s.set(e,a&&a.get(e)||(h=!0,Fv({reset:Mm,onDirty:Tm})));n.context={model:t,dirtyOnOverallProgress:c},n.agent=o,n.__block=c,r._pipe(t,n)}ct(!t.createOnAllSeries,d),l?n.eachRawSeriesByType(l,p):u?u(n,i).each(p):B(n.getSeries(),p),h&&o.dirty()},t.prototype._pipe=function(t,e){var n=t.uid,i=this._pipelineMap.get(n);!i.head&&(i.head=e),i.tail&&i.tail.pipe(e),i.tail=e,e.__idxInPipeline=i.count++,e.__pipeline=i},t.wrapStageHandler=function(t,e){return Z(t)&&(t={overallReset:t,seriesType:Pm(t)}),t.uid=Pp("stageHandler"),e&&(t.visualType=e),t},t}();function Sm(t){t.overallReset(t.ecModel,t.api,t.payload)}function Mm(t){return t.dirtyOnOverallProgress&&Im}function Im(){this.agent.dirty(),this.getDownstream().dirty()}function Tm(){this.agent&&this.agent.dirty()}function Cm(t){return t.plan?t.plan(t.model,t.ecModel,t.api,t.payload):null}function Dm(t){t.useClearVisual&&t.data.clearAllVisual();var e=t.resetDefines=_a(t.reset(t.model,t.ecModel,t.api,t.payload));return e.length>1?V(e,function(t,e){return km(e)}):Am}var Am=km(0);function km(t){return function(e,n){var i=n.data,r=n.resetDefines[t];if(r&&r.dataEach)for(var o=e.start;o0&&c===r.length-u.length){var h=r.slice(0,c);"data"!==h&&(e.mainType=h,e[u.toLowerCase()]=t,s=!0)}}a.hasOwnProperty(r)&&(n[r]=t,s=!0),s||(i[r]=t)})}return{cptQuery:e,dataQuery:n,otherQuery:i}},t.prototype.filter=function(t,e){var n=this.eventInfo;if(!n)return!0;var i=n.targetEl,r=n.packedEvent,o=n.model,a=n.view;if(!o||!a)return!0;var s=e.cptQuery,l=e.dataQuery;return u(s,o,"mainType")&&u(s,o,"subType")&&u(s,o,"index","componentIndex")&&u(s,o,"name")&&u(s,o,"id")&&u(l,r,"name")&&u(l,r,"dataIndex")&&u(l,r,"dataType")&&(!a.filterForExposedEvent||a.filterForExposedEvent(t,e.otherQuery,i,r));function u(t,e,n,i){return null==t[n]||e[i||n]===t[n]}},t.prototype.afterTrigger=function(){this.eventInfo=null},t}(),Um=["symbol","symbolSize","symbolRotate","symbolOffset"],Ym=Um.concat(["symbolKeepAspect"]),Xm={createOnAllSeries:!0,performRawSeries:!0,reset:function(t,e){var n=t.getData();if(t.legendIcon&&n.setVisual("legendIcon",t.legendIcon),t.hasSymbolVisual){for(var i={},r={},o=!1,a=0;a=0&&Sx(l)?l:.5,t.createRadialGradient(a,s,0,a,s,l)}(t,e,n):function(t,e,n){var i=null==e.x?0:e.x,r=null==e.x2?1:e.x2,o=null==e.y?0:e.y,a=null==e.y2?0:e.y2;return e.global||(i=i*n.width+n.x,r=r*n.width+n.x,o=o*n.height+n.y,a=a*n.height+n.y),i=Sx(i)?i:0,r=Sx(r)?r:1,o=Sx(o)?o:0,a=Sx(a)?a:0,t.createLinearGradient(i,o,r,a)}(t,e,n),r=e.colorStops,o=0;o0&&(e=i.lineDash,n=i.lineWidth,e&&"solid"!==e&&n>0?"dashed"===e?[4*n,2*n]:"dotted"===e?[n]:K(e)?[e]:X(e)?e:null:null),o=i.lineDashOffset;if(r){var a=i.strokeNoScale&&t.getLineScale?t.getLineScale():1;a&&1!==a&&(r=V(r,function(t){return t/a}),o/=a)}return[r,o]}var Dx=new Al(!0);function Ax(t){var e=t.stroke;return!(null==e||"none"===e||!(t.lineWidth>0))}function kx(t){return"string"==typeof t&&"none"!==t}function Lx(t){var e=t.fill;return null!=e&&"none"!==e}function Px(t,e){if(null!=e.fillOpacity&&1!==e.fillOpacity){var n=t.globalAlpha;t.globalAlpha=e.fillOpacity*e.opacity,t.fill(),t.globalAlpha=n}else t.fill()}function Ox(t,e){if(null!=e.strokeOpacity&&1!==e.strokeOpacity){var n=t.globalAlpha;t.globalAlpha=e.strokeOpacity*e.opacity,t.stroke(),t.globalAlpha=n}else t.stroke()}function Rx(t,e,n){var i=bs(e.image,e.__image,n);if(Ss(i)){var r=t.createPattern(i,e.repeat||"repeat");if("function"==typeof DOMMatrix&&r&&r.setTransform){var o=new DOMMatrix;o.translateSelf(e.x||0,e.y||0),o.rotateSelf(0,0,(e.rotation||0)*Mt),o.scaleSelf(e.scaleX||1,e.scaleY||1),r.setTransform(o)}return r}}var Nx=["shadowBlur","shadowOffsetX","shadowOffsetY"],Ex=[["lineCap","butt"],["lineJoin","miter"],["miterLimit",10]];function zx(t,e,n,i,r){var o=!1;if(!i&&e===(n=n||{}))return!1;if(i||e.opacity!==n.opacity){Gx(t,r),o=!0;var a=Math.max(Math.min(e.opacity,1),0);t.globalAlpha=isNaN(a)?Ws.opacity:a}(i||e.blend!==n.blend)&&(o||(Gx(t,r),o=!0),t.globalCompositeOperation=e.blend||Ws.blend);for(var s=0;s0&&t.unfinished);t.unfinished||this._zr.flush()}}},e.prototype.getDom=function(){return this._dom},e.prototype.getId=function(){return this.id},e.prototype.getZr=function(){return this._zr},e.prototype.isSSR=function(){return this._ssr},e.prototype.setOption=function(t,e,n){if(!this[e_])if(this._disposed)R_(this.id);else{var i,r,o;if($(e)&&(n=e.lazyUpdate,i=e.silent,r=e.replaceMerge,o=e.transition,e=e.notMerge),this[e_]=!0,D_(this),!this._model||e){var a=new Hg(this._api),s=this._theme,l=this._model=new zg;l.scheduler=this._scheduler,l.ssr=this._ssr,l.init(null,null,null,s,this._locale,a)}this._model.setOption(t,{replaceMerge:r},V_);var u={seriesTransition:o,optionChanged:!0};if(n)this[i_]={silent:i,updateParams:u},this[e_]=!1,this.getZr().wakeUp();else{try{c_(this),p_.update.call(this,null,u)}catch(t){throw this[i_]=null,this[e_]=!1,t}this._ssr||this._zr.flush(),this[i_]=null,this[e_]=!1,y_.call(this,i),m_.call(this,i)}}},e.prototype.setTheme=function(t,e){if(!this[e_])if(this._disposed)R_(this.id);else{var n=this._model;if(n){var i=e&&e.silent,r=null;this[i_]&&(null==i&&(i=this[i_].silent),r=this[i_].updateParams,this[i_]=null),this[e_]=!0,D_(this);try{this._updateTheme(t),n.setTheme(this._theme),c_(this),p_.update.call(this,{type:"setTheme"},r)}catch(t){throw this[e_]=!1,t}this[e_]=!1,y_.call(this,i),m_.call(this,i)}}},e.prototype._updateTheme=function(t){j(t)&&(t=F_[t]),t&&((t=C(t))&&uv(t,!0),this._theme=t)},e.prototype.getModel=function(){return this._model},e.prototype.getOption=function(){return this._model&&this._model.getOption()},e.prototype.getWidth=function(){return this._zr.getWidth()},e.prototype.getHeight=function(){return this._zr.getHeight()},e.prototype.getDevicePixelRatio=function(){return this._zr.painter.dpr||r.hasGlobalWindow&&window.devicePixelRatio||1},e.prototype.getRenderedCanvas=function(t){return this.renderToCanvas(t)},e.prototype.renderToCanvas=function(t){t=t||{};var e=this._zr.painter;return e.getRenderedCanvas({backgroundColor:t.backgroundColor||this._model.get("backgroundColor"),pixelRatio:t.pixelRatio||this.getDevicePixelRatio()})},e.prototype.renderToSVGString=function(t){t=t||{};var e=this._zr.painter;return e.renderToString({useViewBox:t.useViewBox})},e.prototype.getSvgDataURL=function(){var t=this._zr;return B(t.storage.getDisplayList(),function(t){t.stopAnimation(null,!0)}),t.painter.toDataURL()},e.prototype.getDataURL=function(t){if(!this._disposed){var e=(t=t||{}).excludeComponents,n=this._model,i=[],r=this;B(e,function(t){n.eachComponent({mainType:t},function(t){var e=r._componentsMap[t.__viewId];e.group.ignore||(i.push(e),e.group.ignore=!0)})});var o="svg"===this._zr.painter.getType()?this.getSvgDataURL():this.renderToCanvas(t).toDataURL("image/"+(t&&t.type||"png"));return B(i,function(t){t.group.ignore=!1}),o}R_(this.id)},e.prototype.getConnectedDataURL=function(t){if(!this._disposed){var e="svg"===t.type,n=this.group,i=Math.min,r=Math.max,o=1/0;if(U_[n]){var a=o,s=o,l=-1/0,u=-1/0,h=[],d=t&&t.pixelRatio||this.getDevicePixelRatio();B(W_,function(o,c){if(o.group===n){var d=e?o.getZr().painter.getSvgDom().innerHTML:o.renderToCanvas(C(t)),p=o.getDom().getBoundingClientRect();a=i(p.left,a),s=i(p.top,s),l=r(p.right,l),u=r(p.bottom,u),h.push({dom:d,left:p.left,top:p.top})}});var p=(l*=d)-(a*=d),f=(u*=d)-(s*=d),g=c.createCanvas(),v=bo(g,{renderer:e?"svg":"canvas"});if(v.resize({width:p,height:f}),e){var y="";return B(h,function(t){var e=t.left-a,n=t.top-s;y+=''+t.dom+""}),v.painter.getSvgRoot().innerHTML=y,t.connectedBackgroundColor&&v.painter.setBackgroundColor(t.connectedBackgroundColor),v.refreshImmediately(),v.painter.toDataURL()}return t.connectedBackgroundColor&&v.add(new lu({shape:{x:0,y:0,width:p,height:f},style:{fill:t.connectedBackgroundColor}})),B(h,function(t){var e=new eu({style:{x:t.left*d-a,y:t.top*d-s,image:t.dom}});v.add(e)}),v.refreshImmediately(),g.toDataURL("image/"+(t&&t.type||"png"))}return this.getDataURL(t)}R_(this.id)},e.prototype.convertToPixel=function(t,e,n){return f_(this,"convertToPixel",t,e,n)},e.prototype.convertToLayout=function(t,e,n){return f_(this,"convertToLayout",t,e,n)},e.prototype.convertFromPixel=function(t,e,n){return f_(this,"convertFromPixel",t,e,n)},e.prototype.containPixel=function(t,e){var n;if(!this._disposed)return B(Ra(this._model,t),function(t,i){i.indexOf("Models")>=0&&B(t,function(t){var r=t.coordinateSystem;if(r&&r.containPoint)n=n||!!r.containPoint(e);else if("seriesModels"===i){var o=this._chartsMap[t.__viewId];o&&o.containPoint&&(n=n||o.containPoint(e,t))}else 0},this)},this),!!n;R_(this.id)},e.prototype.getVisual=function(t,e){var n=Ra(this._model,t,{defaultMainType:"series"}),i=n.seriesModel;var r=i.getData(),o=n.hasOwnProperty("dataIndexInside")?n.dataIndexInside:n.hasOwnProperty("dataIndex")?r.indexOfRawIndex(n.dataIndex):null;return null!=o?jm(r,o,e):qm(r,e)},e.prototype.getViewOfComponentModel=function(t){return this._componentsMap[t.__viewId]},e.prototype.getViewOfSeriesModel=function(t){return this._chartsMap[t.__viewId]},e.prototype._initEvents=function(){var t=this;B(O_,function(e){var n=function(n){var i,r=t.getModel(),o=n.target,a="globalout"===e;if(a?i={}:o&&Qm(o,function(t){var e=Iu(t);if(e&&null!=e.dataIndex){var n=e.dataModel||r.getSeriesByIndex(e.seriesIndex);return i=n&&n.getDataParams(e.dataIndex,e.dataType,o)||{},!0}if(e.eventData)return i=k({},e.eventData),!0},!0),i){var s=i.componentType,l=i.componentIndex;"markLine"!==s&&"markPoint"!==s&&"markArea"!==s||(s="series",l=i.seriesIndex);var u=s&&null!=l&&r.getComponent(s,l),c=u&&t["series"===u.mainType?"_chartsMap":"_componentsMap"][u.__viewId];0,i.event=n,i.type=e,t._$eventProcessor.eventInfo={targetEl:o,packedEvent:i,model:u,view:c},t.trigger(e,i)}};n.zrEventfulCallAtLast=!0,t._zr.on(e,n,t)});var e=this._messageCenter;B(z_,function(n,i){e.on(i,function(e){t.trigger(i,e)})}),function(t,e,n){t.on("selectchanged",function(t){var i=n.getModel();t.isFromClick?(Jm("map","selectchanged",e,i,t),Jm("pie","selectchanged",e,i,t)):"select"===t.fromAction?(Jm("map","selected",e,i,t),Jm("pie","selected",e,i,t)):"unselect"===t.fromAction&&(Jm("map","unselected",e,i,t),Jm("pie","unselected",e,i,t))})}(e,this,this._api)},e.prototype.isDisposed=function(){return this._disposed},e.prototype.clear=function(){this._disposed?R_(this.id):this.setOption({series:[]},!0)},e.prototype.dispose=function(){if(this._disposed)R_(this.id);else{this._disposed=!0,this.getDom()&&Ga(this.getDom(),Z_,"");var t=this,e=t._api,n=t._model;B(t._componentsViews,function(t){t.dispose(n,e)}),B(t._chartsViews,function(t){t.dispose(n,e)}),t._zr.dispose(),t._dom=t._model=t._chartsMap=t._componentsMap=t._chartsViews=t._componentsViews=t._scheduler=t._api=t._zr=t._throttledZrFlush=t._theme=t._coordSysMgr=t._messageCenter=null,delete W_[t.id]}},e.prototype.resize=function(t){if(!this[e_])if(this._disposed)R_(this.id);else{this._zr.resize(t);var e=this._model;if(this._loadingFX&&this._loadingFX.resize(),e){var n=e.resetOption("media"),i=t&&t.silent;this[i_]&&(null==i&&(i=this[i_].silent),n=!0,this[i_]=null),this[e_]=!0,D_(this);try{n&&c_(this),p_.update.call(this,{type:"resize",animation:k({duration:0},t&&t.animation)})}catch(t){throw this[e_]=!1,t}this[e_]=!1,y_.call(this,i),m_.call(this,i)}}},e.prototype.showLoading=function(t,e){if(this._disposed)R_(this.id);else if($(t)&&(e=t,t=""),t=t||"default",this.hideLoading(),H_[t]){var n=H_[t](this._api,e),i=this._zr;this._loadingFX=n,i.add(n)}},e.prototype.hideLoading=function(){this._disposed?R_(this.id):(this._loadingFX&&this._zr.remove(this._loadingFX),this._loadingFX=null)},e.prototype.makeActionFromEvent=function(t){var e=k({},t);return e.type=E_[t.type],e},e.prototype.dispatchAction=function(t,e){if(this._disposed)R_(this.id);else if($(e)||(e={silent:!!e}),N_[t.type]&&this._model)if(this[e_])this._pendingActions.push(t);else{var n=e.silent;v_.call(this,t,n);var i=e.flush;i?this._zr.flush():!1!==i&&r.browser.weChat&&this._throttledZrFlush(),y_.call(this,n),m_.call(this,n)}},e.prototype.updateLabelLayout=function(){tx.trigger("series:layoutlabels",this._model,this._api,{updatedSeries:[]})},e.prototype.appendData=function(t){if(this._disposed)R_(this.id);else{var e=t.seriesIndex,n=this.getModel().getSeriesByIndex(e);0,n.appendData(t),this._scheduler.unfinished=!0,this.getZr().wakeUp()}},e.internalField=function(){function t(t){t.clearColorPalette(),t.eachSeries(function(t){t.clearColorPalette()})}function e(t){for(var e=[],n=t.currentStates,i=0;i0?{duration:o,delay:i.get("delay"),easing:i.get("easing")}:null;n.eachRendered(function(t){if(t.states&&t.states.emphasis){if(ad(t))return;if(t instanceof Kl&&function(t){var e=Uu(t);e.normalFill=t.style.fill,e.normalStroke=t.style.stroke;var n=t.states.select||{};e.selectFill=n.style&&n.style.fill||null,e.selectStroke=n.style&&n.style.stroke||null}(t),t.__dirty){var n=t.prevStates;n&&t.useStates(n)}if(r){t.stateTransition=a;var i=t.getTextContent(),o=t.getTextGuideLine();i&&(i.stateTransition=a),o&&(o.stateTransition=a)}t.__dirty&&e(t)}})}c_=function(t){var e;e=t._model,ox(e).prepare={};var n=t._scheduler;n.restorePipelines(t._zr,t._model),n.prepareStageTasks(),h_(t,!0),h_(t,!1),n.plan()},h_=function(t,e){for(var n=t._model,i=t._scheduler,r=e?t._componentsViews:t._chartsViews,o=e?t._componentsMap:t._chartsMap,a=t._zr,s=t._api,l=0;lat(e.get("hoverLayerThreshold"),yg.hoverLayerThreshold)&&!r.node&&!r.worker;(t._usingTHL||a)&&(e.eachSeries(function(e){if(!e.preventUsingHoverLayer){var n=t._chartsMap[e.__viewId];n.__alive&&n.eachRendered(function(t){var e=t.states.emphasis;e&&2!==e.hoverLayer&&(e.hoverLayer=a?1:0)})}}),t._usingTHL=a)}(t,e),tx.trigger("series:afterupdate",e,n,l)},T_=function(t){t[r_]=!0,t.getZr().wakeUp()},D_=function(t){t[n_]=(t[n_]+1)%1e6},C_=function(t){t[r_]&&(t.getZr().storage.traverse(function(t){ad(t)||e(t)}),t[r_]=!1)},M_=function(t){return new(function(e){function i(){return null!==e&&e.apply(this,arguments)||this}return n(i,e),i.prototype.getCoordinateSystems=function(){return t._coordSysMgr.getCoordinateSystems()},i.prototype.getComponentByElement=function(e){for(;e;){var n=e.__ecComponentInfo;if(null!=n)return t._model.getComponent(n.mainType,n.index);e=e.parent}},i.prototype.enterEmphasis=function(e,n){vc(e,n),T_(t)},i.prototype.leaveEmphasis=function(e,n){yc(e,n),T_(t)},i.prototype.enterBlur=function(e){mc(e),T_(t)},i.prototype.leaveBlur=function(e){xc(e),T_(t)},i.prototype.enterSelect=function(e){_c(e),T_(t)},i.prototype.leaveSelect=function(e){bc(e),T_(t)},i.prototype.getModel=function(){return t.getModel()},i.prototype.getViewOfComponentModel=function(e){return t.getViewOfComponentModel(e)},i.prototype.getViewOfSeriesModel=function(e){return t.getViewOfSeriesModel(e)},i.prototype.getECUpdateCycleVersion=function(){return t[n_]},i.prototype.usingTHL=function(){return t._usingTHL},i}(Gu))(t)},I_=function(t){function e(t,e){for(var n=0;n=0)){sb.push(n);var a=wm.wrapStageHandler(n,r);a.__prio=e,a.__raw=n,t.push(a)}}function ub(t,e){H_[t]=e}function cb(t,e,n){var i=nx("registerMap");i&&i(t,e,n)}var hb=function(t){var e=(t=C(t)).type,n="";e||va(n);var i=e.split(":");2!==i.length&&va(n);var r=!1;"echarts"===i[0]&&(e=i[1],r=!0),t.__isBuiltIn=r,oy.set(e,t)};function db(t,e,n,i){return{eventContent:{selected:Dc(n),isFromClick:e.isFromClick||!1}}}ab(Jx,ym),ab(Qx,xm),ab(Qx,_m),ab(Jx,Xm),ab(Qx,Zm),ab(7e3,$x),J_(uv),Q_(900,cv),ub("default",function(t,e){P(e=e||{},{text:"loading",textColor:ug.color.primary,fontSize:12,fontWeight:"normal",fontStyle:"normal",fontFamily:"sans-serif",maskColor:"rgba(255,255,255,0.8)",showSpinner:!0,color:ug.color.theme[0],spinnerRadius:10,lineWidth:5,zlevel:0});var n=new vo,i=new lu({style:{fill:e.maskColor},zlevel:e.zlevel,z:1e4});n.add(i);var r,o=new du({style:{text:e.text,fill:e.textColor,fontSize:e.fontSize,fontWeight:e.fontWeight,fontStyle:e.fontStyle,fontFamily:e.fontFamily},zlevel:e.zlevel,z:10001}),a=new lu({style:{fill:"none"},textContent:o,textConfig:{position:"right",distance:10},zlevel:e.zlevel,z:10001});return n.add(a),e.showSpinner&&((r=new Vh({shape:{startAngle:-bm/2,endAngle:-bm/2+.1,r:e.spinnerRadius},style:{stroke:e.color,lineCap:"round",lineWidth:e.lineWidth},zlevel:e.zlevel,z:10001})).animateShape(!0).when(1e3,{endAngle:3*bm/2}).start("circularInOut"),r.animateShape(!0).when(1e3,{startAngle:3*bm/2}).delay(300).start("circularInOut"),n.add(r)),n.resize=function(){var n=o.getBoundingRect().width,s=e.showSpinner?e.spinnerRadius:0,l=(t.getWidth()-2*s-(e.showSpinner&&n?10:0)-n)/2-(e.showSpinner&&n?0:5+n/2)+(e.showSpinner?0:n/2)+(n?0:s),u=t.getHeight()/2;e.showSpinner&&r.setShape({cx:l,cy:u}),a.setShape({x:l-s,y:u-s,width:2*s,height:2*s}),i.setShape({x:0,y:0,width:t.getWidth(),height:t.getHeight()})},n.resize(),n}),ib({type:qu,event:qu,update:qu},St),ib({type:Ku,event:Ku,update:Ku},St),ib({type:$u,event:tc,update:$u,action:St,refineEvent:db,publishNonRefinedEvent:!0}),ib({type:Ju,event:tc,update:Ju,action:St,refineEvent:db,publishNonRefinedEvent:!0}),ib({type:Qu,event:tc,update:Qu,action:St,refineEvent:db,publishNonRefinedEvent:!0}),$_("default",{}),$_("dark",Hm);var pb=[],fb={registerPreprocessor:J_,registerProcessor:Q_,registerPostInit:tb,registerPostUpdate:eb,registerUpdateLifecycle:nb,registerAction:ib,registerCoordinateSystem:rb,registerLayout:ob,registerVisual:ab,registerTransform:hb,registerLoading:ub,registerMap:cb,registerImpl:function(t,e){ex[t]=e},PRIORITY:t_,ComponentModel:lg,ComponentView:Ky,SeriesModel:Hy,ChartView:tm,registerComponentModel:function(t){lg.registerClass(t)},registerComponentView:function(t){Ky.registerClass(t)},registerSeriesModel:function(t){Hy.registerClass(t)},registerChartView:function(t){tm.registerClass(t)},registerCustomSeries:function(t,e){rx(t,e)},registerSubTypeDefaulter:function(t,e){lg.registerSubTypeDefaulter(t,e)},registerPainter:function(t,e){wo(t,e)}};function gb(t){X(t)?B(t,function(t){gb(t)}):R(pb,t)>=0||(pb.push(t),Z(t)&&(t={install:t}),t.install(fb))}function vb(t){return null==t?0:t.length||1}function yb(t){return t}var mb=function(){function t(t,e,n,i,r,o){this._old=t,this._new=e,this._oldKeyGetter=n||yb,this._newKeyGetter=i||yb,this.context=r,this._diffModeMultiple="multiple"===o}return t.prototype.add=function(t){return this._add=t,this},t.prototype.update=function(t){return this._update=t,this},t.prototype.updateManyToOne=function(t){return this._updateManyToOne=t,this},t.prototype.updateOneToMany=function(t){return this._updateOneToMany=t,this},t.prototype.updateManyToMany=function(t){return this._updateManyToMany=t,this},t.prototype.remove=function(t){return this._remove=t,this},t.prototype.execute=function(){this[this._diffModeMultiple?"_executeMultiple":"_executeOneToOne"]()},t.prototype._executeOneToOne=function(){var t=this._old,e=this._new,n={},i=new Array(t.length),r=new Array(e.length);this._initIndexMap(t,null,i,"_oldKeyGetter"),this._initIndexMap(e,n,r,"_newKeyGetter");for(var o=0;o1){var u=s.shift();1===s.length&&(n[a]=s[0]),this._update&&this._update(u,o)}else 1===l?(n[a]=null,this._update&&this._update(s,o)):this._remove&&this._remove(o)}this._performRestAdd(r,n)},t.prototype._executeMultiple=function(){var t=this._old,e=this._new,n={},i={},r=[],o=[];this._initIndexMap(t,n,r,"_oldKeyGetter"),this._initIndexMap(e,i,o,"_newKeyGetter");for(var a=0;a1&&1===h)this._updateManyToOne&&this._updateManyToOne(u,l),i[s]=null;else if(1===c&&h>1)this._updateOneToMany&&this._updateOneToMany(u,l),i[s]=null;else if(1===c&&1===h)this._update&&this._update(u,l),i[s]=null;else if(c>1&&h>1)this._updateManyToMany&&this._updateManyToMany(u,l),i[s]=null;else if(c>1)for(var d=0;d1)for(var a=0;a30}var kb,Lb,Pb,Ob,Rb,Nb,Eb,zb=$,Bb=V,Vb="undefined"==typeof Int32Array?Array:Int32Array,Gb=["hasItemOption","_nameList","_idList","_invertedIndicesMap","_dimSummary","userOutput","_rawData","_dimValueGetter","_nameDimIdx","_idDimIdx","_nameRepeatCount"],Fb=["_approximateExtent"],Hb=function(){function t(t,e){var n;this.type="list",this._dimOmitted=!1,this._nameList=[],this._idList=[],this._visual={},this._layout={},this._itemVisuals=[],this._itemLayouts=[],this._graphicEls=[],this._approximateExtent={},this._calculationInfo={},this.hasItemOption=!1,this.TRANSFERABLE_METHODS=["cloneShallow","downSample","minmaxDownSample","lttbDownSample","map"],this.CHANGABLE_METHODS=["filterSelf","selectRange"],this.DOWNSAMPLE_METHODS=["downSample","minmaxDownSample","lttbDownSample"];var i=!1;Tb(t)?(n=t.dimensions,this._dimOmitted=t.isDimensionOmitted(),this._schema=t):(i=!0,n=t),n=n||["x","y"];for(var r={},o=[],a={},s=!1,l={},u=0;u=e)){var n=this._store.getProvider();this._updateOrdinalMeta();var i=this._nameList,r=this._idList;if(n.getSource().sourceFormat===ku&&!n.pure)for(var o=[],a=t;a0},t.prototype.ensureUniqueItemVisual=function(t,e){var n=this._itemVisuals,i=n[t];i||(i=n[t]={});var r=i[e];return null==r&&(X(r=this.getVisual(e))?r=r.slice():zb(r)&&(r=k({},r)),i[e]=r),r},t.prototype.setItemVisual=function(t,e,n){var i=this._itemVisuals[t]||{};this._itemVisuals[t]=i,zb(e)?k(i,e):i[e]=n},t.prototype.clearAllVisual=function(){this._visual={},this._itemVisuals=[]},t.prototype.setLayout=function(t,e){zb(t)?k(this._layout,t):this._layout[t]=e},t.prototype.getLayout=function(t){return this._layout[t]},t.prototype.getItemLayout=function(t){return this._itemLayouts[t]},t.prototype.setItemLayout=function(t,e,n){this._itemLayouts[t]=n?k(this._itemLayouts[t]||{},e):e},t.prototype.clearItemLayouts=function(){this._itemLayouts.length=0},t.prototype.setItemGraphicEl=function(t,e){var n=this.hostModel&&this.hostModel.seriesIndex;Tu(n,this.dataType,t,e),this._graphicEls[t]=e},t.prototype.getItemGraphicEl=function(t){return this._graphicEls[t]},t.prototype.eachItemGraphicEl=function(t,e){B(this._graphicEls,function(n,i){n&&t&&t.call(e,n,i)})},t.prototype.cloneShallow=function(e){return e||(e=new t(this._schema?this._schema:Bb(this.dimensions,this._getDimInfo,this),this.hostModel)),Rb(e,this),e._store=this._store,e},t.prototype.wrapMethod=function(t,e){var n=this[t];Z(n)&&(this.__wrappedMethods=this.__wrappedMethods||[],this.__wrappedMethods.push(t),this[t]=function(){var t=n.apply(this,arguments);return e.apply(this,[t].concat(lt(arguments)))})},t.internalField=(kb=function(t){var e=t._invertedIndicesMap;B(e,function(n,i){var r=t._dimInfos[i],o=r.ordinalMeta,a=t._store;if(o){n=e[i]=new Vb(o.categories.length);for(var s=0;s1&&(s+="__ec__"+u),i[e]=s}})),t}();function Wb(t,e){mv(t)||(t=_v(t));var n=(e=e||{}).coordDimensions||[],i=e.dimensionsDefine||t.dimensionsDefine||[],r=mt(),o=[],a=function(t,e,n,i){var r=Math.max(t.dimensionsDetectedCount||1,e.length,n.length,i||0);return B(e,function(t){var e;$(t)&&(e=t.dimsDef)&&(r=Math.max(r,e.length))}),r}(t,n,i,e.dimensionsCount),s=e.canOmitUnusedDimensions&&Ab(a),l=i===t.dimensionsDefine,u=l?Db(t):Cb(i),c=e.encodeDefine;!c&&e.encodeDefaulter&&(c=e.encodeDefaulter(t,a));for(var h=mt(c),d=new hy(a),p=0;p0&&(t.name=t.name+(e-1))}),new Ib({source:t,dimensions:o,fullDimensionCount:a,dimensionOmitted:s})}function Ub(t,e,n){if(n||e.hasKey(t)){for(var i=0;e.hasKey(t+i);)i++;t+=i}return e.set(t,!0),t}var Yb=function(t){this.coordSysDims=[],this.axisMap=mt(),this.categoryAxisMap=mt(),this.coordSysName=t};var Xb={cartesian2d:function(t,e,n,i){var r=t.getReferringComponents("xAxis",Ea).models[0],o=t.getReferringComponents("yAxis",Ea).models[0];e.coordSysDims=["x","y"],n.set("x",r),n.set("y",o),Zb(r)&&(i.set("x",r),e.firstCategoryDimIndex=0),Zb(o)&&(i.set("y",o),null==e.firstCategoryDimIndex&&(e.firstCategoryDimIndex=1))},singleAxis:function(t,e,n,i){var r=t.getReferringComponents("singleAxis",Ea).models[0];e.coordSysDims=["single"],n.set("single",r),Zb(r)&&(i.set("single",r),e.firstCategoryDimIndex=0)},polar:function(t,e,n,i){var r=t.getReferringComponents("polar",Ea).models[0],o=r.findAxisModel("radiusAxis"),a=r.findAxisModel("angleAxis");e.coordSysDims=["radius","angle"],n.set("radius",o),n.set("angle",a),Zb(o)&&(i.set("radius",o),e.firstCategoryDimIndex=0),Zb(a)&&(i.set("angle",a),null==e.firstCategoryDimIndex&&(e.firstCategoryDimIndex=1))},geo:function(t,e,n,i){e.coordSysDims=["lng","lat"]},parallel:function(t,e,n,i){var r=t.ecModel,o=r.getComponent("parallel",t.get("parallelIndex")),a=e.coordSysDims=o.dimensions.slice();B(o.parallelAxisIndex,function(t,o){var s=r.getComponent("parallelAxis",t),l=a[o];n.set(l,s),Zb(s)&&(i.set(l,s),null==e.firstCategoryDimIndex&&(e.firstCategoryDimIndex=o))})},matrix:function(t,e,n,i){var r=t.getReferringComponents("matrix",Ea).models[0];e.coordSysDims=["x","y"];var o=r.getDimensionModel("x"),a=r.getDimensionModel("y");n.set("x",o),n.set("y",a),i.set("x",o),i.set("y",a)}};function Zb(t){return"category"===t.get("type")}function jb(t,e,n){var i,r,o,a=(n=n||{}).byIndex,s=n.stackedCoordDimension;!function(t){return!Tb(t.schema)}(e)?(r=e.schema,i=r.dimensions,o=e.store):i=e;var l,u,c,h,d=!(!t||!t.get("stack")),p=!0;function f(t){return"ordinal"!==t.type&&"time"!==t.type}if(B(i,function(t,e){j(t)&&(i[e]=t={name:t}),f(t)||(p=!1)}),B(i,function(t,e){d&&!t.isExtraCoord&&(a||l||!t.ordinalMeta||(l=t),u||!f(t)||p&&("x"===t.coordDim||"angle"===t.coordDim)||s&&s!==t.coordDim||(u=t))}),!u||a||l||(a=!0),u){c="__\0ecstackresult_"+t.id,h="__\0ecstackedover_"+t.id,l&&(l.createInvertedIndices=!0);var g=u.coordDim,v=u.type,y=0;B(i,function(t){t.coordDim===g&&y++});var m={name:c,coordDim:g,coordDimIndex:y,type:v,isExtraCoord:!0,isCalculationCoord:!0,storeDimIndex:i.length},x={name:h,coordDim:h,coordDimIndex:y+1,type:v,isExtraCoord:!0,isCalculationCoord:!0,storeDimIndex:i.length+1};r?(o&&(m.storeDimIndex=o.ensureCalculationDimension(h,v),x.storeDimIndex=o.ensureCalculationDimension(c,v)),r.appendCalculationDimension(m),r.appendCalculationDimension(x)):(i.push(m),i.push(x))}return{stackedDimension:u&&u.name,stackedByDimension:l&&l.name,isStackedByIndex:a,stackedOverDimension:h,stackResultDimension:c}}function qb(t,e){return!!e&&e===t.getCalculationInfo("stackedDimension")}function Kb(t,e){return qb(t,e)?t.getCalculationInfo("stackResultDimension"):e}function $b(t,e,n){n=n||{};var i,r=e.getSourceManager(),o=!1;t?(o=!0,i=_v(t)):o=(i=r.getSource()).sourceFormat===ku;var a=function(t){var e=t.get("coordinateSystem"),n=new Yb(e),i=Xb[e];if(i)return i(t,n,n.axisMap,n.categoryAxisMap),n}(e),s=function(t,e){var n,i=t.get("coordinateSystem"),r=Vf.get(i);return e&&e.coordSysDims&&(n=V(e.coordSysDims,function(t){var n={name:t},i=e.axisMap.get(t);if(i){var r=i.get("type");n.type=bb(r)}return n})),n||(n=r&&(r.getDimensionsInfo?r.getDimensionsInfo():r.dimensions.slice())||["x","y"]),n}(e,a),l=n.useEncodeDefaulter,u=Z(l)?l:l?Y(wg,s,e):null,c=Wb(i,{coordDimensions:s,generateCoord:n.generateCoord,encodeDefine:e.getEncode(),encodeDefaulter:u,canOmitUnusedDimensions:!o}),h=function(t,e,n){var i,r;return n&&B(t,function(t,o){var a=t.coordDim,s=n.categoryAxisMap.get(a);s&&(null==i&&(i=o),t.ordinalMeta=s.getOrdinalMeta(),e&&(t.createInvertedIndices=!0)),null!=t.otherDims.itemName&&(r=!0)}),r||null==i||(t[i].otherDims.itemName=0),i}(c.dimensions,n.createInvertedIndices,a),d=o?null:r.getSharedDataStore(c),p=jb(e,{schema:c,store:d}),f=new Hb(c,e);f.setCalculationInfo(p);var g=null!=h&&function(t){if(t.sourceFormat===ku){var e=function(t){var e=0;for(;e=e[0]&&t<=e[1]},getExtent:function(){return this._extents[0].slice()},getExtentUnsafe:function(t){return this._extents[t]},setExtent:function(t,e){hw(this._extents,0,t,e)},setExtent2:function(t,e,n){var i=this._extents;i[t]||(i[t]=i[0].slice()),hw(i,t,e,n)},freeze:function(){0}};function hw(t,e,n,i){Ka(n,i)&&(t[e][0]=n,t[e][1]=i)}function dw(t){return pw(t)||gw(t)}function pw(t){return"interval"===t.type}function fw(t){return"time"===t.type}function gw(t){return"log"===t.type}function vw(t){return"ordinal"===t.type}function yw(t){var e=ea(t),n=Oo(10,e),i=ko(t/n);return i?2===i?i=3:3===i?i=5:i*=2:i=1,Ho(i*n,-e)}function mw(t){return Uo(t)+2}function xw(t,e){return Ro(t)/Ro(e)}function _w(t,e,n){var i=n&&n.lookup;if(i)for(var r=0;r1&&o/a>2&&(r=Math.round(Math.ceil(r/a)*a)),r!==i[0]&&l(i[0],!0,!0);for(var s=r;s<=i[1];s+=a)l(s,!1,s===i[0]||s===i[1]);function l(t,e,i){n({value:t,offInterval:e},i)}s-a!==i[1]&&l(i[1],!0,!0)}var Mw=function(t){function e(n){var i=t.call(this)||this;i.type="ordinal",i.parse=e.parse,rw(i,e.decoratedMethods);var r=n.ordinalMeta;r||(r=new tw({})),X(r)&&(r=new tw({categories:V(r,function(t){return $(t)?t.value:t})})),i._ordinalMeta=r;var o=iw(null,null,n.extent||[0,r.categories.length-1]);return i._mapper=o.mapper,ow(i,o.mapper),i}return n(e,t),e.parse=function(t){return null==t?t=NaN:j(t)?null==(t=this._ordinalMeta.getOrdinal(t))&&(t=NaN):t=ko(t),t},e.prototype.getTicks=function(){var t=[];return Sw(this,0,function(e){t.push(e)}),t},e.prototype.getMinorTicks=function(t){},e.prototype.setSortInfo=function(t){if(null!=t){for(var e=t.ordinalNumbers,n=this._ordinalNumbersByTick=[],i=this._ticksByOrdinalNumber=[],r=0,o=this._ordinalMeta.categories.length,a=Co(o,e.length);r=0&&t=0&&t=0&&ta[0]&&fr[1]||!isFinite(d)||!isFinite(r[1]))break}else{if(p>h)break;d=Co(d,r[1]),p===h&&(d=r[1])}if(u.push({value:d}),d=Ho(d+n,o),s){var f=s.calcNiceTickMultiple(d,c);f>=0&&(d=Ho(d+f*n,o))}if(u.length>0&&d===u[u.length-1].value)break;if(u.length>3e3)return[]}var g=u.length?u[u.length-1].value:r[1];return i[1]>g&&u.push({value:t.expandToNicedExtent?Ho(g+n,o):i[1]}),l&&a.pruneTicksByBreak(t.pruneByBreak,u,s.breaks,function(t){return t.value},e.interval,i),l&&"none"!==t.breakTicks&&a.addBreaksToTicks(u,s.breaks,i),u},e.prototype.getMinorTicks=function(t){return Iw(this,t,Yp(this),this._cfg.interval)},e.prototype.getLabel=function(t,e){if(null==t)return"";var n=e&&e.precision;return null==n?n=Uo(t.value)||0:"auto"===n&&(n=this._cfg.intervalPrecision),Tf(Ho(t.value,n,!0))},e.type="interval",e}(Jb);Jb.registerClass(Tw);var Cw=function(t){function e(n){var i=t.call(this)||this;i.type="time",i.parse=e.parse,i._locale=n.locale,i._useUTC=n.useUTC,i._interval=0;var r=iw(i,Up(i,n),null);return i.brk=r.brk,i}return n(e,t),e.prototype.getLabel=function(t){return uf(t.value,ef[function(t){switch(t){case"year":case"month":return"day";case"millisecond":return"millisecond";default:return"second"}}(sf(this._minLevelUnit))]||ef.second,this._useUTC,this._locale)},e.prototype.getFormattedLabel=function(t,e,n){return function(t,e,n,i,r){var o=null;if(j(n))o=n;else if(Z(n)){var a={time:t.time,level:t.time?t.time.level:0},s=Wp();s&&s.makeAxisLabelFormatterParamBreak(a,t.break),o=n(t.value,e,a)}else{var l=t.time;if(l){var u=n[l.lowerTimeUnit][l.upperTimeUnit];o=u[Math.min(l.level,u.length-1)]||""}else{var c=cf(t.value,r);o=n[c][c][0]}}return uf(new Date(t.value),o,r,i)}(t,e,n,this._locale,this._useUTC)},e.prototype.getTicks=function(t){t=t||{};var e=this._interval,n=aw(this),i=Wp(),r=this.brk,o=i&&r,a=[];if(!e)return a;var s=this._useUTC;if(o&&"only_break"===t.breakTicks)return Wp().addBreaksToTicks(a,r.breaks,n),a;a=function(t,e,n,i,r,o){var a=3e3,s=rf,l=0;function u(t,e,n,r,s,u,c){for(var h=function(t,e){var n=new Date(0);n[t](1);var i=n.getTime();n[t](1+e);var r=n.getTime()-i;return function(t,e){return Math.max(0,Math.round((e-t)/r))}}(s,t),d=e,p=new Date(d);da){0;break}if(p[s](p[r]()+t),d=p.getTime(),o){var f=o.calcNiceTickMultiple(d,h);f>0&&(p[s](p[r]()+f*t),d=p.getTime())}}c.push({value:d,notAdd:d>i[1]})}function c(t,r,o){var a=[],s=!r.length;if(!Aw(sf(t),i[0],i[1],n)){s&&(r=[{value:Nw(i[0],t,n)},{value:i[1]}]);for(var l=0;l=i[0]&&c<=i[1]&&u(d,c,h,p,f,g,a),"year"===t&&o.length>1&&0===l&&o.unshift({value:o[0].value-d})}}for(l=0;l=i[0]&&x<=i[1]&&p++)}var _=r/e;if(p>1.5*_&&f>_/1.5)break;if(h.push(y),p>_||t===s[g])break}d=[]}}var b=F(V(h,function(t){return F(t,function(t){return t.value>=i[0]&&t.value<=i[1]&&!t.notAdd})}),function(t){return t.length>0}),w=b.length-1,S=[];for(g=0;gi[0])&&S.unshift({value:i[0],time:{level:0,upperTimeUnit:A,lowerTimeUnit:A},notNice:!0});(!D||D.value16?16:t>7.5?7:t>3.5?4:t>1.5?2:1}function Lw(t){return(t/=2592e6)>6?6:t>3?3:t>2?2:1}function Pw(t){return(t/=qp)>12?12:t>6?6:t>3.5?4:t>2?2:1}function Ow(t,e){return(t/=e?jp:Zp)>30?30:t>20?20:t>15?15:t>10?10:t>5?5:t>2?2:1}function Rw(t){return Do(na(t,!0),1)}function Nw(t,e,n){var i=Math.max(0,R(nf,e)-1);return hf(new Date(t),nf[i],n).getTime()}Jb.registerClass(Cw);var Ew=function(t){function e(n){var i=t.call(this)||this;i.type="log",i.parse=Tw.parse,i.base=n.logBase||10;var r=[],o=[],a=i._lookup={from:r,to:o};r[0]=r[1]=o[0]=o[1]=NaN,rw(i,e.mapperMethods);var s=Wp(),l=n.breakOption,u={lookup:a};return s&&s.parseAxisBreakOptionInwardTransform(l,i,{noNegative:!0},2,u),i.powStub=new Tw({breakParsed:u.original}),i.intervalStub=new Tw({breakParsed:u.transformed}),ow(i,i.intervalStub),i}return n(e,t),e.prototype.getTicks=function(t){var e=this.base,n=this.powStub,i=Wp(),r=this.intervalStub,o={lookup:{from:r.getExtent(),to:n.getExtent()}};return V(r.getTicks(t||{}),function(t){var r,a=_w(t.value,e,o);if(i){var s=i.getTicksBreakOutwardTransform(this,t,Yp(n),this._lookup);s&&(r=s.vBreak,a=s.tickVal)}return{value:a,break:r}},this)},e.prototype.getMinorTicks=function(t){return Iw(this,t,Yp(this.powStub),this.intervalStub.getConfig().interval)},e.prototype.getLabel=function(t,e){return this.intervalStub.getLabel(t,e)},e.type="log",e.mapperMethods={needTransform:function(){return!0},normalize:function(t){return this.intervalStub.normalize(xw(t,this.base))},scale:function(t){return _w(this.intervalStub.scale(t),this.base,null)},transformIn:function(t,e){return t=xw(t,this.base),e&&2===e.depth?t:this.intervalStub.transformIn(t,e)},transformOut:function(t,e){var n=e?e.depth:null;return zw.depth=n,Bw.lookup=this._lookup,_w(2===n?t:this.intervalStub.transformOut(t,zw),this.base,Bw)},contain:function(t){return this.powStub.contain(t)},setExtent:function(t,e){this.setExtent2(0,t,e)},setExtent2:function(t,e,n){if(!(!Ka(e,n)||e<=0||n<=0)){var i=Vw,r=Vw;if(0===t){var o=this._lookup;i=o.to,r=o.from}this.powStub.setExtent2(t,i[0]=e,i[1]=n);var a=this.base;this.intervalStub.setExtent2(t,r[0]=xw(e,a),r[1]=xw(n,a))}},getFilter:function(){return{g:0}},sanitize:function(t,e){return Ka(e[0],e[1])&&ca(t)&&t<=0&&(t=e[0]),t},getDefaultStartValue:function(){return 1},getExtent:function(){return this.powStub.getExtent()},getExtentUnsafe:function(t,e){return null===e?this.powStub.getExtentUnsafe(t,null):this.intervalStub.getExtentUnsafe(t,e)}},e}(Jb);Jb.registerClass(Ew);var zw={},Bw={},Vw=[],Gw={value:1,category:1,time:1,log:1},Fw=Pa();function Hw(t){var e=t.get("type");return null!=e&&(wt(Gw,e)||Jb.getClass(e))||(e="value"),e}function Ww(t,e,n){var i;switch(Wp()&&(i=Jw(t,e,n)),e){case"category":return new Mw({ordinalMeta:t.getOrdinalMeta?t.getOrdinalMeta():t.getCategories(),extent:[1/0,-1/0]});case"time":return new Cw({locale:t.ecModel.getLocaleModel(),useUTC:t.ecModel.get("useUTC"),breakOption:i});case"log":return new Ew({logBase:t.get("logBase"),breakOption:i});case"value":return new Tw({breakOption:i});default:return new(Jb.getClass(e)||Tw)({})}}var Uw=1,Yw=2,Xw=3;function Zw(t){var e=t.getLabelModel().get("formatter");if("time"===t.type){var n=of(e);return function(e,i){return t.scale.getFormattedLabel(e,i,n)}}if(j(e))return function(n){var i=t.scale.getLabel(n);return e.replace("{value}",null!=i?i:"")};if(Z(e)){if("category"===t.type)return function(n,i){return e(jw(t,n),n.value-t.scale.getExtent()[0],null)};var i=Wp();return function(n,r){var o=null;return i&&(o=i.makeAxisLabelFormatterParamBreak(o,n.break)),e(jw(t,n),r,o)}}return function(e){return t.scale.getLabel(e)}}function jw(t,e){var n=t.scale;return vw(n)?n.getLabel(e):e.value}function qw(t){var e=t.get("interval");return null==e?"auto":e}function Kw(t){return"middle"===t||"center"===t}function $w(t){return t.getShallow("show")}function Jw(t,e,n){var i=t.get("breaks",!0);if(null!=i){if(!Wp())return void 0;if(!n||!function(t){return"category"!==t}(e))return;return i}}function Qw(t,e,n,i,r,o){var a,s,l=gw(t),u=l?t.intervalStub:t;if(u.setExtent(i[0],i[1]),l){var c=t.powStub,h={depth:2},d=t.transformOut(i[0],h),p=t.transformOut(i[1],h),f=(s=i,[(a=n)[0]!==s[0],a[1]!==s[1]]);e[0]&&!f[0]&&(d=r[0]),e[1]&&!f[1]&&(p=r[1]),c.setExtent(d,p)}u.setConfig(o)}function tS(t,e){return vw(t)?t.getRawOrdinalNumber(e.value):e.value}function eS(t,e){return vw(t)&&!!e.get("boundaryGap")}var nS=function(){function t(){}return t.prototype.needIncludeZero=function(){return!this.option.scale},t.prototype.getCoordSysModel=function(){},t}(),iS=Ja(),rS="|&",oS=Pa(),aS=Pa();function sS(t,e){var n=t.model,i=oS(sx(n.ecModel)).keyed,r=i&&i.get(e);return r&&r.get(n.uid)}function lS(t,e){return cS(sS(t,e))}function uS(t,e){var n=oS(sx(t)).keyed;n&&n.each(function(t,n){t.each(function(t,i){e(t,n,i)})})}function cS(t){return{liPosMinGap:t?t.liPosMinGap:void 0}}function hS(t,e,n){var i=sS(t,e);i&&dS(t.model.ecModel,i.sers,n)}function dS(t,e,n){if(e)for(var i=0;i=0}(u)||(u[0]=u[1]=NaN);var c=[],h=[!1,!1],d=e.get("min",!0);"dataMin"===d?(c[0]=u[0],h[0]=!0):(c[0]=MS(t,Z(d)?d({min:u[0],max:u[1]}):d),h[0]=null!=c[0]);var p=e.get("max",!0);"dataMax"===p?(c[1]=u[1],h[1]=!0):(c[1]=MS(t,Z(p)?p({min:u[0],max:u[1]}):p),h[1]=null!=c[1]);var f=function(t,e){var n;if(vw(t))n=[0,0];else{var i=e.get("boundaryGap");"boolean"==typeof i&&(i=null),n=X(i)?i:[i,i]}return[IS(n[0]),IS(n[1])]}(t,e),g=a?null:u[1]-u[0]||Math.abs(u[0]);null==c[0]&&(c[0]=a?o?u[0]:s?0:NaN:u[0]-f[0]*g),null==c[1]&&(c[1]=a?o?u[1]:s?s-1:NaN:u[1]+f[1]*g),!qa(c[0])&&(c[0]=NaN),!qa(c[1])&&(c[1]=NaN);var v=o||rt(c[0])||rt(c[1])||a&&!s,y=pw(t),m=y&&e.needIncludeZero&&e.needIncludeZero();m&&(c[0]>0&&c[1]>0&&!h[0]&&(c[0]=0),c[0]<0&&c[1]<0&&!h[1]&&(c[1]=0));var x=!1;c[0]>c[1]&&(c.reverse(),x=!0);var _=MS(t,e.get("startValue",!0)),b=null!=_;!ca(_)&&i&&(_=t.getDefaultStartValue?t.getDefaultStartValue():0),ca(_)&&(b||!y||m)&&(_c[1]&&!h[1]&&(c[1]=_,h[1]=!0)),SS(this._i={scale:t,dataMM:u,noZoomEffMM:c,zoomMM:[],fixMM:h,zoomFixMM:[!1,!1],startValue:_,isBlank:v,incl0:m,tggAxInv:x,ctnShp:r},c)}return t.prototype.makeNoZoom=function(){return this._i.noZoomEffMM.slice()},t.prototype.makeFinal=function(){var t=this._i,e=t.zoomMM,n=t.noZoomEffMM,i=t.zoomFixMM,r=t.fixMM,o={fixMM:r,zoomFixMM:i,isBlank:t.isBlank,incl0:t.incl0,tggAxInv:t.tggAxInv,ctnShp:t.ctnShp,effMM:n.slice()},a=o.effMM;return null!=e[0]&&(a[0]=e[0],r[0]=i[0]=!0),null!=e[1]&&(a[1]=e[1],r[1]=i[1]=!0),SS(t,a),o},t.prototype.makeRenderInfo=function(){return{startValue:this._i.startValue}},t.prototype.setZoomMM=function(t,e){this._i.zoomMM[t]=e},t}();function SS(t,e){var n=t.scale,i=t.dataMM;n.sanitize&&(e[0]=n.sanitize(e[0],i),e[1]=n.sanitize(e[1],i),$a(e))}function MS(t,e){return null==e?null:rt(e)?NaN:t.parse(e)}function IS(t){return Qr("boolean"==typeof t?0:t,1)||0}function TS(t){var e=bS(t.scale);return e.extent||(e.extent=[1/0,-1/0]),e}function CS(t,e){var n=t.scale,i=t.model,r=t.dim;n.rawExtentInfo||function(t,e,n,i,r){var o=TS(e),a=o.extent,s=!1;!function(t,e){var n=t.model.ecModel,i=oS(sx(n)).axSer;i&&dS(n,i.get(t.model.uid),e)}(e,function(i){if(i.boxCoordinateSystem){var r=Ff(i).coord,l=o.dimIdxInCoord;if(l>=0){if(X(r)){var u=r[l];null==u||X(u)||Xa(a,t.parse(u))}}else 0}else if(i.coordinateSystem){var c=i.getData();if(c){var h=t.getFilter?t.getFilter():null;B(function(t,e){var n={};return B(t.mapDimensionsAll(e),function(e){n[Kb(t,e)]=!0}),W(n)}(c,n),function(t){var e,n;e=a,Ka((n=c.getApproximateExtent(t,h))[0],n[1])&&(n[0]e[1]&&(e[1]=n[1]))})}i.__requireStartValue&&i.__requireStartValue(e)&&(s=!0)}});var l=function(t,e,n){var i=eS(t,n),r=n.get("containShape",!0);null!=r||i||(r=!0);if(!r)return!1;var o=!1;return fS(e,function(t){o=!!kS.get(t)||o}),o}(t,e,i),u=new wS(t,i,a,s,l);DS(t,u,r),o.extent=null}(n,t,r,i,e)}function DS(t,e,n){t.rawExtentInfo=e,e.from=n}function AS(t,e){kS.set(t,e)}var kS=mt();function LS(t,e,n,i,r){t.rawExtentInfo||function(t,e){var n=t.scale;DS(n,new wS(n,t.model,e,!1,!1),3)}({scale:t,model:e},r||[1/0,-1/0]);var o=t.rawExtentInfo.makeFinal(),a=o.effMM;return t.setExtent(a[0],a[1]),t.setBlank(o.isBlank),i&&o.tggAxInv&&n&&!n.get("legacyMinMaxDontInverseAxis")&&(i.inverse=!i.inverse),o}function PS(t,e,n,i){var r;if(n.ctnShp&&(fS(t,function(e){var n=kS.get(e);if(n){var o=n(t,i);o&&(Za(r=r||[0,0],o[0]),ja(r,o[1]),function(t){Fw(t).noOnMyZero=!0}(t))}}),r)){var o=e.getExtent();if(vw(e))t.onBand||e.setExtent2(1,Co(o[0],o[0]+r[0]),Do(o[1],o[1]+r[1]));else{var a=o.slice();n.zoomFixMM[0]||(a[0]=Co(a[0],e.transformOut(e.transformIn(a[0],null)+r[0],null))),n.zoomFixMM[1]||(a[1]=Do(a[1],e.transformOut(e.transformIn(a[1],null)+r[1],null))),(a[0]o[1])&&e.setExtent2(1,a[0],a[1])}}}function OS(t,e){var n=gw(t),i=n?t.intervalStub:t,r=e.fixMinMax||[],o=n?t.getExtent():null,a=i.getExtent(),s=bw(a,r,e.rawExtentResult);i.setExtent(s[0],s[1]),s=i.getExtent();var l=n?function(t,e){var n=ww(e.splitNumber,10),i=t.getExtent(),r=lw(t);0;var o=Do(ta(r),1);n/r*o<=.5&&(o*=10);var a=mw(o),s=[Ho(Po(i[0]/o)*o,a),Ho(Lo(i[1]/o)*o,a)];return{intervalPrecision:a,interval:o,niceExtent:s}}(i,e):function(t,e){var n=ww(e.splitNumber,5),i=lw(t);0;var r=e.minInterval,o=e.maxInterval,a=na(i/n,!0);null!=r&&ao&&(a=o);var s=mw(a),l=t.getExtent(),u=[Ho(Po(l[0]/a)*a,s),Ho(Lo(l[1]/a)*a,s)];return{interval:a,intervalPrecision:s,niceExtent:u}}(i,e),u=l.intervalPrecision,c=l.interval,h=e.userInterval;null!=h&&(l.interval=h,l.intervalPrecision=mw(h)),r[0]||(s[0]=Ho(Lo(s[0]/c)*c,u)),r[1]||(s[1]=Ho(Po(s[1]/c)*c,u)),null!=h&&(l.niceExtent=s.slice()),Qw(t,r,a,s,o,l)}function RS(t){var e=t.scale,n=t.model,i=n.axis,r=n.ecModel;NS(e,n,i,r,null)}function NS(t,e,n,i,r){var o=LS(t,e,i,n,r),a=pw(t)||fw(t);ES(t,{splitNumber:e.get("splitNumber"),fixMinMax:o.fixMM,userInterval:e.get("interval"),minInterval:a?e.get("minInterval"):null,maxInterval:a?e.get("maxInterval"):null,rawExtentResult:o}),n&&i&&PS(n,t,o,i)}function ES(t,e){zS[t.type](t,e)}var zS={interval:OS,log:OS,time:function(t,e){var n=t.getExtent();if(n[0]===n[1]&&(n[0]-=Kp,n[1]+=Kp),n[1]===-1/0&&n[0]===1/0){var i=new Date;n[1]=+new Date(i.getFullYear(),i.getMonth(),i.getDate()),n[0]=n[1]-Kp}t.setExtent(n[0],n[1]);var r=ww(e.splitNumber,10),o=lw(t)/r,a=e.minInterval,s=e.maxInterval;null!=a&&os&&(o=s);var l=Dw.length,u=Math.min(function(t,e,n,i){for(;n>>1;t[r][1]n&&(t=r,n=a)}if(t)return function(t){for(var e=0,n=0,i=0,r=t.length,o=t[r-1][0],a=t[r-1][1],s=0;s>1^-(1&s),l=l>>1^-(1&l),r=s+=r,o=l+=o,i.push([s/n,l/n])}return i}function JS(t,e){return V(F((t=function(t){if(!t.UTF8Encoding)return t;var e=t,n=e.UTF8Scale;return null==n&&(n=1024),B(e.features,function(t){var e=t.geometry,i=e.encodeOffsets,r=e.coordinates;if(i)switch(e.type){case"LineString":e.coordinates=$S(r,i,n);break;case"Polygon":case"MultiLineString":KS(r,i,n);break;case"MultiPolygon":B(r,function(t,e){return KS(t,i[e],n)})}}),e.UTF8Encoding=!1,e}(t)).features,function(t){return t.geometry&&t.properties&&t.geometry.coordinates.length>0}),function(t){var n=t.properties,i=t.geometry,r=[];switch(i.type){case"Polygon":var o=i.coordinates;r.push(new XS(o[0],o.slice(1)));break;case"MultiPolygon":B(i.coordinates,function(t){t[0]&&r.push(new XS(t[0],t.slice(1)))});break;case"LineString":r.push(new ZS([i.coordinates]));break;case"MultiLineString":r.push(new ZS(i.coordinates))}var a=new jS(n[e||"name"],r,n.cp);return a.properties=n,a})}var QS=Object.freeze({__proto__:null,linearMap:Bo,round:function(t,e,n){return null==e&&(e=10),Ho(t,e,n)},asc:Wo,getPrecision:Uo,getPrecisionSafe:Yo,getPixelPrecision:function(t,e){var n=Lo(Ro(t[1]-t[0])/No),i=ko(Ro(Ao(e[1]-e[0]))/No),r=Co(Do(-n+i,0),20);return isFinite(r)?r:20},getPercentWithPrecision:function(t,e,n){return t[e]&&Zo(t,n)[e]||0},parsePercent:Vo,MAX_SAFE_INTEGER:qo,remRadian:Ko,isRadianAroundZero:$o,parseDate:Qo,quantity:ta,quantityExponent:ea,nice:na,quantile:ia,reformIntervals:ra,isNumeric:aa,numericToNumber:oa}),tM=Object.freeze({__proto__:null,parse:Qo,format:uf,roundTime:hf}),eM=Object.freeze({__proto__:null,extendShape:fd,extendPath:vd,makePath:xd,makeImage:_d,mergePath:wd,resizePath:Sd,createIcon:Od,updateProps:rd,initProps:od,getTransform:Td,clipPointsByRect:Ld,clipRectByRect:Pd,registerShape:yd,getShapeClass:md,Group:vo,Image:eu,Text:du,Circle:ah,Ellipse:lh,Sector:Sh,Ring:Ih,Polygon:Dh,Polyline:kh,Rect:lu,Line:Oh,BezierCurve:zh,Arc:Vh,IncrementalDisplayable:td,CompoundPath:Gh,LinearGradient:Hh,RadialGradient:Wh,BoundingRect:Xe}),nM=Object.freeze({__proto__:null,addCommas:Tf,toCamelCase:Cf,normalizeCssArray:Df,encodeHTML:se,formatTpl:Pf,getTooltipMarker:Rf,formatTime:function(t,e,n){"week"!==t&&"month"!==t&&"quarter"!==t&&"half-year"!==t&&"year"!==t||(t="MM-dd\nyyyy");var i=Qo(e),r=n?"getUTC":"get",o=i[r+"FullYear"](),a=i[r+"Month"]()+1,s=i[r+"Date"](),l=i[r+"Hours"](),u=i[r+"Minutes"](),c=i[r+"Seconds"](),h=i[r+"Milliseconds"]();return t=t.replace("MM",af(a,2)).replace("M",a).replace("yyyy",o).replace("yy",af(o%100+"",2)).replace("dd",af(s,2)).replace("d",s).replace("hh",af(l,2)).replace("h",l).replace("mm",af(u,2)).replace("m",u).replace("ss",af(c,2)).replace("s",c).replace("SSS",af(h,3))},capitalFirst:function(t){return t?t.charAt(0).toUpperCase()+t.substr(1):t},truncateText:function(t,e,n,i,r){var o={};return Is(o,t,e,n,i,r),o.text},getTextRect:function(t,e,n,i,r,o,a,s){return new du({style:{text:t,font:e,align:n,verticalAlign:i,padding:r,rich:o,overflow:a?"truncate":null,lineHeight:s}}).getBoundingRect()}}),iM=Object.freeze({__proto__:null,map:V,each:B,indexOf:R,inherits:N,reduce:G,filter:F,bind:U,curry:Y,isArray:X,isString:j,isObject:$,isFunction:Z,extend:k,defaults:P,clone:C,merge:D}),rM=Pa(),oM=Pa(),aM=1,sM=2;function lM(t){return{out:{noPxChangeTryDetermine:[]},kind:t}}function uM(t,e){var n=t.getLabelModel().get("customValues");if(n){var i=t.scale;return{labels:V(hM(n,i),function(e,n){return{formattedLabel:Zw(t)(e,n),rawLabel:i.getLabel(e),tick:e}})}}return"category"===t.type?function(t,e){var n=t.getLabelModel(),i=dM(t,n,e);return!n.get("show")||t.scale.isBlank()?{labels:[]}:i}(t,e):function(t){var e=t.scale.getTicks(),n=Zw(t);return{labels:V(e,function(e,i){return{formattedLabel:n(e,i),rawLabel:t.scale.getLabel(e),tick:e}})}}(t)}function cM(t,e,n){var i=t.scale,r=t.getTickModel().get("customValues");return r?{ticks:hM(r,i)}:"category"===t.type?function(t,e){var n,i,r=pM(t),o=qw(e),a=vM(r,o);if(a)return a;e.get("show")&&!t.scale.isBlank()||(n=[]);if(Z(o))n=_M(t,o,!0);else if("auto"===o){var s=dM(t,t.getLabelModel(),lM(sM));i=s.labelCategoryInterval,n=V(s.labels,function(t){return t.tick})}else n=_M(t,i=o,!0);return yM(r,o,{ticks:n,tickCategoryInterval:i})}(t,e):{ticks:i.getTicks(n)}}function hM(t,e){var n=e.getExtent(),i=[];return B(t,function(t){(t=e.parse(t))>=n[0]&&t<=n[1]&&i.push(t)}),ts(i,ns,null),Wo(i),V(i,function(t){return{value:t}})}function dM(t,e,n){var i,r,o=fM(t),a=qw(e),s=n.kind===aM;if(!s){var l=vM(o,a);if(l)return l}Z(a)?i=_M(t,a,!1):(r="auto"===a?function(t,e){if(e.kind===aM){var n=t.calculateCategoryInterval(e);return e.out.noPxChangeTryDetermine.push(function(){return oM(t).autoInterval=n,!0}),n}var i=oM(t).autoInterval;return null!=i?i:oM(t).autoInterval=t.calculateCategoryInterval(e)}(t,n):a,i=_M(t,r,!1));var u={labels:i,labelCategoryInterval:r};return s?n.out.noPxChangeTryDetermine.push(function(){return yM(o,a,u),!0}):yM(o,a,u),u}var pM=gM("axisTick"),fM=gM("axisLabel");function gM(t){return function(e){return oM(e)[t]||(oM(e)[t]={list:[]})}}function vM(t,e){for(var n=0;ne&&i.axisExtent0===r[0]&&i.axisExtent1===r[1])return o;i.lastTickCount=n,i.lastAutoInterval=e,i.axisExtent0=r[0],i.axisExtent1=r[1]}function _M(t,e,n){var i=Zw(t),r=t.scale,o=[],a=Z(e);return Sw(r,a?0:e,function(t,s){var l=r.getLabel(t);if(a){var u=!!e(t.value,l);if(t.offInterval=!u,!u&&!s)return}o.push(n?t:{formattedLabel:i(t),rawLabel:l,tick:t})}),o}function bM(t,e){e=e||{};var n,i={w:NaN,w2:NaN},r=t.scale,o=e.fromStat,a=e.min,s=(n=sw(r,3))[1]-n[0];ca(s)||(s=NaN);var l=t.getExtent(),u=Ao(l[1]-l[0]);return vw(r)?function(t,e,n,i){var r=e.onBand,o=n+(r?1:0);0===o&&(o=1),t.w=i/o,!r&&n&&i&&(t.w2=t.w*n/i)}(i,t,s,u):o&&function(t,e,n,i,r){0;var o=!1,a=-1/0;B(r.key?[lS(e,r.key)]:function(t,e){var n=[];return uS(t.model.ecModel,function(t){for(var i=0;i0?(e>a&&(a=e),o=!1):-2===e&&(o=!0))}),ca(n)&&n>0&&ca(a)?(t.w=i/n*a,t.w2=a):o&&(t.w=.8*i,t.w2=t.w*n/i)}(i,t,s,u,o),null!=a&&(i.w=ca(i.w)?Do(a,i.w):a),i}var wM=[0,1],SM=function(){function t(t,e,n){this.onBand=!1,this.inverse=!1,this.dim=t,this.scale=e,this._extent=n||[0,0]}return t.prototype.contain=function(t){var e=this._extent,n=Math.min(e[0],e[1]),i=Math.max(e[0],e[1]);return t>=n&&t<=i},t.prototype.containData=function(t){return this.scale.contain(this.scale.parse(t))},t.prototype.getExtent=function(){return this._extent.slice()},t.prototype.setExtent=function(t,e){var n=this._extent;n[0]=t,n[1]=e},t.prototype.dataToCoord=function(t,e){var n=this.scale;return Bo(t=n.normalize(n.parse(t)),wM,MM(this),e)},t.prototype.coordToData=function(t,e){var n=Bo(t,MM(this),wM,e);return this.scale.scale(n)},t.prototype.pointToData=function(t,e){},t.prototype.getTicksCoords=function(t){var e=(t=t||{}).tickModel||this.getTickModel(),n=V(cM(this,e,{breakTicks:t.breakTicks,pruneByBreak:t.pruneByBreak}).ticks,function(t){return{coord:this.dataToCoord(tS(this.scale,t)),tick:t}},this),i=function(t,e,n){var i=e.length;if(!t.onBand||n||!i)return!1;var r=bM(t).w;if(!r)return!1;B(e,function(t){t.coord-=r/2});var o=t.scale.getExtent(),a=e[i-1];a.tick.offInterval&&e.pop();return e.push({coord:a.coord+r,tick:{value:o[1]+1}}),!0}(this,n,e.get("alignWithLabel"));return V(n,function(t){return{coord:t.coord,tickValue:t.tick.value,onBand:i}})},t.prototype.getMinorTicksCoords=function(){if(vw(this.scale))return[];var t=this.model.getModel("minorTick").get("splitNumber");return t>0&&t<100||(t=5),V(this.scale.getMinorTicks(t),function(t){return V(t,function(t){return{coord:this.dataToCoord(t),tickValue:t}},this)},this)},t.prototype.getViewLabels=function(t){return uM(this,t=t||lM(sM)).labels},t.prototype.getLabelModel=function(){return this.model.getModel("axisLabel")},t.prototype.getTickModel=function(){return this.model.getModel("axisTick")},t.prototype.getBandWidth=function(){return bM(this,{min:1}).w},t.prototype.calculateCategoryInterval=function(t){return function(t,e){var n=e.kind,i=function(t){var e=t.getLabelModel();return{axisRotate:t.getRotate?t.getRotate():t.isHorizontal&&!t.isHorizontal()?90:0,labelRotate:e.get("rotate")||0,font:e.getFont()}}(t),r=Zw(t),o=(i.axisRotate-i.labelRotate)/180*Math.PI,a=t.scale,s=a.getExtent(),l=a.count();if(s[1]-s[0]<1)return 0;var u=1;l>40&&(u=Math.max(1,Math.floor(l/40)));for(var c=s[0],h=t.dataToCoord(c+1)-t.dataToCoord(c),d=Math.abs(h*Math.cos(o)),p=Math.abs(h*Math.sin(o)),f=0,g=0;c<=s[1];c+=u){var v,y,m=qr(r({value:c}),i.font,"center","top");v=1.3*m.width,y=1.3*m.height,f=Math.max(f,v,7),g=Math.max(g,y,7)}var x=f/d,_=g/p;isNaN(x)&&(x=1/0),isNaN(_)&&(_=1/0);var b=Math.max(0,Math.floor(Math.min(x,_)));if(n===aM)return e.out.noPxChangeTryDetermine.push(U(mM,null,t,b,l)),b;var w=xM(t,b,l);return null!=w?w:b}(this,t=t||lM(sM))},t}();function MM(t){var e=t.getExtent();if(t.onBand){var n=(e[1]-e[0])/t.scale.count()/2;e[0]+=n,e[1]-=n}return e}var IM=2*Math.PI,TM=Al.CMD,CM=["top","right","bottom","left"];function DM(t,e,n,i,r){var o=n.width,a=n.height;switch(t){case"top":i.set(n.x+o/2,n.y-e),r.set(0,-1);break;case"bottom":i.set(n.x+o/2,n.y+a+e),r.set(0,1);break;case"left":i.set(n.x-e,n.y+a/2),r.set(-1,0);break;case"right":i.set(n.x+o+e,n.y+a/2),r.set(1,0)}}function AM(t,e,n,i,r,o,a,s,l){a-=t,s-=e;var u=Math.sqrt(a*a+s*s),c=(a/=u)*n+t,h=(s/=u)*n+e;if(Math.abs(i-r)%IM<1e-4)return l[0]=c,l[1]=h,u-n;if(o){var d=i;i=Rl(r),r=Rl(d)}else i=Rl(i),r=Rl(r);i>r&&(r+=IM);var p=Math.atan2(s,a);if(p<0&&(p+=IM),p>=i&&p<=r||p+IM>=i&&p+IM<=r)return l[0]=c,l[1]=h,u-n;var f=n*Math.cos(i)+t,g=n*Math.sin(i)+e,v=n*Math.cos(r)+t,y=n*Math.sin(r)+e,m=(f-a)*(f-a)+(g-s)*(g-s),x=(v-a)*(v-a)+(y-s)*(y-s);return m0){e=e/180*Math.PI,NM.fromArray(t[0]),EM.fromArray(t[1]),zM.fromArray(t[2]),Pe.sub(BM,NM,EM),Pe.sub(VM,zM,EM);var n=BM.len(),i=VM.len();if(!(n<.001||i<.001)){BM.scale(1/n),VM.scale(1/i);var r=BM.dot(VM);if(Math.cos(e)1&&Pe.copy(HM,zM),HM.toArray(t[1])}}}}function UM(t,e,n){if(n<=180&&n>0){n=n/180*Math.PI,NM.fromArray(t[0]),EM.fromArray(t[1]),zM.fromArray(t[2]),Pe.sub(BM,EM,NM),Pe.sub(VM,zM,EM);var i=BM.len(),r=VM.len();if(!(i<.001||r<.001))if(BM.scale(1/i),VM.scale(1/r),BM.dot(e)=a)Pe.copy(HM,zM);else{HM.scaleAndAdd(VM,o/Math.tan(Math.PI/2-s));var l=zM.x!==EM.x?(HM.x-EM.x)/(zM.x-EM.x):(HM.y-EM.y)/(zM.y-EM.y);if(isNaN(l))return;l<0?Pe.copy(HM,EM):l>1&&Pe.copy(HM,zM)}HM.toArray(t[1])}}}function YM(t,e,n,i){var r="normal"===n,o=r?t:t.ensureState(n);o.ignore=e;var a=i.get("smooth");a=!0===a?.3:Math.max(+a,0)||0,o.shape=o.shape||{},o.shape.smooth=a;var s=i.getModel("lineStyle").getLineStyle();r?t.useStyle(s):o.style=s}function XM(t,e){var n=e.smooth,i=e.points;if(i)if(t.moveTo(i[0][0],i[0][1]),n>0&&i.length>=3){var r=Ft(i[0],i[1]),o=Ft(i[1],i[2]);if(!r||!o)return t.lineTo(i[1][0],i[1][1]),void t.lineTo(i[2][0],i[2][1]);var a=Math.min(r,o)*n,s=Ut([],i[1],i[0],a/r),l=Ut([],i[1],i[2],a/o),u=Ut([],s,l,.5);t.bezierCurveTo(s[0],s[1],s[0],s[1],u[0],u[1]),t.bezierCurveTo(l[0],l[1],l[0],l[1],i[2][0],i[2][1])}else for(var c=1;c0&&r&&b(-h/o,0,o);var g,v,y=t[0],m=t[o-1];function x(){g=y.rect[a]-n,v=i-m.rect[a]-m.rect[s]}function _(t,e,n){if(t<0){var i=Math.min(e,-t);if(i>0){b(i*n,0,o);var r=i+t;r<0&&w(-r*n,1)}else w(-t*n,1)}}function b(e,n,i){0!==e&&(c=!0);for(var r=n;r0)for(l=0;l0;l--){b(-(i[l-1]*h),l,o)}}}function S(t){var e=t<0?-1:1;t=Math.abs(t);for(var n=Math.ceil(t/(o-1)),i=0;i0?b(n,0,i+1):b(-n,o-i-1,o),(t-=n)<=0)return}return x(),g<0&&w(-g,.8),v<0&&w(v,.8),x(),_(g,v,1),_(v,g,-1),x(),g<0&&S(-g),v<0&&S(v),c}function oI(t){var e=[];function n(t){if(!t.ignore){var e=t.ensureState("emphasis");null==e.ignore&&(e.ignore=!1)}t.ignore=!0}t.sort(function(t,e){return(e.suggestIgnore?1:0)-(t.suggestIgnore?1:0)||e.priority-t.priority});for(var i=0;i=0&&n.attr(p.oldLayoutSelect),R(u,"emphasis")>=0&&n.attr(p.oldLayoutEmphasis)),rd(n,s,e,a)}else if(n.attr(s),!fp(n).valueAnimation){var c=at(n.style.opacity,1);n.style.opacity=0,od(n,{style:{opacity:c}},e,a)}if(p.oldLayout=s,n.states.select){var h=p.oldLayoutSelect={};pI(h,s,fI),pI(h,n.states.select,fI)}if(n.states.emphasis){var d=p.oldLayoutEmphasis={};pI(d,s,fI),pI(d,n.states.emphasis,fI)}vp(n,a,l,e,e)}if(i&&!i.ignore&&!i.invisible){r=(p=dI(i)).oldLayout;var p,f={points:i.shape.points};r?(i.attr({shape:r}),rd(i,{shape:f},e)):(i.setShape(f),i.style.strokePercent=0,od(i,{style:{strokePercent:1}},e)),p.oldLayout=f}},t}(),vI=Pa();var yI=Math.sin,mI=Math.cos,xI=Math.PI,_I=2*Math.PI,bI=180/xI,wI=function(){function t(){}return t.prototype.reset=function(t){this._start=!0,this._d=[],this._str="",this._p=Math.pow(10,t||4)},t.prototype.moveTo=function(t,e){this._add("M",t,e)},t.prototype.lineTo=function(t,e){this._add("L",t,e)},t.prototype.bezierCurveTo=function(t,e,n,i,r,o){this._add("C",t,e,n,i,r,o)},t.prototype.quadraticCurveTo=function(t,e,n,i){this._add("Q",t,e,n,i)},t.prototype.arc=function(t,e,n,i,r,o){this.ellipse(t,e,n,n,0,i,r,o)},t.prototype.ellipse=function(t,e,n,i,r,o,a,s){var l=a-o,u=!s,c=Math.abs(l),h=Pi(c-_I)||(u?l>=_I:-l>=_I),d=l>0?l%_I:l%_I+_I,p=!1;p=!!h||!Pi(c)&&d>=xI==!!u;var f=t+n*mI(o),g=e+i*yI(o);this._start&&this._add("M",f,g);var v=Math.round(r*bI);if(h){var y=1/this._p,m=(u?1:-1)*(_I-y);this._add("A",n,i,v,1,+u,t+n*mI(o+m),e+i*yI(o+m)),y>.01&&this._add("A",n,i,v,0,+u,f,g)}else{var x=t+n*mI(a),_=e+i*yI(a);this._add("A",n,i,v,+p,+u,x,_)}},t.prototype.rect=function(t,e,n,i){this._add("M",t,e),this._add("l",n,0),this._add("l",0,i),this._add("l",-n,0),this._add("Z")},t.prototype.closePath=function(){this._d.length>0&&this._add("Z")},t.prototype._add=function(t,e,n,i,r,o,a,s,l){for(var u=[],c=this._p,h=1;h"}(r,o)+("style"!==r?se(a):a||"")+(i?""+n+V(i,function(e){return t(e)}).join(n)+n:"")+("")}(t)}function RI(t){return{zrId:t,shadowCache:{},patternCache:{},gradientCache:{},clipPathCache:{},defs:{},cssNodes:{},cssAnims:{},cssStyleCache:{},cssAnimIdx:0,shadowIdx:0,gradientIdx:0,patternIdx:0,clipPathIdx:0}}function NI(t,e,n,i){return PI("svg","root",{width:t,height:e,xmlns:DI,"xmlns:xlink":AI,version:"1.1",baseProfile:"full",viewBox:!!i&&"0 0 "+t+" "+e},n)}var EI=0;function zI(){return EI++}var BI={cubicIn:"0.32,0,0.67,0",cubicOut:"0.33,1,0.68,1",cubicInOut:"0.65,0,0.35,1",quadraticIn:"0.11,0,0.5,0",quadraticOut:"0.5,1,0.89,1",quadraticInOut:"0.45,0,0.55,1",quarticIn:"0.5,0,0.75,0",quarticOut:"0.25,1,0.5,1",quarticInOut:"0.76,0,0.24,1",quinticIn:"0.64,0,0.78,0",quinticOut:"0.22,1,0.36,1",quinticInOut:"0.83,0,0.17,1",sinusoidalIn:"0.12,0,0.39,0",sinusoidalOut:"0.61,1,0.88,1",sinusoidalInOut:"0.37,0,0.63,1",exponentialIn:"0.7,0,0.84,0",exponentialOut:"0.16,1,0.3,1",exponentialInOut:"0.87,0,0.13,1",circularIn:"0.55,0,1,0.45",circularOut:"0,0.55,0.45,1",circularInOut:"0.85,0,0.15,1"},VI="transform-origin";function GI(t,e,n){var i=k({},t.shape);k(i,e),t.buildPath(n,i);var r=new wI;return r.reset(Hi(t)),n.rebuildPath(r,1),r.generateStr(),r.getStr()}function FI(t,e){var n=e.originX,i=e.originY;(n||i)&&(t[VI]=n+"px "+i+"px")}var HI={fill:"fill",opacity:"opacity",lineWidth:"stroke-width",lineDashOffset:"stroke-dashoffset"};function WI(t,e){var n=e.zrId+"-ani-"+e.cssAnimIdx++;return e.cssAnims[n]=t,n}function UI(t){return j(t)?BI[t]?"cubic-bezier("+BI[t]+")":Jn(t)?t:"":""}function YI(t,e,n,i){var r=t.animators,o=r.length,a=[];if(t instanceof Gh){var s=function(t,e,n){var i,r,o=t.shape.paths,a={};if(B(o,function(t){var e=RI(n.zrId);e.animation=!0,YI(t,{},e,!0);var o=e.cssAnims,s=e.cssNodes,l=W(o),u=l.length;if(u){var c=o[r=l[u-1]];for(var h in c){var d=c[h];a[h]=a[h]||{d:""},a[h].d+=d.d||""}for(var p in s){var f=s[p].animation;f.indexOf(r)>=0&&(i=f)}}}),i){e.d=!1;var s=WI(a,n);return i.replace(r,s)}}(t,e,n);if(s)a.push(s);else if(!o)return}else if(!o)return;for(var l={},u=0;u0}).length)return WI(c,n)+" "+r[0]+" both"}for(var v in l){(s=g(l[v]))&&a.push(s)}if(a.length){var y=n.zrId+"-cls-"+zI();n.cssNodes["."+y]={animation:a.join(",")},e.class=y}}function XI(t,e,n,i){var r=JSON.stringify(t),o=n.cssStyleCache[r];o||(o=n.zrId+"-cls-"+zI(),n.cssStyleCache[r]=o,n.cssNodes["."+o+(i?":hover":"")]=t),e.class=e.class?e.class+" "+o:o}var ZI=Math.round;function jI(t){return t&&j(t.src)}function qI(t){return t&&Z(t.toDataURL)}function KI(t,e,n,i){CI(function(r,o){var a="fill"===r||"stroke"===r;a&&Gi(o)?sT(e,t,r,i):a&&zi(o)?lT(n,t,r,i):t[r]=o,a&&i.ssr&&"none"===o&&(t["pointer-events"]="visible")},e,n,!1),function(t,e,n){var i=t.style;if(function(t){return t&&(t.shadowBlur||t.shadowOffsetX||t.shadowOffsetY)}(i)){var r=function(t){var e=t.style,n=t.getGlobalScale();return[e.shadowColor,(e.shadowBlur||0).toFixed(2),(e.shadowOffsetX||0).toFixed(2),(e.shadowOffsetY||0).toFixed(2),n[0],n[1]].join(",")}(t),o=n.shadowCache,a=o[r];if(!a){var s=t.getGlobalScale(),l=s[0],u=s[1];if(!l||!u)return;var c=i.shadowOffsetX||0,h=i.shadowOffsetY||0,d=i.shadowBlur,p=ki(i.shadowColor),f=p.opacity,g=p.color,v=d/2/l+" "+d/2/u;a=n.zrId+"-s"+n.shadowIdx++,n.defs[a]=PI("filter",a,{id:a,x:"-100%",y:"-100%",width:"300%",height:"300%"},[PI("feDropShadow","",{dx:c/l,dy:h/u,stdDeviation:v,"flood-color":g,"flood-opacity":f})]),o[r]=a}e.filter=Fi(a)}}(n,t,i)}function $I(t,e){var n=So(e);n&&(n.each(function(e,n){null!=e&&(t[(kI+n).toLowerCase()]=e+"")}),e.isSilent()&&(t[kI+"silent"]="true"))}function JI(t){return Pi(t[0]-1)&&Pi(t[1])&&Pi(t[2])&&Pi(t[3]-1)}function QI(t,e,n){if(e&&(!function(t){return Pi(t[4])&&Pi(t[5])}(e)||!JI(e))){var i=n?10:1e4;t.transform=JI(e)?"translate("+ZI(e[4]*i)/i+" "+ZI(e[5]*i)/i+")":function(t){return"matrix("+Oi(t[0])+","+Oi(t[1])+","+Oi(t[2])+","+Oi(t[3])+","+Ri(t[4])+","+Ri(t[5])+")"}(e)}}function tT(t,e,n){for(var i=t.points,r=[],o=0;o=0&&a||o;s&&(r=Ci(s))}var l=i.lineWidth;l&&(l/=!i.strokeNoScale&&t.transform?t.transform[0]:1);var u={cursor:"pointer"};r&&(u.fill=r),i.stroke&&(u.stroke=i.stroke),l&&(u["stroke-width"]=l),XI(u,e,n,!0)}}(t,o,e),PI(s,t.id+"",o)}function aT(t,e){return t instanceof Kl?oT(t,e):t instanceof eu?function(t,e){var n=t.style,i=n.image;if(i&&!j(i)&&(jI(i)?i=i.src:qI(i)&&(i=i.toDataURL())),i){var r=n.x||0,o=n.y||0,a={href:i,width:n.width,height:n.height};return r&&(a.x=r),o&&(a.y=o),QI(a,t.transform),KI(a,n,t,e),$I(a,t),e.animation&&YI(t,a,e),PI("image",t.id+"",a)}}(t,e):t instanceof Jl?function(t,e){var n=t.style,i=n.text;if(null!=i&&(i+=""),i&&!isNaN(n.x)&&!isNaN(n.y)){var r=n.font||a,s=n.x||0,l=function(t,e,n){return"top"===n?t+=e/2:"bottom"===n&&(t-=e/2),t}(n.y||0,Jr(r),n.textBaseline),u={"dominant-baseline":"central","text-anchor":Ni[n.textAlign]||n.textAlign};if(mu(n)){var c="",h=n.fontStyle,d=vu(n.fontSize);if(!parseFloat(d))return;var p=n.fontFamily||o,f=n.fontWeight;c+="font-size:"+d+";font-family:"+p+";",h&&"normal"!==h&&(c+="font-style:"+h+";"),f&&"normal"!==f&&(c+="font-weight:"+f+";"),u.style=c}else u.style="font: "+r;return i.match(/\s/)&&(u["xml:space"]="preserve"),s&&(u.x=s),l&&(u.y=l),QI(u,t.transform),KI(u,n,t,e),$I(u,t),e.animation&&YI(t,u,e),PI("text",t.id+"",u,void 0,i)}}(t,e):void 0}function sT(t,e,n,i){var r,o=t[n],a={gradientUnits:o.global?"userSpaceOnUse":"objectBoundingBox"};if(Bi(o))r="linearGradient",a.x1=o.x,a.y1=o.y,a.x2=o.x2,a.y2=o.y2;else{if(!Vi(o))return void 0;r="radialGradient",a.cx=at(o.x,.5),a.cy=at(o.y,.5),a.r=at(o.r,.5)}for(var s=o.colorStops,l=[],u=0,c=s.length;ul?ST(t,null==n[h+1]?null:n[h+1].elm,n,s,h):MT(t,e,a,l))}(n,i,r):xT(r)?(xT(t.text)&&vT(n,""),ST(n,null,r,0,r.length-1)):xT(i)?MT(n,i,0,i.length-1):xT(t.text)&&vT(n,""):t.text!==e.text&&(xT(i)&&MT(n,i,0,i.length-1),vT(n,e.text)))}var CT=0,DT=function(){function t(t,e,n){if(this.type="svg",this.configLayer=function(){},this.storage=e,this._opts=n=k({},n),this.root=t,this._id="zr"+CT++,this._oldVNode=NI(n.width,n.height),t&&!n.ssr){var i=this._viewport=document.createElement("div");i.style.cssText="position:relative;overflow:hidden";var r=this._svgDom=this._oldVNode.elm=LI("svg");IT(null,this._oldVNode),i.appendChild(r),t.appendChild(i)}this.resize(n.width,n.height)}return t.prototype.getType=function(){return this.type},t.prototype.getViewportRoot=function(){return this._viewport},t.prototype.getViewportRootOffset=function(){var t=this.getViewportRoot();if(t)return{offsetLeft:t.offsetLeft||0,offsetTop:t.offsetTop||0}},t.prototype.getSvgDom=function(){return this._svgDom},t.prototype.refresh=function(){if(this.root){var t=this.renderToVNode({willUpdate:!0});t.attrs.style="position:absolute;left:0;top:0;user-select:none",function(t,e){if(bT(t,e))TT(t,e);else{var n=t.elm,i=fT(n);wT(e),null!==i&&(hT(i,e.elm,gT(n)),MT(i,[t],0,0))}}(this._oldVNode,t),this._oldVNode=t}},t.prototype.renderOneToVNode=function(t){return aT(t,RI(this._id))},t.prototype.renderToVNode=function(t){t=t||{};var e=this.storage.getDisplayList(!0),n=this._width,i=this._height,r=RI(this._id);r.animation=t.animation,r.willUpdate=t.willUpdate,r.compress=t.compress,r.emphasis=t.emphasis,r.ssr=this._opts.ssr;var o=[],a=this._bgVNode=function(t,e,n,i){var r;if(n&&"none"!==n)if(r=PI("rect","bg",{width:t,height:e,x:"0",y:"0"}),Gi(n))sT({fill:n},r.attrs,"fill",i);else if(zi(n))lT({style:{fill:n},dirty:St,getBoundingRect:function(){return{width:t,height:e}}},r.attrs,"fill",i);else{var o=ki(n),a=o.color,s=o.opacity;r.attrs.fill=a,s<1&&(r.attrs["fill-opacity"]=s)}return r}(n,i,this._backgroundColor,r);a&&o.push(a);var s=t.compress?null:this._mainVNode=PI("g","main",{},[]);this._paintList(e,r,s?s.children:o),s&&o.push(s);var l=V(W(r.defs),function(t){return r.defs[t]});if(l.length&&o.push(PI("defs","defs",{},l)),t.animation){var u=function(t,e,n){var i=(n=n||{}).newline?"\n":"",r=" {"+i,o=i+"}",a=V(W(t),function(e){return e+r+V(W(t[e]),function(n){return n+":"+t[e][n]+";"}).join(i)+o}).join(i),s=V(W(e),function(t){return"@keyframes "+t+r+V(W(e[t]),function(n){return n+r+V(W(e[t][n]),function(i){var r=e[t][n][i];return"d"===i&&(r='path("'+r+'")'),i+":"+r+";"}).join(i)+o}).join(i)+o}).join(i);return a||s?[""].join(i):""}(r.cssNodes,r.cssAnims,{newline:!0});if(u){var c=PI("style","stl",{},[],u);o.push(c)}}return NI(n,i,o,t.useViewBox)},t.prototype.renderToString=function(t){return t=t||{},OI(this.renderToVNode({animation:at(t.cssAnimation,!0),emphasis:at(t.cssEmphasis,!0),willUpdate:!1,compress:!0,useViewBox:at(t.useViewBox,!0)}),{newline:!0})},t.prototype.setBackgroundColor=function(t){this._backgroundColor=t},t.prototype.getSvgRoot=function(){return this._mainVNode&&this._mainVNode.elm},t.prototype._paintList=function(t,e,n){for(var i,r,o=t.length,a=[],s=0,l=0,u=0;u=0&&(!h||!r||h[f]!==r[f]);f--);for(var g=p-1;g>f;g--)i=a[--s-1];for(var v=f+1;v=o)}}for(var u=LT(this),c=u.startIdx;c=0)&&(o=!0)}),o||r.__dirty){var a=n._opts.useDirtyRect&&!kT(r)?r.createRepaintRects(t,e,n._width,n._height):null,s=n._i.layerStack[0],l=!0;if(r.__dirty){l=!1,r.__dirty=!1;var u=r.zlevel===s.zl&&r.zlevel2===s.zl2?n._backgroundColor:null;r.clear(!1,u,a)}VT(r,function(e){var o=n._paintPerCursor(r,e,t,a,l);i=i&&o})}},YT),r.wxa&&FT(this._i,function(t){t&&t.ctx&&t.ctx.draw&&t.ctx.draw()}),i},t.prototype._paintPerCursor=function(t,e,n,i,r){var o=t.ctx;if(i)if(i.length)for(var a=this.dpr,s=0;s=e.endIdx},t.prototype._paintPerCursorInRect=function(t,e,n,i,r){for(var o={inHover:!1,allClipped:!1,prevEl:null,viewWidth:this._width,viewHeight:this._height,beforeBrushParam:{contentRetained:r}},a=t.ctx,s=kT(t),l=s&&c.getTime(),u=e.drawIdx,h=e.notClearIdx,d=h>=0?Math.min(h,u):u;d15){d++;break}}}Wx(a,o),e.drawIdx=Math.max(d,u)},t.prototype.getLayer=function(t,e){return this._ensureLayer(t,0,e)},t.prototype._ensureLayer=function(t,e,n){e=e||0;var i=this._singleCanvas;i&&!this._needsManuallyCompositing&&(t=RT,e=0);var r=GT(this._i,t)[e];return r||(r=ET("zr_"+t+"."+e,this,t,e),this._layerConfig[t]&&D(r,this._layerConfig[t],!0),(n||i&&t!==RT)&&(r.virtual=!0),this._insertLayer(r,t,e,!1),r.initContext()),r},t.prototype.insertLayer=function(t,e){this._insertLayer(e,t,0,!1)},t.prototype._insertLayer=function(t,e,n,i){var r=this._i,o=r.layers,a=r.layerStack,s=this._domRoot,l=null;if((!o[e]||!o[e][n])&&function(t){return!!t&&(!!t.__builtin__||"function"==typeof t.resize&&"function"==typeof t.refresh)}(t)){for(var u=a.length,c=0;c0&&(l=GT(r,a[c-1].zl)[a[c-1].zl2]),a.splice(c,0,{zl:e,zl2:n}),GT(r,e)[n]=t,!i&&!t.virtual)if(l){var h=l.dom;h.nextSibling?s.insertBefore(t.dom,h.nextSibling):s.appendChild(t.dom)}else s.firstChild?s.insertBefore(t.dom,s.firstChild):s.appendChild(t.dom);t.painter||(t.painter=this)}},t.prototype.eachLayer=function(t,e){return FT(this._i,function(n,i){t.call(e,n,i)})},t.prototype.eachBuiltinLayer=function(t,e){return FT(this._i,function(n,i){t.call(e,n,i)},HT)},t.prototype.eachOtherLayer=function(t,e){return FT(this._i,function(n,i){t.call(e,n,i)},WT)},t.prototype.getLayers=function(){var t={};return FT(this._i,function(e,n,i){t[e.id]=e}),t},t.prototype._updateLayerStatus=function(t,e){var n,i=this;if(i._singleCanvas)for(var r=1;r=0;o--){var a=r.get(n[o]);if(a.used){var s=a.endIdxNew;(kT(e)?s=0;i--){var r=e[i];if(r.zl===t){var o=n[t][r.zl2];if(o.__builtin__)continue;if(e.splice(i,1),n[t][r.zl2]=void 0,!o.virtual){var a=o.dom.parentNode;a&&a.removeChild(o.dom)}}}},t.prototype.resize=function(t,e){if(this._domRoot.style){var n=this._domRoot;n.style.display="none";var i=this._opts,r=this.root;null!=t&&(i.width=t),null!=e&&(i.height=e),t=Tx(r,0,i),e=Tx(r,1,i),n.style.display="",this._width===t&&e===this._height||(n.style.width=t+"px",n.style.height=e+"px",FT(this._i,function(n){n.resize(t,e)}),this.refresh({paintAll:!0})),this._width=t,this._height=e}else{if(null==t||null==e)return;this._width=t,this._height=e,this._ensureLayer(RT).resize(t,e)}return this},t.prototype.clearLayer=function(t){B(this._i.layers[t],function(t){t&&!t.__builtin__&&t.clear()})},t.prototype.dispose=function(){this.root.innerHTML="",this.root=this.storage=this._domRoot=this._i=null},t.prototype.getRenderedCanvas=function(t){if(t=t||{},this._singleCanvas&&!this._compositeManually)return this._i.layers[314159][0].dom;var e=new PT("image",this,t.pixelRatio||this.dpr);e.initContext(),e.clear(!1,t.backgroundColor||this._backgroundColor);var n=e.ctx;if(t.pixelRatio<=this.dpr){this.refresh();var i=e.dom.width,r=e.dom.height;FT(this._i,function(t){t.__builtin__?n.drawImage(t.dom,0,0,i,r):t.renderToCanvas&&(n.save(),t.renderToCanvas(n),n.restore())})}else{for(var o={inHover:!1,viewWidth:this._width,viewHeight:this._height,beforeBrushParam:{}},a=this.storage.getDisplayList(!0),s=0,l=a.length;s-1&&(s.style.stroke=s.style.fill,s.style.fill=ug.color.neutral00,s.style.lineWidth=2),e},e.type="series.line",e.dependencies=["grid","polar"],e.defaultOption={z:3,coordinateSystem:"cartesian2d",legendHoverLink:!0,clip:!0,label:{position:"top"},endLabel:{show:!1,valueAnimation:!0,distance:8},lineStyle:{width:2,type:"solid"},emphasis:{scale:!0},step:!1,smooth:!1,smoothMonotone:null,symbol:"emptyCircle",symbolSize:6,symbolRotate:null,showSymbol:!0,showAllSymbol:"auto",connectNulls:!1,sampling:"none",animationEasing:"linear",progressive:0,hoverLayerThreshold:1/0,universalTransition:{divideShape:"clone"},triggerLineEvent:!1,triggerEvent:!1},e}(Hy);function jT(t,e){var n=t.mapDimensionsAll("defaultedLabel"),i=n.length;if(1===i){var r=zv(t,e,n[0]);return null!=r?r+"":null}if(i){for(var o=[],a=0;a=0&&i.push(e[o])}return i.join(" ")}var KT=function(t){function e(e,n,i,r){var o=t.call(this)||this;return o.updateData(e,n,i,r),o}return n(e,t),e.prototype._createSymbol=function(t,e,n,i,r,o){this.removeAll();var a=xx(t,-1,-1,2,2,null,o);a.attr({z2:at(r,100),culling:!0,scaleX:i[0]/2,scaleY:i[1]/2}),a.drift=$T,this._symbolType=t,this.add(a)},e.prototype.stopSymbolAnimation=function(t){this.childAt(0).stopAnimation(null,t)},e.prototype.getSymbolType=function(){return this._symbolType},e.prototype.getSymbolPath=function(){return this.childAt(0)},e.prototype.highlight=function(){vc(this.childAt(0))},e.prototype.downplay=function(){yc(this.childAt(0))},e.prototype.setZ=function(t,e){var n=this.childAt(0);n.zlevel=t,n.z=e},e.prototype.setDraggable=function(t,e){var n=this.childAt(0);n.draggable=t,n.cursor=!e&&t?"move":n.cursor},e.prototype.updateData=function(t,n,i,r){this.silent=!1;var o=t.getItemVisual(n,"symbol")||"circle",a=t.hostModel,s=e.getSymbolSize(t,n),l=e.getSymbolZ2(t,n),u=o!==this._symbolType,c=r&&r.disableAnimation;if(u){var h=t.getItemVisual(n,"symbolKeepAspect");this._createSymbol(o,t,n,s,l,h)}else{(p=this.childAt(0)).silent=!1;var d={scaleX:s[0]/2,scaleY:s[1]/2};c?p.attr(d):rd(p,d,a,n),cd(p)}if(this._updateCommon(t,n,s,i,r),u){var p=this.childAt(0);if(!c){d={scaleX:this._sizeX,scaleY:this._sizeY,style:{opacity:p.style.opacity}};p.scaleX=p.scaleY=0,p.style.opacity=0,od(p,d,a,n)}}c&&this.childAt(0).stopAnimation("leave")},e.prototype._updateCommon=function(t,e,n,i,r){var o,a,s,l,u,c,h,d,p,f=this.childAt(0),g=t.hostModel;if(i&&(o=i.emphasisItemStyle,a=i.blurItemStyle,s=i.selectItemStyle,l=i.focus,u=i.blurScope,h=i.labelStatesModels,d=i.hoverScale,p=i.cursorStyle,c=i.emphasisDisabled),!i||t.hasItemOption){var v=i&&i.itemModel?i.itemModel:t.getItemModel(e),y=v.getModel("emphasis");o=y.getModel("itemStyle").getItemStyle(),s=v.getModel(["select","itemStyle"]).getItemStyle(),a=v.getModel(["blur","itemStyle"]).getItemStyle(),l=y.get("focus"),u=y.get("blurScope"),c=y.get("disabled"),h=ap(v),d=y.getShallow("scale"),p=v.getShallow("cursor")}var m=t.getItemVisual(e,"symbolRotate");f.attr("rotation",(m||0)*Math.PI/180||0);var x=bx(t.getItemVisual(e,"symbolOffset"),n);x&&(f.x=x[0],f.y=x[1]),p&&f.attr("cursor",p);var _=t.getItemVisual(e,"style"),b=_.fill;if(f instanceof eu){var w=f.style;f.useStyle(k({image:w.image,x:w.x,y:w.y,width:w.width,height:w.height},_))}else f.__isEmptyBrush?f.useStyle(k({},_)):f.useStyle(_),f.style.decal=null,f.setColor(b,r&&r.symbolInnerColor),f.style.strokeNoScale=!0;var S=t.getItemVisual(e,"liftZ"),M=this._z2;null!=S?null==M&&(this._z2=f.z2,f.z2+=S):null!=M&&(f.z2=M,this._z2=null);var I=r&&r.useNameLabel;op(f,h,{labelFetcher:g,labelDataIndex:e,defaultText:function(e){return I?t.getName(e):jT(t,e)},inheritColor:b,defaultOpacity:_.opacity}),this._sizeX=n[0]/2,this._sizeY=n[1]/2;var T=f.ensureState("emphasis");T.style=o,f.ensureState("select").style=s,f.ensureState("blur").style=a;var C=null==d||!0===d?Math.max(1.1,3/this._sizeY):isFinite(d)&&d>0?+d:1;T.scaleX=this._sizeX*C,T.scaleY=this._sizeY*C,this.setSymbolScale(1),kc(this,l,u,c)},e.prototype.setSymbolScale=function(t){this.scaleX=this.scaleY=t},e.prototype.fadeOut=function(t,e,n){var i=this.childAt(0),r=Iu(this).dataIndex,o=n&&n.animation;if(this.silent=i.silent=!0,n&&n.fadeLabel){var a=i.getTextContent();a&&sd(a,{style:{opacity:0}},e,{dataIndex:r,removeOpt:o,cb:function(){i.removeTextContent()}})}else i.removeTextContent();sd(i,{style:{opacity:0},scaleX:0,scaleY:0},e,{dataIndex:r,cb:t,removeOpt:o})},e.getSymbolSize=function(t,e){return _x(t.getItemVisual(e,"symbolSize"))},e.getSymbolZ2=function(t,e){return t.getItemVisual(e,"z2")},e}(vo);function $T(t,e){this.parent.drift(t,e)}function JT(t,e,n,i){return e&&!isNaN(e[0])&&!isNaN(e[1])&&!(i&&i.isIgnore&&i.isIgnore(n))&&!(i&&i.clipShape&&!i.clipShape.contain(e[0],e[1]))&&"none"!==t.getItemVisual(n,"symbol")}function QT(t){return null==t||$(t)||(t={isIgnore:t}),t||{}}function tC(t){var e=t.hostModel,n=e.getModel("emphasis");return{emphasisItemStyle:n.getModel("itemStyle").getItemStyle(),blurItemStyle:e.getModel(["blur","itemStyle"]).getItemStyle(),selectItemStyle:e.getModel(["select","itemStyle"]).getItemStyle(),focus:n.get("focus"),blurScope:n.get("blurScope"),emphasisDisabled:n.get("disabled"),hoverScale:n.get("scale"),labelStatesModels:ap(e),cursorStyle:e.get("cursor")}}function eC(t,e,n,i,r,o,a){var s=new t(e,n,i,r);return s.setPosition(o),e.setItemGraphicEl(n,s),a.add(s),s}var nC=function(){function t(t){this.group=new vo,this._SymbolCtor=t||KT}return t.prototype.updateData=function(t,e){this._progressiveEls=null,e=QT(e);var n=this.group,i=t.hostModel,r=this._data,o=this._SymbolCtor,a=e.disableAnimation,s=this._seriesScope=tC(t),l={disableAnimation:a},u=e.getSymbolPoint||function(e){return t.getItemLayout(e)};r||n.removeAll(),t.diff(r).add(function(i){var r=u(i);JT(t,r,i,e)&&eC(o,t,i,s,l,r,n)}).update(function(c,h){var d=r.getItemGraphicEl(h),p=u(c);if(JT(t,p,c,e)){var f=t.getItemVisual(c,"symbol")||"circle",g=d&&d.getSymbolType&&d.getSymbolType();if(!d||g&&g!==f)n.remove(d),(d=new o(t,c,s,l)).setPosition(p);else{d.updateData(t,c,s,l);var v={x:p[0],y:p[1]};a?d.attr(v):rd(d,v,i)}n.add(d),t.setItemGraphicEl(c,d)}else n.remove(d)}).remove(function(t){var e=r.getItemGraphicEl(t);e&&e.fadeOut(function(){n.remove(e)},i)}).execute(),this._getSymbolPoint=u,this._data=t},t.prototype.updateLayout=function(t){var e=this._data;if(e)for(var n=this,i=0,r=e.getStore().count();i0?n=i[0]:i[1]<0&&(n=i[1]);return n}(r,n),a=i.dim,s=r.dim,l=e.mapDimension(s),u=e.mapDimension(a),c="x"===s||"radius"===s?1:0,h=V(t.dimensions,function(t){return e.mapDimension(t)}),d=!1,p=e.getCalculationInfo("stackResultDimension");return qb(e,h[0])&&(d=!0,h[0]=p),qb(e,h[1])&&(d=!0,h[1]=p),{dataDimsForPoint:h,valueStart:o,valueAxisDim:s,baseAxisDim:a,stacked:!!d,valueDim:l,baseDim:u,baseDataOffset:c,stackedOverDimension:e.getCalculationInfo("stackedOverDimension")}}function rC(t,e,n,i){var r=NaN;t.stacked&&(r=n.get(n.getCalculationInfo("stackedOverDimension"),i)),isNaN(r)&&(r=t.valueStart);var o=t.baseDataOffset,a=[];return a[o]=n.get(t.baseDim,i),a[1-o]=r,e.dataToPoint(a)}function oC(t,e){return!isFinite(t)||!isFinite(e)}var aC=typeof Float32Array!==Cu?Float32Array:void 0,sC=typeof Float64Array!==Cu?Float64Array:void 0;function lC(t){return uC({ctor:aC},t).arr}function uC(t,e){var n=t.arr,i=t.ctor;if(e>qo&&(e=qo),!n||t.typed&&n.length=r||g<0)break;if(oC(y,m)){if(l){g+=o;continue}break}if(g===n)t[o>0?"moveTo":"lineTo"](y,m),h=y,d=m;else{var x=y-u,_=m-c;if(x*x+_*_<.5){g+=o;continue}if(a>0){for(var b=g+o,w=e[2*b],S=e[2*b+1];w===y&&S===m&&v=i||oC(w,S))p=y,f=m;else{T=w-u,C=S-c;var k=y-u,L=w-y,P=m-c,O=S-m,R=void 0,N=void 0;if("x"===s){var E=T>0?1:-1;p=y-E*(R=Math.abs(k))*a,f=m,D=y+E*(N=Math.abs(L))*a,A=m}else if("y"===s){var z=C>0?1:-1;p=y,f=m-z*(R=Math.abs(P))*a,D=y,A=m+z*(N=Math.abs(O))*a}else R=Math.sqrt(k*k+P*P),p=y-T*a*(1-(I=(N=Math.sqrt(L*L+O*O))/(N+R))),f=m-C*a*(1-I),A=m+C*a*I,D=cC(D=y+T*a*I,hC(w,y)),A=cC(A,hC(S,m)),D=hC(D,cC(w,y)),f=m-(C=(A=hC(A,cC(S,m)))-m)*R/N,p=cC(p=y-(T=D-y)*R/N,hC(u,y)),f=cC(f,hC(c,m)),D=y+(T=y-(p=hC(p,cC(u,y))))*N/R,A=m+(C=m-(f=hC(f,cC(c,m))))*N/R}t.bezierCurveTo(h,d,p,f,y,m),h=D,d=A}else t.lineTo(y,m)}u=y,c=m,g+=o}return v}var pC=function(){this.smooth=0,this.smoothConstraint=!0},fC=function(t){function e(e){var n=t.call(this,e)||this;return n.type="ec-polyline",n}return n(e,t),e.prototype.getDefaultStyle=function(){return{stroke:ug.color.neutral99,fill:null}},e.prototype.getDefaultShape=function(){return new pC},e.prototype.buildPath=function(t,e){var n=e.points,i=0,r=n.length/2;if(e.connectNulls){for(;r>0&&oC(n[2*r-2],n[2*r-1]);r--);for(;i=0){var v=a?(c-i)*g+i:(u-n)*g+n;return a?[t,v]:[v,t]}n=u,i=c;break;case o.C:u=r[l++],c=r[l++],h=r[l++],d=r[l++],p=r[l++],f=r[l++];var y=a?Gn(n,u,h,p,t,s):Gn(i,c,d,f,t,s);if(y>0)for(var m=0;m=0){v=a?Bn(i,c,d,f,x):Bn(n,u,h,p,x);return a?[t,v]:[v,t]}}n=p,i=f}}},e}(Kl),gC=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e}(pC),vC=function(t){function e(e){var n=t.call(this,e)||this;return n.type="ec-polygon",n}return n(e,t),e.prototype.getDefaultShape=function(){return new gC},e.prototype.buildPath=function(t,e){var n=e.points,i=e.stackedOnPoints,r=0,o=n.length/2,a=e.smoothMonotone;if(e.connectNulls){for(;o>0&&oC(n[2*o-2],n[2*o-1]);o--);for(;r=0,o=t.fill||ug.color.neutral99;DC(i,e);var a=null==i.textFill;return r?a&&(i.textFill=n.insideFill||ug.color.neutral00,!i.textStroke&&n.insideStroke&&(i.textStroke=n.insideStroke),!i.textStroke&&(i.textStroke=o),null==i.textStrokeWidth&&(i.textStrokeWidth=2)):(a&&(i.textFill=t.fill||n.outsideFill||ug.color.neutral00),!i.textStroke&&n.outsideStroke&&(i.textStroke=n.outsideStroke)),i.text=e.text,i.rich=e.rich,B(e.rich,function(t){DC(t,t)}),i}function DC(t,e){e&&(wt(e,"fill")&&(t.textFill=e.fill),wt(e,"stroke")&&(t.textStroke=e.fill),wt(e,"lineWidth")&&(t.textStrokeWidth=e.lineWidth),wt(e,"font")&&(t.font=e.font),wt(e,"fontStyle")&&(t.fontStyle=e.fontStyle),wt(e,"fontWeight")&&(t.fontWeight=e.fontWeight),wt(e,"fontSize")&&(t.fontSize=e.fontSize),wt(e,"fontFamily")&&(t.fontFamily=e.fontFamily),wt(e,"align")&&(t.textAlign=e.align),wt(e,"verticalAlign")&&(t.textVerticalAlign=e.verticalAlign),wt(e,"lineHeight")&&(t.textLineHeight=e.lineHeight),wt(e,"width")&&(t.textWidth=e.width),wt(e,"height")&&(t.textHeight=e.height),wt(e,"backgroundColor")&&(t.textBackgroundColor=e.backgroundColor),wt(e,"padding")&&(t.textPadding=e.padding),wt(e,"borderColor")&&(t.textBorderColor=e.borderColor),wt(e,"borderWidth")&&(t.textBorderWidth=e.borderWidth),wt(e,"borderRadius")&&(t.textBorderRadius=e.borderRadius),wt(e,"shadowColor")&&(t.textBoxShadowColor=e.shadowColor),wt(e,"shadowBlur")&&(t.textBoxShadowBlur=e.shadowBlur),wt(e,"shadowOffsetX")&&(t.textBoxShadowOffsetX=e.shadowOffsetX),wt(e,"shadowOffsetY")&&(t.textBoxShadowOffsetY=e.shadowOffsetY),wt(e,"textShadowColor")&&(t.textShadowColor=e.textShadowColor),wt(e,"textShadowBlur")&&(t.textShadowBlur=e.textShadowBlur),wt(e,"textShadowOffsetX")&&(t.textShadowOffsetX=e.textShadowOffsetX),wt(e,"textShadowOffsetY")&&(t.textShadowOffsetY=e.textShadowOffsetY))}function AC(t,e){if(t.length===e.length){for(var n=0;n=0;a--){var s=t.getDimensionInfo(i[a].dimension);if("x"===(r=s&&s.coordDim)||"y"===r){o=i[a];break}}if(o){var l=e.getAxis(r),u=V(o.stops,function(t){return{coord:l.toGlobalCoord(l.dataToCoord(t.value)),color:t.color}}),c=u.length,h=o.outerColors.slice();c&&u[0].coord>u[c-1].coord&&(u.reverse(),h.reverse());var d=function(t,e){var n,i,r=[],o=t.length;function a(t,e,n){var i=t.coord;return{coord:n,color:_i((n-i)/(e.coord-i),[t.color,e.color])}}for(var s=0;se){i?r.push(a(i,l,e)):n&&r.push(a(n,l,0),a(n,l,e));break}n&&(r.push(a(n,l,0)),n=null),r.push(l),i=l}}return r}(u,"x"===r?n.getWidth():n.getHeight()),p=d.length;if(!p&&c)return u[0].coord<0?h[1]?h[1]:u[c-1].color:h[0]?h[0]:u[0].color;var f=d[0].coord-10,g=d[p-1].coord+10,v=g-f;if(v<.001)return"transparent";B(d,function(t){t.offset=(t.coord-f)/v}),d.push({offset:p?d[p-1].offset:.5,color:h[1]||"transparent"}),d.unshift({offset:p?d[0].offset:.5,color:h[0]||"transparent"});var y=new Hh(0,0,0,0,d,!0);return y[r]=f,y[r+"2"]=g,y}}}function NC(t,e,n){var i=t.get("showAllSymbol"),r="auto"===i;if(!i||r){var o=n.getAxesByScale("ordinal")[0];if(o&&(!r||!function(t,e){var n=t.getExtent(),i=Math.abs(n[1]-n[0])/t.scale.count();isNaN(i)&&(i=0);for(var r=e.count(),o=Math.max(1,Math.round(r/5)),a=0;ai)return!1;return!0}(o,e))){var a=e.mapDimension(o.dim),s={};return B(o.getViewLabels(),function(t){t.tick.offInterval||(s[tS(o.scale,t.tick)]=1)}),function(t){return!s.hasOwnProperty(e.get(a,t))}}}}function EC(t,e){return[t[2*e],t[2*e+1]]}function zC(t){if(t.get(["endLabel","show"]))return!0;for(var e=0;e0&&"bolder"===t.get(["emphasis","lineStyle","width"]))&&(d.getState("emphasis").style.lineWidth=+d.style.lineWidth+1);Iu(d).seriesIndex=t.seriesIndex,kc(d,A,k,L);var O=PC(t.get("smooth")),R=t.get("smoothMonotone");if(d.setShape({smooth:O,smoothMonotone:R,connectNulls:b}),p){var N=o.getCalculationInfo("stackedOnSeries"),E=0;p.useStyle(P(s.getAreaStyle(),{fill:T,opacity:.7,lineJoin:"bevel",decal:o.getVisual("style").decal})),N&&(E=PC(N.get("smooth"))),p.setShape({smooth:O,stackedOnSmooth:E,smoothMonotone:R,connectNulls:b}),Rc(p,t,"areaStyle"),Iu(p).seriesIndex=t.seriesIndex,kc(p,A,k,L)}var z=this._changePolyState;o.eachItemGraphicEl(function(t){t&&(t.onHoverStateChange=z)}),this._polyline.onHoverStateChange=z,this._data=o,this._coordSys=i,this._stackedOnPoints=x,this._points=l,this._step=I,this._valueOrigin=y;var B=t.get("triggerEvent"),V=t.get("triggerLineEvent");var G=!0===V||!0===B||"line"===B,F=!0===V||!0===B||"area"===B;this.packEventData(t,d,G),p&&this.packEventData(t,p,F)},e.prototype.packEventData=function(t,e,n){Iu(e).eventData=n?{componentType:"series",componentSubType:"line",componentIndex:t.componentIndex,seriesIndex:t.seriesIndex,seriesName:t.name,seriesType:"line",selfType:e===this._polygon?"area":"line"}:null},e.prototype.highlight=function(t,e,n,i){var r=t.getData(),o=La(r,i);if(this._changePolyState("emphasis"),!(o instanceof Array)&&null!=o&&o>=0){var a=r.getLayout("points"),s=r.getItemGraphicEl(o);if(!s){var l=a[2*o],u=a[2*o+1];if(oC(l,u))return;if(this._clipShapeForSymbol&&!this._clipShapeForSymbol.contain(l,u))return;var c=t.get("zlevel")||0,h=t.get("z")||0;(s=new KT(r,o)).x=l,s.y=u,s.setZ(c,h);var d=s.getSymbolPath().getTextContent();d&&(d.zlevel=c,d.z=h,d.z2=this._polyline.z2+1),s.__temp=!0,r.setItemGraphicEl(o,s),s.stopSymbolAnimation(!0),this.group.add(s)}s.highlight()}else tm.prototype.highlight.call(this,t,e,n,i)},e.prototype.downplay=function(t,e,n,i){var r=t.getData(),o=La(r,i);if(this._changePolyState("normal"),null!=o&&o>=0){var a=r.getItemGraphicEl(o);a&&(a.__temp?(r.setItemGraphicEl(o,null),this.group.remove(a)):a.downplay())}else tm.prototype.downplay.call(this,t,e,n,i)},e.prototype._changePolyState=function(t){var e=this._polygon;hc(this._polyline,t),e&&hc(e,t)},e.prototype._newPolyline=function(t){var e=this._polyline;return e&&this._lineGroup.remove(e),e=new fC({shape:{points:t},segmentIgnoreThreshold:2,z2:10}),this._lineGroup.add(e),this._polyline=e,e},e.prototype._newPolygon=function(t,e){var n=this._polygon;return n&&this._lineGroup.remove(n),n=new vC({shape:{points:t,stackedOnPoints:e},segmentIgnoreThreshold:2}),this._lineGroup.add(n),this._polygon=n,n},e.prototype._initSymbolLabelAnimation=function(t,e,n){var i,r,o=e.getBaseAxis(),a=o.inverse;"cartesian2d"===e.type?(i=o.isHorizontal(),r=!1):"polar"===e.type&&(i="angle"===o.dim,r=!0);var s=t.hostModel,l=s.get("animationDuration");Z(l)&&(l=l(null));var u=s.get("animationDelay")||0,c=Z(u)?u(null):u;t.eachItemGraphicEl(function(t,o){var s=t;if(s){var h=[t.x,t.y],d=void 0,p=void 0,f=void 0;if(n)if(r){var g=n,v=e.pointToCoord(h);i?(d=g.startAngle,p=g.endAngle,f=-v[1]/180*Math.PI):(d=g.r0,p=g.r,f=v[0])}else{var y=n;i?(d=y.x,p=y.x+y.width,f=t.x):(d=y.y+y.height,p=y.y,f=t.y)}var m=p===d?0:(f-d)/(p-d);a&&(m=1-m);var x=Z(u)?u(o):l*m+c,_=s.getSymbolPath(),b=_.getTextContent();s.attr({scaleX:0,scaleY:0}),s.animateTo({scaleX:1,scaleY:1},{duration:200,setToFinal:!0,delay:x}),b&&b.animateFrom({style:{opacity:0}},{duration:300,delay:x}),_.disableLabelAnimation=!0}})},e.prototype._initOrUpdateEndLabel=function(t,e,n){var i=t.getModel("endLabel");if(zC(t)){var r=t.getData(),o=this._polyline,a=r.getLayout("points");if(!a)return o.removeTextContent(),void(this._endLabel=null);var s=this._endLabel;s||((s=this._endLabel=new du({z2:200})).ignoreClip=!0,o.setTextContent(this._endLabel),o.disableLabelAnimation=!0);var l=function(t){for(var e=t.length/2;e>0&&oC(t[2*e-2],t[2*e-1]);e--);return e-1}(a);l>=0&&(op(o,ap(t,"endLabel"),{inheritColor:n,labelFetcher:t,labelDataIndex:l,defaultText:function(t,e,n){return null!=n?qT(r,n):jT(r,t)},enableTextSetter:!0},function(t,e){var n=e.getBaseAxis(),i=n.isHorizontal(),r=n.inverse,o=i?r?"right":"left":"center",a=i?"middle":r?"top":"bottom";return{normal:{align:t.get("align")||o,verticalAlign:t.get("verticalAlign")||a}}}(i,e)),o.textConfig.position=null)}else this._endLabel&&(this._polyline.removeTextContent(),this._endLabel=null)},e.prototype._endLabelOnDuring=function(t,e,n,i,r,o,a){var s=this._endLabel,l=this._polyline;if(s){t<1&&null==i.originalX&&(i.originalX=s.x,i.originalY=s.y);var u=n.getLayout("points"),c=n.hostModel,h=c.get("connectNulls"),d=o.get("precision"),p=o.get("distance")||0,f=a.getBaseAxis(),g=f.isHorizontal(),v=f.inverse,y=e.shape,m=v?g?y.x:y.y+y.height:g?y.x+y.width:y.y,x=(g?p:0)*(v?-1:1),_=(g?0:-p)*(v?-1:1),b=g?"x":"y",w=function(t,e,n){for(var i,r,o=t.length/2,a="x"===n?0:1,s=0,l=-1,u=0;u=e||i>=e&&r<=e){l=u;break}s=u,i=r}else i=r;return{range:[s,l],t:(e-i)/(r-i)}}(u,m,b),S=w.range,M=S[1]-S[0],I=void 0;if(M>=1){if(M>1&&!h){var T=EC(u,S[0]);s.attr({x:T[0]+x,y:T[1]+_}),r&&(I=c.getRawValue(S[0]))}else{(T=l.getPointOn(m,b))&&s.attr({x:T[0]+x,y:T[1]+_});var C=c.getRawValue(S[0]),D=c.getRawValue(S[1]);r&&(I=Ha(n,d,C,D,w.t))}i.lastFrameIndex=S[0]}else{var A=1===t||i.lastFrameIndex>0?S[0]:0;T=EC(u,A);r&&(I=c.getRawValue(A)),s.attr({x:T[0]+x,y:T[1]+_})}if(r){var k=fp(s);"function"==typeof k.setLabelText&&k.setLabelText(I)}}},e.prototype._doUpdateAnimation=function(t,e,n,i,r,o,a){var s=this._polyline,l=this._polygon,u=t.hostModel,c=function(t,e,n,i,r,o,a){for(var s=function(t,e){var n=[];return e.diff(t).add(function(t){n.push({cmd:"+",idx:t})}).update(function(t,e){n.push({cmd:"=",idx:e,idx1:t})}).remove(function(t){n.push({cmd:"-",idx:t})}).execute(),n}(t,e),l=[],u=[],c=[],h=[],d=[],p=[],f=[],g=iC(r,e,a),v=t.getLayout("points")||[],y=e.getLayout("points")||[],m=0;m3e3||l&&LC(d,f)>3e3)return s.stopAnimation(),s.setShape({points:p}),void(l&&(l.stopAnimation(),l.setShape({points:p,stackedOnPoints:f})));s.shape.__points=c.current,s.shape.points=h;var g={shape:{points:p}};c.current!==h&&(g.shape.__points=c.next),s.stopAnimation(),rd(s,g,u),l&&(l.setShape({points:h,stackedOnPoints:d}),l.stopAnimation(),rd(l,{shape:{stackedOnPoints:f}},u),s.shape.points!==l.shape.points&&(l.shape.points=s.shape.points));for(var v=[],y=c.status,m=0;me&&(e=t[n]);return isFinite(e)?e:NaN},min:function(t){for(var e=1/0,n=0;n10&&"cartesian2d"===o.type&&r){var s=o.getBaseAxis(),l=o.getOtherAxis(s),u=s.getExtent(),c=n.getDevicePixelRatio(),h=Math.abs(u[1]-u[0])*(c||1),d=Math.round(a/h);if(isFinite(d)&&d>1){"lttb"===r?t.setData(i.lttbDownSample(i.mapDimension(l.dim),1/d)):"minmax"===r&&t.setData(i.minmaxDownSample(i.mapDimension(l.dim),1/d));var p=void 0;j(r)?p=FC[r]:Z(r)&&(p=r),p&&t.setData(i.downSample(i.mapDimension(l.dim),1/d,p,HC))}}}}}var UC=function(t){function e(e,n,i,r,o){var a=t.call(this,e,n,i)||this;return a.index=0,a.type=r||"value",a.position=o||"bottom",a}return n(e,t),e.prototype.isHorizontal=function(){var t=this.position;return"top"===t||"bottom"===t},e.prototype.getGlobalExtent=function(t){var e=this.getExtent();return e[0]=this.toGlobalCoord(e[0]),e[1]=this.toGlobalCoord(e[1]),t&&e[0]>e[1]&&e.reverse(),e},e.prototype.pointToData=function(t,e){return this.coordToData(this.toLocalCoord(t["x"===this.dim?0:1]),e)},e.prototype.setCategorySortInfo=function(t){if("category"!==this.type)return!1;this.model.option.categorySortInfo=t,this.scale.setSortInfo(t)},e}(SM),YC=null;function XC(){return YC}var ZC="expandAxisBreak",jC="collapseAxisBreak",qC="toggleAxisBreak",KC="axisbreakchanged",$C={type:ZC,event:KC,update:"update",refineEvent:tD},JC={type:jC,event:KC,update:"update",refineEvent:tD},QC={type:qC,event:KC,update:"update",refineEvent:tD};function tD(t,e,n,i){var r=[];return B(t,function(t){r=r.concat(t.eventBreaks)}),{eventContent:{breaks:r}}}var eD=Math.PI,nD=[[1,2,1,2],[5,3,5,3],[8,3,8,3]],iD=[[0,1,0,1],[0,3,0,3],[0,3,0,3]],rD=Pa(),oD=Pa(),aD=function(){function t(t){this.recordMap={},this.resolveAxisNameOverlap=t}return t.prototype.ensureRecord=function(t){var e=t.axis.dim,n=t.componentIndex,i=this.recordMap,r=i[e]||(i[e]=[]);return r[n]||(r[n]={ready:{}})},t}();var sD=[1,0,0,1,0,0],lD=new Xe(0,0,0,0),uD=function(t,e,n,i,r,o){if(Kw(t.nameLocation)){var a=o.stOccupiedRect;a&&cD(function(t,e,n){return t.transform=Xd(t.transform,n),t.localRect=Yd(t.localRect,e),t.rect=Yd(t.rect,e),n&&t.rect.applyTransform(n),t.axisAligned=Wd(n),t.obb=void 0,(t.label=t.label||{}).ignore=!1,t}({},a,o.transGroup.transform),i,r)}else hD(o.labelInfoList,o.dirVec,i,r)};function cD(t,e,n){var i=new Pe;aI(t,e,i,{direction:Math.atan2(n.y,n.x),bidirectional:!1,touchThreshold:.05})&&eI(e,i)}function hD(t,e,n,i){for(var r=Pe.dot(i,e)>=0,o=0,a=t.length;o0?"top":"bottom",i="center"):$o(o-eD)?(r=n>0?"bottom":"top",i="center"):(r="middle",i=o>0&&o0?"right":"left":n>0?"left":"right"),{rotation:o,textAlign:i,textVerticalAlign:r}},t.makeAxisEventDataBase=function(t){var e={componentType:t.mainType,componentIndex:t.componentIndex};return e[t.mainType+"Index"]=t.componentIndex,e},t.isLabelSilent=function(t){var e=t.get("tooltip");return t.get("silent")||!(t.get("triggerEvent")||e&&e.show)},t}(),pD=["axisLine","axisTickLabelEstimate","axisTickLabelDetermine","axisName"],fD={axisLine:function(t,e,n,i,r,o,a){var s=i.get(["axisLine","show"]);if("auto"===s&&(s=!0,null!=t.raw.axisLineAutoShow&&(s=!!t.raw.axisLineAutoShow)),s){var l=i.axis.getExtent(),u=o.transform,c=[l[0],0],h=[l[1],0],d=c[0]>h[0];u&&(Yt(c,c,u),Yt(h,h,u));var p=k({lineCap:"round"},i.getModel(["axisLine","lineStyle"]).getLineStyle()),f={strokeContainThreshold:t.raw.strokeContainThreshold||5,silent:!0,z2:1,style:p};if(i.get(["axisLine","breakLine"])&&Xp(i.axis.scale))XC().buildAxisBreakLine(i,r,o,f);else{var g=new Oh(k({shape:{x1:c[0],y1:c[1],x2:h[0],y2:h[1]}},f));Md(g.shape,g.style.lineWidth),g.anid="line",r.add(g)}var v=i.get(["axisLine","symbol"]);if(null!=v){var y=i.get(["axisLine","symbolSize"]);j(v)&&(v=[v,v]),(j(y)||K(y))&&(y=[y,y]);var m=bx(i.get(["axisLine","symbolOffset"])||0,y),x=y[0],_=y[1];B([{rotate:t.rotation+Math.PI/2,offset:m[0],r:0},{rotate:t.rotation-Math.PI/2,offset:m[1],r:Math.sqrt((c[0]-h[0])*(c[0]-h[0])+(c[1]-h[1])*(c[1]-h[1]))}],function(e,n){if("none"!==v[n]&&null!=v[n]){var i=xx(v[n],-x/2,-_/2,x,_,p.stroke,!0),o=e.r+e.offset,a=d?h:c;i.attr({rotation:e.rotate,x:a[0]+o*Math.cos(t.rotation),y:a[1]-o*Math.sin(t.rotation),silent:!0,z2:11}),r.add(i)}})}}},axisTickLabelEstimate:function(t,e,n,i,r,o,a,s){mD(e,r,s)&&gD(t,e,n,i,r,o,a,aM)},axisTickLabelDetermine:function(t,e,n,i,r,o,a,s){mD(e,r,s)&&gD(t,e,n,i,r,o,a,sM);var l=function(t,e,n,i){var r=i.axis,o=i.getModel("axisTick"),a=o.get("show");"auto"===a&&(a=!0,null!=t.raw.axisTickAutoShow&&(a=!!t.raw.axisTickAutoShow));if(!a||r.scale.isBlank())return[];for(var s=o.getModel("lineStyle"),l=t.tickDirection*o.get("length"),u=yD(r.getTicksCoords(),n.transform,l,P(s.getLineStyle(),{stroke:i.get(["axisLine","lineStyle","color"])}),"ticks"),c=0;ci[1],l="start"===e&&!s||"start"!==e&&s;$o(a-eD/2)?(o=l?"bottom":"top",r="center"):$o(a-1.5*eD)?(o=l?"top":"bottom",r="center"):(o="middle",r=a<1.5*eD&&a>eD/2?l?"left":"right":l?"right":"left");return{rotation:a,textAlign:r,textVerticalAlign:o}}(t.rotation,c,b||0,f),null!=(_=t.raw.axisNameAvailableWidth)&&(_=Math.abs(_/Math.sin(x.rotation)),!isFinite(_)&&(_=null)));var w=d.getFont(),S=i.get("nameTruncate",!0)||{},M=S.ellipsis,I=ot(t.raw.nameTruncateMaxWidth,S.maxWidth,_),T=s.nameMarginLevel||0,C=new du({x:v.x,y:v.y,rotation:x.rotation,silent:dD.isLabelSilent(i),style:sp(d,{text:u,font:w,overflow:"truncate",width:I,ellipsis:M,fill:d.getTextColor()||i.get(["axisLine","lineStyle","color"]),align:d.get("align")||x.textAlign,verticalAlign:d.get("verticalAlign")||x.textVerticalAlign}),z2:1});if(Gd({el:C,componentModel:i,itemName:u}),C.__fullText=u,C.anid="name",i.get("triggerEvent")){var D=dD.makeAxisEventDataBase(i);D.targetType="axisName",D.name=u,Iu(C).eventData=D}o.add(C),C.updateTransform(),e.nameEl=C;var A=l.nameLayout=JM({label:C,priority:C.z2,defaultAttr:{ignore:C.ignore},marginDefault:Kw(c)?nD[T]:iD[T]});if(l.nameLocation=c,r.add(C),C.decomposeTransform(),t.shouldNameMoveOverlap&&A){var k=n.ensureRecord(i);0,n.resolveAxisNameOverlap(t,n,i,A,y,k)}}}};function gD(t,e,n,i,r,o,a,s){xD(e)||function(t,e,n,i,r,o){var a=r.axis,s=ot(t.raw.axisLabelShow,r.get(["axisLabel","show"])),l=new vo;n.add(l);var u=lM(i);if(!s||a.scale.isBlank())return void _D(e,[],l,u);var c=r.getModel("axisLabel"),h=a.getViewLabels(u),d=(ot(t.raw.labelRotate,c.get("rotate"))||0)*eD/180,p=dD.innerTextLayout(t.rotation,d,t.labelDirection),f=r.getCategories&&r.getCategories(!0),g=[],v=r.get("triggerEvent"),y=1/0,m=-1/0;B(h,function(t,e){var n,i=t.tick,s=t.formattedLabel,u=t.rawLabel,d=c,x=tS(a.scale,i);if(f&&f[x]){var _=f[x];$(_)&&_.textStyle&&(d=new kp(_.textStyle,c,r.ecModel))}var b=d.getTextColor()||r.get(["axisLine","lineStyle","color"]),w=d.getShallow("align",!0)||p.textAlign,S=at(d.getShallow("alignMinLabel",!0),w),M=at(d.getShallow("alignMaxLabel",!0),w),I=d.getShallow("verticalAlign",!0)||d.getShallow("baseline",!0)||p.textVerticalAlign,T=at(d.getShallow("verticalAlignMinLabel",!0),I),C=at(d.getShallow("verticalAlignMaxLabel",!0),I),D=10+((null===(n=i.time)||void 0===n?void 0:n.level)||0);y=Math.min(y,D),m=Math.max(m,D);var A=new du({x:0,y:0,rotation:0,silent:dD.isLabelSilent(r),z2:D,style:sp(d,{text:s,align:0===e?S:e===h.length-1?M:w,verticalAlign:0===e?T:e===h.length-1?C:I,fill:Z(b)?b("category"===a.type?u:"value"===a.type?x+"":x,e):b})});A.anid="label_"+x;var k=rD(A);if(k.labelInfo=t,k.layoutRotation=p.rotation,Gd({el:A,componentModel:r,itemName:s,formatterParamsExtra:{isTruncated:function(){return A.isTruncated},value:u,tickIndex:e}}),v){var L=dD.makeAxisEventDataBase(r);L.targetType="axisLabel",L.value=u,L.tickIndex=e;var P=t.tick.break;if(P){var O=P.parsedBreak;L.break={start:O.vmin,end:O.vmax}}"category"===a.type&&(L.dataIndex=x),Iu(A).eventData=L,P&&function(t,e,n,i){n.on("click",function(n){var r={type:ZC,breaks:[{start:i.parsedBreak.breakOption.start,end:i.parsedBreak.breakOption.end}]};r[t.axis.dim+"AxisIndex"]=t.componentIndex,e.dispatchAction(r)})}(r,o,A,P)}g.push(A),l.add(A)});var x=V(g,function(t){return{label:t,priority:rD(t).labelInfo.tick.break?t.z2+(m-y+1):t.z2,defaultAttr:{ignore:t.ignore}}});_D(e,x,l,u)}(t,e,r,s,i,a);var l=e.labelLayoutList;!function(t,e,n,i){var r=e.get(["axisLabel","margin"]);B(n,function(n,o){var a=JM(n);if(a){var s=a.label,l=rD(s);a.suggestIgnore=s.ignore,s.ignore=!1,Hr(bD,wD);var u=e.axis;bD.x=u.dataToCoord(tS(u.scale,l.labelInfo.tick)),bD.y=t.labelOffset+t.labelDirection*r,bD.rotation=l.layoutRotation,i.add(bD),bD.updateTransform(),i.remove(bD),bD.decomposeTransform(),Hr(s,bD),s.markRedraw(),KM(a,!0),JM(a)}})}(t,i,l,o),function(t,e,n){var i=Wp();if(!i)return;var r=i.retrieveAxisBreakPairs(n,function(t){return t&&rD(t.label).labelInfo.tick.break},!0),o=t.get(["breakLabelLayout","moveOverlap"],!0);!0!==o&&"auto"!==o||B(r,function(i){XC().adjustBreakLabelPair(t.axis.inverse,e,[JM(n[i[0]]),JM(n[i[1]])])})}(i,t.rotation,l);var u=t.optionHideOverlap;!function(t,e,n){var i=t.axis,r=t.get(["axisLabel","customValues"]);if(function(t){return"category"===t.type&&0===qw(t.getLabelModel())}(i))return;function o(t,o,a){var s=JM(e[o]),l=JM(e[a]),u=i.scale;if(s&&l){if(null==t){if(!n&&r)return;var c=rD(s.label).labelInfo.tick;if(fw(u)&&c.notNice||vw(u)&&c.offInterval)return void vD(s.label)}if(!1===t||s.suggestIgnore)vD(s.label);else if(l.suggestIgnore)vD(l.label);else{var h=.1;if(!n){var d=[0,0,0,0];s=nI({marginForce:d},s),l=nI({marginForce:d},l)}aI(s,l,null,{touchThreshold:h})&&vD(t?l.label:s.label)}}}var a=t.get(["axisLabel","showMinLabel"]),s=t.get(["axisLabel","showMaxLabel"]),l=e.length;o(a,0,1),o(s,l-1,l-2)}(i,l,u),u&&oI(F(l,function(t){return t&&!t.label.ignore})),function(t,e,n,i){var r,o=n.axis,a=e.ensureRecord(n),s=[],l=SD(t.axisName)&&Kw(t.nameLocation);B(i,function(t){var e=JM(t);if(e&&!e.label.ignore){s.push(e);var n=a.transGroup;l&&(n.transform?Ae(sD,n.transform):Se(sD),e.transform&&Ie(sD,sD,e.transform),Xe.copy(lD,e.localRect),lD.applyTransform(sD),r?r.union(lD):Xe.copy(r=new Xe(0,0,0,0),lD))}});var u=Math.abs(a.dirVec.x)>.1?"x":"y",c=a.transGroup[u];if(s.sort(function(t,e){return Math.abs(t.label[u]-c)-Math.abs(e.label[u]-c)}),l&&r){var h=o.getExtent(),d=Math.min(h[0],h[1]),p=Math.max(h[0],h[1])-d;r.union(new Xe(d,0,p,1))}a.stOccupiedRect=r,a.labelInfoList=s}(t,n,i,l)}function vD(t){t&&(t.ignore=!0)}function yD(t,e,n,i,r){for(var o=[],a=[],s=[],l=0;l=0&&n(i,t,e.getStore())})}var p=0;if(d(function(t,e,n){r.set(e.uid,1),o&&o.hasKey(e.uid)||(i=!0),p+=n.count()}),o&&o.keys().length===r.keys().length||(i=!0),i||null==a){uC(CD,p);var f=0;d(function(t,e,n){for(var i=0,r=n.count();i0&&m0?-2:-1,n.serUids=r}else e.liPosMinGap=a}var CD=uC({ctor:sC},50);function DD(t){return function(e,n){var i=bM(e,{fromStat:{key:t}});if(ca(i.w2))return[-i.w2/2,i.w2/2]}}function AD(t){return t+rS}function kD(t,e){return t+rS+e}function LD(t){return ID(),{liPosMinGap:!vw(t.scale)}}var PD="bar",OD="pictorialBar";function RD(t,e,n,i){xS(t,{key:e,seriesType:n,coordSysType:i,getMetrics:LD})}function ND(t){var e=t.scale.rawExtentInfo.makeRenderInfo().startValue;return e}var ED={left:0,right:0,top:0,bottom:0},zD=["25%","25%"],BD="cartesian2d",VD=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.mergeDefaultAndTheme=function(e,n){var i=og(e.outerBounds);t.prototype.mergeDefaultAndTheme.apply(this,arguments),i&&e.outerBounds&&rg(e.outerBounds,i)},e.prototype.mergeOption=function(e,n){t.prototype.mergeOption.apply(this,arguments),this.option.outerBounds&&e.outerBounds&&rg(this.option.outerBounds,e.outerBounds)},e.type="grid",e.dependencies=["xAxis","yAxis"],e.layoutMode="box",e.defaultOption={show:!1,z:0,left:"15%",top:65,right:"10%",bottom:80,containLabel:!1,outerBoundsMode:"auto",outerBounds:ED,outerBoundsContain:"all",outerBoundsClampWidth:zD[0],outerBoundsClampHeight:zD[1],backgroundColor:ug.color.transparent,borderWidth:1,borderColor:ug.color.neutral30},e}(lg),GD=Ja(),FD="__ec_stack_";function HD(t){return t.get("stack")||FD+t.seriesIndex}function WD(t,e){var n=function(t,e){var n=kD(e,BD),i=[],r=bM(t,{fromStat:{key:n},min:1});return hS(t,n,function(t){i.push({barWidth:Vo(t.get("barWidth"),r.w),barMaxWidth:Vo(t.get("barMaxWidth"),r.w),barMinWidth:Vo(t.get("barMinWidth")||(ZD(t)?.5:1),r.w),barGap:t.get("barGap"),barCategoryGap:t.get("barCategoryGap"),defaultBarGap:t.get("defaultBarGap"),stackId:HD(t)})}),{bandWidthResult:r,seriesInfo:i}}(t,e);return n.columnMap=UD(n),n}function UD(t){var e,n,i=t.bandWidthResult.w,r=i,o=0,a=[],s={};B(t.seriesInfo,function(t,i){i||(n=t.defaultBarGap||0);var l=t.stackId;wt(s,l)||o++;var u=s[l];u||(u=s[l]={width:0,maxWidth:0},a.push(l));var c=t.barWidth;c&&!u.width&&(u.width=c,c=Co(r,c),r-=c);var h=t.barMaxWidth;h&&(u.maxWidth=h);var d=t.barMinWidth;d&&(u.minWidth=d);var p=t.barGap;null!=p&&(n=p);var f=t.barCategoryGap;null!=f&&(e=f)}),null==e&&(e=Do(35-4*a.length,15)+"%");var l=Vo(e,i),u=Vo(n,1),c=(r-l)/(o+(o-1)*u);c=Do(c,0),B(a,function(t){var e=s[t],n=e.maxWidth,i=e.minWidth;if(e.width){a=e.width;n&&(a=Co(a,n)),i&&(a=Do(a,i)),e.width=a,r-=a+u*a,o--}else{var a=c;n&&na&&(a=i),a!==c&&(e.width=a,r-=a+u*a,o--)}}),c=Do(c=(r-l)/(o+(o-1)*u),0);var h,d=0;B(a,function(t){var e=s[t];e.width||(e.width=c),h=e,d+=e.width*(1+u)}),h&&(d-=h.width*u);var p={},f=-d/2;return B(a,function(t){var e=s[t];p[t]=p[t]||{bandWidth:i,offset:f,width:e.width},f+=e.width*(1+u)}),p}function YD(t){return{seriesType:t,overallReset:function(e){var n=kD(t,BD);pS(e,n,function(e){var i=WD(e,t);hS(e,n,function(t){var e=i.columnMap[HD(t)];t.getData().setLayout({bandWidth:e.bandWidth,offset:e.offset,size:e.width})})})}}}function XD(t){return{seriesType:t,plan:$y(),reset:function(t){if(function(t){return t.coordinateSystem&&"cartesian2d"===t.coordinateSystem.type}(t)){var e=t.getData(),n=t.coordinateSystem,i=n.getBaseAxis(),r=n.getOtherAxis(i),o=e.getDimensionIndex(e.mapDimension(r.dim)),a=e.getDimensionIndex(e.mapDimension(i.dim)),s=t.get("showBackground",!0),l=e.mapDimension(r.dim),u=e.getCalculationInfo("stackResultDimension"),c=qb(e,l)&&!!e.getCalculationInfo("stackedOnSeries"),h=r.isHorizontal(),d=r.toGlobalCoord(r.dataToCoord(ND(r))),p=ZD(t),f=t.get("barMinHeight")||0,g=u&&e.getDimensionIndex(u),v=e.getLayout("size"),y=e.getLayout("offset");return{progress:function(t,e){for(var i,r=t.count,l=p&&lC(3*r),u=p&&s&&lC(3*r),m=p&&lC(r),x=n.master.getRect(),_=h?x.width:x.height,b=e.getStore(),w=0;null!=(i=t.next());){var S=b.get(c?g:o,i),M=b.get(a,i),I=d,T=void 0;c&&(T=+S-b.get(o,i));var C=void 0,D=void 0,A=void 0,k=void 0;if(h){var L=n.dataToPoint([S,M]);c&&(I=n.dataToPoint([T,M])[0]),C=I,D=L[1]+y,A=L[0]-I,k=v,Ao(A)s){c=(p+u)/2;break}1===d&&(h=f-i[0].tickValue)}null==c&&(u?u&&(c=i[i.length-1].coord):c=i[0].coord),o[n]=t.toGlobalCoord(c)}});else{var a=this.getData(),s=a.getLayout("offset"),l=a.getLayout("size"),u=i.getBaseAxis().isHorizontal()?0:1;o[u]+=s+l/2}return o}return[NaN,NaN]},e.prototype.__requireStartValue=function(t){return this.getBaseAxis()!==t},e.type="series.__base_bar__",e.defaultOption={z:2,coordinateSystem:"cartesian2d",legendHoverLink:!0,barMinHeight:0,barMinAngle:0,large:!1,largeThreshold:400,progressive:3e3,progressiveChunkMode:"mod",defaultBarGap:"10%"},e}(Hy);Hy.registerClass(qD);var KD=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.getInitialData=function(){return $b(null,this,{useEncodeDefaulter:!0,createInvertedIndices:!!this.get("realtimeSort",!0)||null})},e.prototype.getProgressive=function(){return!!this.get("large")&&this.get("progressive")},e.prototype.__preparePipelineContext=function(t,e){var n=rs(this,t,e);return n.progressiveRender&&(n.large=!0),n},e.prototype.brushSelector=function(t,e,n){return n.rect(e.getItemLayout(t))},e.type="series."+PD,e.dependencies=["grid","polar"],e.defaultOption=Op(qD.defaultOption,{clip:!0,roundCap:!1,showBackground:!1,backgroundStyle:{color:"rgba(180, 180, 180, 0.2)",borderColor:null,borderWidth:0,borderType:"solid",borderRadius:0,shadowBlur:0,shadowColor:null,shadowOffsetX:0,shadowOffsetY:0,opacity:1},select:{itemStyle:{borderColor:ug.color.primary,borderWidth:2}},realtimeSort:!1}),e}(qD),$D=function(){this.cx=0,this.cy=0,this.r0=0,this.r=0,this.startAngle=0,this.endAngle=2*Math.PI,this.clockwise=!0},JD=function(t){function e(e){var n=t.call(this,e)||this;return n.type="sausage",n}return n(e,t),e.prototype.getDefaultShape=function(){return new $D},e.prototype.buildPath=function(t,e){var n=e.cx,i=e.cy,r=Math.max(e.r0||0,0),o=Math.max(e.r,0),a=.5*(o-r),s=r+a,l=e.startAngle,u=e.endAngle,c=e.clockwise,h=2*Math.PI,d=c?u-lo)return!0;o=u}return!1},e.prototype._isOrderDifferentInView=function(t,e){for(var n=e.scale,i=n.getExtent(),r=Math.max(0,i[0]),o=Math.min(i[1],n.getOrdinalMeta().categories.length-1);r<=o;++r)if(t.ordinalNumbers[r]!==n.getRawOrdinalNumber(r))return!0},e.prototype._updateSortWithinSameData=function(t,e,n,i){if(this._isOrderChangedWithinSameData(t,e,n)){var r=this._dataSort(t,n,e);this._isOrderDifferentInView(r,n)&&(this._removeOnRenderedListener(i),i.dispatchAction({type:"changeAxisOrder",componentType:n.dim+"Axis",axisId:n.index,sortInfo:r}))}},e.prototype._dispatchInitSort=function(t,e,n){var i=e.baseAxis,r=this._dataSort(t,i,function(n){return t.get(t.mapDimension(e.otherAxis.dim),n)});n.dispatchAction({type:"changeAxisOrder",componentType:i.dim+"Axis",isInitSort:!0,axisId:i.index,sortInfo:r})},e.prototype.remove=function(t,e){this._clear(this._model),this._removeOnRenderedListener(e)},e.prototype.dispose=function(t,e){this._removeOnRenderedListener(e)},e.prototype._removeOnRenderedListener=function(t){this._onRendered&&(t.getZr().off("rendered",this._onRendered),this._onRendered=null)},e.prototype._clear=function(t){var e=this.group,n=this._data;t&&t.isAnimationEnabled()&&n&&!this._isLargeDraw?(this._removeBackground(),this._backgroundEls=[],n.eachItemGraphicEl(function(e){ud(e,t,Iu(e).dataIndex)})):e.removeAll(),this._data=null,this._isFirstFrame=!0},e.prototype._removeBackground=function(){this.group.remove(this._backgroundGroup),this._backgroundGroup=null},e.type=PD,e}(tm),oA={cartesian2d:function(t,e){var n=e.width<0?-1:1,i=e.height<0?-1:1;n<0&&(e.x+=e.width,e.width=-e.width),i<0&&(e.y+=e.height,e.height=-e.height);var r=t.x+t.width,o=t.y+t.height,a=nA(e.x,t.x),s=iA(e.x+e.width,r),l=nA(e.y,t.y),u=iA(e.y+e.height,o),c=sr?s:a,e.y=h&&l>o?u:l,e.width=c?0:s-a,e.height=h?0:u-l,n<0&&(e.x+=e.width,e.width=-e.width),i<0&&(e.y+=e.height,e.height=-e.height),c||h},polar:function(t,e){var n=e.r0<=e.r?1:-1;if(n<0){var i=e.r;e.r=e.r0,e.r0=i}var r=iA(e.r,t.r),o=nA(e.r0,t.r0);e.r=r,e.r0=o;var a=r-o<0;if(n<0){i=e.r;e.r=e.r0,e.r0=i}return a}},aA={cartesian2d:function(t,e,n,i,r,o,a,s,l){var u=new lu({shape:k({},i),z2:1});(u.__dataIndex=n,u.name="item",o)&&(u.shape[r?"height":"width"]=0);return u},polar:function(t,e,n,i,r,o,a,s,l){var u=!r&&l?JD:Sh,c=new u({shape:i,z2:1});c.name="item";var h,d,p=pA(r);if(c.calculateTextPosition=(h=p,d=({isRoundCap:u===JD}||{}).isRoundCap,function(t,e,n){var i=e.position;if(!i||i instanceof Array)return to(t,e,n);var r=h(i),o=null!=e.distance?e.distance:5,a=this.shape,s=a.cx,l=a.cy,u=a.r,c=a.r0,p=(u+c)/2,f=a.startAngle,g=a.endAngle,v=(f+g)/2,y=d?Math.abs(u-c)/2:0,m=Math.cos,x=Math.sin,_=s+u*m(f),b=l+u*x(f),w="left",S="top";switch(r){case"startArc":_=s+(c-o)*m(v),b=l+(c-o)*x(v),w="center",S="top";break;case"insideStartArc":_=s+(c+o)*m(v),b=l+(c+o)*x(v),w="center",S="bottom";break;case"startAngle":_=s+p*m(f)+QD(f,o+y,!1),b=l+p*x(f)+tA(f,o+y,!1),w="right",S="middle";break;case"insideStartAngle":_=s+p*m(f)+QD(f,-o+y,!1),b=l+p*x(f)+tA(f,-o+y,!1),w="left",S="middle";break;case"middle":_=s+p*m(v),b=l+p*x(v),w="center",S="middle";break;case"endArc":_=s+(u+o)*m(v),b=l+(u+o)*x(v),w="center",S="bottom";break;case"insideEndArc":_=s+(u-o)*m(v),b=l+(u-o)*x(v),w="center",S="top";break;case"endAngle":_=s+p*m(g)+QD(g,o+y,!0),b=l+p*x(g)+tA(g,o+y,!0),w="left",S="middle";break;case"insideEndAngle":_=s+p*m(g)+QD(g,-o+y,!0),b=l+p*x(g)+tA(g,-o+y,!0),w="right",S="middle";break;default:return to(t,e,n)}return(t=t||{}).x=_,t.y=b,t.align=w,t.verticalAlign=S,t}),o){var f=r?"r":"endAngle",g={};c.shape[f]=r?i.r0:i.startAngle,g[f]=i[f],(s?rd:od)(c,{shape:g},o)}return c}};function sA(t,e,n,i,r,o,a,s){var l,u;o?(u={x:i.x,width:i.width},l={y:i.y,height:i.height}):(u={y:i.y,height:i.height},l={x:i.x,width:i.width}),s||(a?rd:od)(n,{shape:l},e,r,null),(a?rd:od)(n,{shape:u},e?t.baseAxis.model:null,r)}function lA(t,e){for(var n=0;n0?1:-1,a=i.height>0?1:-1;return{x:i.x+o*r/2,y:i.y+a*r/2,width:i.width-o*r,height:i.height-a*r}},polar:function(t,e,n){var i=t.getItemLayout(e);return{cx:i.cx,cy:i.cy,r0:i.r0,r:i.r,startAngle:i.startAngle,endAngle:i.endAngle,clockwise:i.clockwise}}};function pA(t){return function(t){var e=t?"Arc":"Angle";return function(t){switch(t){case"start":case"insideStart":case"end":case"insideEnd":return t+e;default:return t}}}(t)}function fA(t,e,n,i,r,o,a,s){var l=e.getItemVisual(n,"style");if(s){if(!o.get("roundCap")){var u=t.shape;k(u,eA(i.getModel("itemStyle"),u,!0)),t.setShape(u)}}else{var c=i.get(["itemStyle","borderRadius"])||0;t.setShape("r",c)}t.useStyle(l);var h=i.getShallow("cursor");h&&t.attr("cursor",h);var d=s?a?r.r>=r.r0?"endArc":"startArc":r.endAngle>=r.startAngle?"endAngle":"startAngle":a?function(t,e){if(0===t.height){return e.getOtherAxis(e.getBaseAxis()).inverse?"bottom":"top"}return t.height>0?"bottom":"top"}(r,o.coordinateSystem):function(t,e){if(0===t.width){return e.getOtherAxis(e.getBaseAxis()).inverse?"left":"right"}return t.width>=0?"right":"left"}(r,o.coordinateSystem),p=ap(i);op(t,p,{labelFetcher:o,labelDataIndex:n,defaultText:jT(o.getData(),n),inheritColor:l.fill,defaultOpacity:l.opacity,defaultOutsidePosition:d});var f=t.getTextContent();if(s&&f){var g=i.get(["label","position"]);t.textConfig.inside="middle"===g||null,function(t,e,n,i){if(K(i))t.setTextConfig({rotation:i});else if(X(e))t.setTextConfig({rotation:0});else{var r,o=t.shape,a=o.clockwise?o.startAngle:o.endAngle,s=o.clockwise?o.endAngle:o.startAngle,l=(a+s)/2,u=n(e);switch(u){case"startArc":case"insideStartArc":case"middle":case"insideEndArc":case"endArc":r=l;break;case"startAngle":case"insideStartAngle":r=a;break;case"endAngle":case"insideEndAngle":r=s;break;default:return void t.setTextConfig({rotation:0})}var c=1.5*Math.PI-r;"middle"===u&&c>Math.PI/2&&c<1.5*Math.PI&&(c-=Math.PI),t.setTextConfig({rotation:c})}}(t,"outside"===g?d:g,pA(a),i.get(["label","rotate"]))}gp(f,p,o.getRawValue(n),function(t){return qT(e,t)});var v=i.getModel(["emphasis"]);kc(t,v.get("focus"),v.get("blurScope"),v.get("disabled")),Rc(t,i),function(t){return null!=t.startAngle&&null!=t.endAngle&&t.startAngle===t.endAngle}(r)&&(t.style.fill="none",t.style.stroke="none",B(t.states,function(t){t.style&&(t.style.fill=t.style.stroke="none")}))}var gA=function(){},vA=function(t){function e(e){var n=t.call(this,e)||this;return n.type="largeBar",n}return n(e,t),e.prototype.getDefaultShape=function(){return new gA},e.prototype.buildPath=function(t,e){for(var n=e.points,i=this.baseDimIdx,r=1-this.baseDimIdx,o=[],a=[],s=this.barWidth,l=0;l=s[0]&&e<=s[0]+l[0]&&n>=s[1]&&n<=s[1]+l[1])return a[c]}return-1}(this,t.offsetX,t.offsetY);Iu(this).dataIndex=e>=0?e:null},30,!1);function xA(t,e,n){if(wC(n,"cartesian2d")){var i=e,r=n.getArea();return{x:t?i.x:r.x,y:t?r.y:i.y,width:t?i.width:r.width,height:t?r.height:i.height}}var o=e;return{cx:(r=n.getArea()).cx,cy:r.cy,r0:t?r.r0:o.r0,r:t?r.r:o.r,startAngle:t?o.startAngle:0,endAngle:t?o.endAngle:2*Math.PI}}function _A(t){return{seriesType:t,reset:function(t,e){var n=e.findComponents({mainType:"legend"});if(n&&n.length){var i=t.getData();i.filterSelf(function(t){for(var e=i.getName(t),r=0;r=0},t.prototype.indexOfName=function(t){return this._getDataWithEncodedVisual().indexOfName(t)},t.prototype.getItemVisual=function(t,e){return this._getDataWithEncodedVisual().getItemVisual(t,e)},t}(),MA="pie",IA=Pa(),TA=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(e){t.prototype.init.apply(this,arguments),this.legendVisualProvider=new SA(U(this.getData,this),U(this.getRawData,this)),this._defaultLabelLine(e)},e.prototype.mergeOption=function(){t.prototype.mergeOption.apply(this,arguments)},e.prototype.getInitialData=function(){return bA(this,{coordDimensions:["value"],encodeDefaulter:Y(Sg,this)})},e.prototype.getDataParams=function(e){var n=this.getData(),i=IA(n),r=i.seats;if(!r){var o=[];n.each(n.mapDimension("value"),function(t){o.push(t)}),r=i.seats=Zo(o,n.hostModel.get("percentPrecision"))}var a=t.prototype.getDataParams.call(this,e);return a.percent=r[e]||0,a.$vars.push("percent"),a},e.prototype._defaultLabelLine=function(t){ba(t,"labelLine",["show"]);var e=t.labelLine,n=t.emphasis.labelLine;e.show=e.show&&t.label.show,n.show=n.show&&t.emphasis.label.show},e.type="series."+MA,e.defaultOption={z:2,legendHoverLink:!0,colorBy:"data",center:["50%","50%"],radius:[0,"50%"],clockwise:!0,startAngle:90,endAngle:"auto",padAngle:0,minAngle:0,minShowLabelAngle:0,selectedOffset:10,percentPrecision:2,stillShowZeroSum:!0,coordinateSystemUsage:"box",left:0,top:0,right:0,bottom:0,width:null,height:null,label:{rotate:0,show:!0,overflow:"truncate",position:"outer",alignTo:"none",edgeDistance:"25%",distanceToLabelLine:5},labelLine:{show:!0,length:15,length2:30,smooth:!1,minTurnAngle:90,maxSurfaceAngle:90,lineStyle:{width:1,type:"solid"}},itemStyle:{borderWidth:1,borderJoin:"round"},showEmptyCircle:!0,emptyCircleStyle:{color:"lightgray",opacity:1},labelLayout:{hideOverlap:!0},emphasis:{scale:!0,scaleSize:5},avoidLabelOverlap:!0,animationType:"expansion",animationDuration:1e3,animationTypeUpdate:"transition",animationEasingUpdate:"cubicInOut",animationDurationUpdate:500,animationEasing:"cubicInOut"},e}(Hy);wA={fullType:TA.type,getCoord2:function(t){return t.getShallow("center")}},Gf.set(wA.fullType,{getCoord2:void 0}).getCoord2=wA.getCoord2;var CA=Math.PI/180;function DA(t,e,n,i,r,o,a,s,l,u){if(!(t.length<2)){for(var c=t.length,h=0;hn?a:o,c=Math.abs(l.label.y-n);if(c>=u.maxY){var h=l.label.x-e-l.len2*r,d=i+l.len,f=Math.abs(h)t.unconstrainedWidth?null:d:null;i.setStyle("width",p)}kA(o,i)}}}function kA(t,e){PA.rect=t,QM(PA,e,LA)}var LA={minMarginForce:[null,0,null,0],marginDefault:[1,0,1,0]},PA={};function OA(t){return"center"===t.position}function RA(t){var e,n,i=t.getData(),r=[],o=!1,a=(t.get("minShowLabelAngle")||0)*CA,s=i.getLayout("viewRect"),l=i.getLayout("r"),u=s.width,c=s.x,h=s.y,d=s.height;function p(t){t.ignore=!0}i.each(function(t){var s=i.getItemGraphicEl(t),h=s.shape,f=s.getTextContent(),g=s.getTextGuideLine(),v=i.getItemModel(t),y=v.getModel("label"),m=y.get("position")||v.get(["emphasis","label","position"]),x=y.get("distanceToLabelLine"),_=y.get("alignTo"),b=Vo(y.get("edgeDistance"),u),w=y.get("bleedMargin");null==w&&(w=Math.min(u,d)>200?10:2);var S=v.getModel("labelLine"),M=S.get("length");M=Vo(M,u);var I=S.get("length2");if(I=Vo(I,u),Math.abs(h.endAngle-h.startAngle)0?"right":"left":L>0?"left":"right"}var G=Math.PI,F=0,H=y.get("rotate");if(K(H))F=H*(G/180);else if("center"===m)F=0;else if("radial"===H||!0===H){F=L<0?-k+G:-k}else if("tangential"===H||"tangential-noflip"===H&&"outside"!==m&&"outer"!==m){var W=Math.atan2(L,P);W<0&&(W=2*G+W),P>0&&"tangential-noflip"!==H&&(W=G+W),F=W-G}if(o=!!F,f.x=T,f.y=C,f.rotation=F,f.setStyle({verticalAlign:"middle"}),O){f.setStyle({align:A});var U=f.states.select;U&&(U.x+=f.x,U.y+=f.y)}else{var Y=new Xe(0,0,0,0);kA(Y,f),r.push({label:f,labelLine:g,position:m,len:M,len2:I,minTurnAngle:S.get("minTurnAngle"),maxSurfaceAngle:S.get("maxSurfaceAngle"),surfaceNormal:new Pe(L,P),linePoints:D,textAlign:A,labelDistance:x,labelAlignTo:_,edgeDistance:b,bleedMargin:w,rect:Y,unconstrainedWidth:Y.width,labelStyleWidth:f.style.width})}s.setTextConfig({inside:O})}}),!o&&t.get("avoidLabelOverlap")&&function(t,e,n,i,r,o,a,s){for(var l=[],u=[],c=Number.MAX_VALUE,h=-Number.MAX_VALUE,d=0;di?c=u=D+b*i/2:(u=D+S,c=r-S),n.setItemLayout(e,{angle:i,startAngle:u,endAngle:c,clockwise:y,cx:o,cy:a,r0:l,r:m?Bo(t,_,[l,s]):s}),D=r}}),T0){for(var l=o.getItemLayout(0),u=1;isNaN(l&&l.startAngle)&&u=n.r0}},e.type=MA,e}(tm);var FA=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.hasSymbolVisual=!0,n}return n(e,t),e.prototype.getInitialData=function(t,e){return $b(null,this,{useEncodeDefaulter:!0})},e.prototype.getProgressive=function(){var t=this.option.progressive;return null==t?this.option.large?5e3:this.get("progressive"):t},e.prototype.getProgressiveThreshold=function(){var t=this.option.progressiveThreshold;return null==t?this.option.large?1e4:this.get("progressiveThreshold"):t},e.prototype.brushSelector=function(t,e,n){return n.point(e.getItemLayout(t))},e.prototype.getZLevelKey=function(){return this.getData().count()>this.getProgressiveThreshold()?this.id:""},e.type="series.scatter",e.dependencies=["grid","polar","geo","singleAxis","calendar","matrix"],e.defaultOption={coordinateSystem:"cartesian2d",z:2,legendHoverLink:!0,symbolSize:10,large:!1,largeThreshold:2e3,itemStyle:{opacity:.8},emphasis:{scale:!0},clip:!0,select:{itemStyle:{borderColor:ug.color.primary}},universalTransition:{divideShape:"clone"}},e}(Hy),HA=function(){},WA=function(t){function e(e){var n=t.call(this,e)||this;return n._off=0,n.hoverDataIdx=-1,n}return n(e,t),e.prototype.getDefaultShape=function(){return new HA},e.prototype.reset=function(){this.notClear=!1,this._off=0},e.prototype.beforeBrush=function(t){t&&!t.contentRetained&&this.reset()},e.prototype.buildPath=function(t,e){var n,i=e.points,r=e.size,o=this.symbolProxy,a=o.shape,s=t.getContext?t.getContext():t,l=s&&r[0]<4,u=this.softClipShape;if(l)this._ctx=s;else{for(this._ctx=null,n=this._off;n=0;s--){var l=2*s,u=i[l]-o/2,c=i[l+1]-a/2;if(t>=u&&e>=c&&t<=u+o&&e<=c+a)return s}return-1},e.prototype.contain=function(t,e){var n=this.transformCoordToLocal(t,e),i=this.getBoundingRect();return t=n[0],e=n[1],i.contain(t,e)?(this.hoverDataIdx=this.findDataIndex(t,e))>=0:(this.hoverDataIdx=-1,!1)},e.prototype.getBoundingRect=function(){var t=this._rect;if(!t){for(var e=this.shape,n=e.points,i=e.size,r=i[0],o=i[1],a=1/0,s=1/0,l=-1/0,u=-1/0,c=0;c=0&&(l.dataIndex=n+(t.startIndex||0))})},t.prototype.remove=function(){this._clear()},t.prototype._clear=function(){this._newAdded=[],this.group.removeAll()},t}(),YA=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){var i=t.getData();this._updateSymbolDraw(i,t).updateData(i,XA(t)),this._finished=!0},e.prototype.incrementalPrepareRender=function(t,e,n){var i=t.getData();this._updateSymbolDraw(i,t).incrementalPrepareUpdate(i),this._finished=!1},e.prototype.incrementalRender=function(t,e,n){this._symbolDraw.incrementalUpdate(t,e.getData(),is(e),XA(e)),this._finished=t.end===e.getData().count()},e.prototype.updateTransform=function(t,e,n){var i=t.getData();if(this.group.dirty(),!this._finished)return{update:!0};var r=GC("").reset(t,e,n);r.progress&&r.progress({start:0,end:i.count(),count:i.count()},i),this._symbolDraw.updateLayout(XA(t))},e.prototype.eachRendered=function(t){this._symbolDraw&&this._symbolDraw.eachRendered(t)},e.prototype._updateSymbolDraw=function(t,e){var n=this._symbolDraw,i=e.pipelineContext.large;return n&&i===this._isLargeDraw||(n&&n.remove(),n=this._symbolDraw=i?new UA:new nC,this._isLargeDraw=i,this.group.removeAll()),this.group.add(n.group),n},e.prototype.remove=function(t,e){this._symbolDraw&&this._symbolDraw.remove(!0),this._symbolDraw=null},e.prototype.dispose=function(){},e.type="scatter",e}(tm);function XA(t){return{clipShape:bC(t)}}var ZA=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.getCoordSysModel=function(){return this.getReferringComponents("grid",Ea).models[0]},e.type="cartesian2dAxis",e}(lg);E(ZA,nS);var jA={show:!0,z:0,inverse:!1,name:"",nameLocation:"end",nameRotate:null,nameTruncate:{maxWidth:null,ellipsis:"...",placeholder:"."},nameTextStyle:{},nameGap:15,silent:!1,triggerEvent:!1,tooltip:{show:!1},axisPointer:{},axisLine:{show:!0,onZero:"auto",onZeroAxisIndex:null,lineStyle:{color:ug.color.axisLine,width:1,type:"solid"},symbol:["none","none"],symbolSize:[10,15],breakLine:!0},axisTick:{show:!0,inside:!1,length:5,lineStyle:{width:1}},axisLabel:{show:!0,inside:!1,rotate:0,showMinLabel:null,showMaxLabel:null,margin:8,fontSize:12,color:ug.color.axisLabel,textMargin:[0,3]},splitLine:{show:!0,showMinLine:!0,showMaxLine:!0,lineStyle:{color:ug.color.axisSplitLine,width:1,type:"solid"}},splitArea:{show:!1,areaStyle:{color:[ug.color.backgroundTint,ug.color.backgroundTransparent]}},breakArea:{show:!0,itemStyle:{color:ug.color.neutral00,borderColor:ug.color.border,borderWidth:1,borderType:[3,3],opacity:.6},zigzagAmplitude:4,zigzagMinSpan:4,zigzagMaxSpan:20,zigzagZ:100,expandOnClick:!0},breakLabelLayout:{moveOverlap:"auto"}},qA=D({boundaryGap:!0,deduplication:null,jitter:0,jitterOverlap:!0,jitterMargin:2,splitLine:{show:!1},axisTick:{alignWithLabel:!1,interval:"auto",show:"auto"},axisLabel:{interval:"auto"}},jA),KA=D({boundaryGap:[0,0],axisLine:{show:"auto"},axisTick:{show:"auto"},splitNumber:5,minorTick:{show:!1,splitNumber:5,length:3,lineStyle:{}},minorSplitLine:{show:!1,lineStyle:{color:ug.color.axisMinorSplitLine,width:1}}},jA),$A={category:qA,value:KA,time:D({splitNumber:6,axisLabel:{rich:{primary:{fontWeight:"bold"}}},splitLine:{show:!1}},KA),log:P({logBase:10},KA)};function JA(t,e,i,r){B(Gw,function(o,a){var s=D(D({},$A[a],!0),r,!0),l=function(t){function i(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e+"Axis."+a,n}return n(i,t),i.prototype.mergeDefaultAndTheme=function(t,e){var n=ig(this),i=n?og(t):{};D(t,e.getTheme().get(a+"Axis")),D(t,this.getDefaultOption()),t.type=QA(t),n&&rg(t,i,n)},i.prototype.optionUpdated=function(){"category"===this.option.type&&(this.__ordinalMeta=tw.createByAxisModel(this))},i.prototype.getCategories=function(t){var e=this.option;if("category"===e.type)return t?e.data:this.__ordinalMeta.categories},i.prototype.getOrdinalMeta=function(){return this.__ordinalMeta},i.prototype.updateAxisBreaks=function(t){var e=XC();return e?e.updateModelAxisBreak(this,t):{breaks:[]}},i.type=e+"Axis."+a,i.defaultOption=s,i}(i);t.registerComponentModel(l)}),t.registerSubTypeDefaulter(e+"Axis",QA)}function QA(t){return t.type||(t.data?"category":"value")}var tk=function(){function t(t){this.type="cartesian",this._dimList=[],this._axes={},this.name=t||""}return t.prototype.getAxis=function(t){return this._axes[t]},t.prototype.getAxes=function(){return V(this._dimList,function(t){return this._axes[t]},this)},t.prototype.getAxesByScale=function(t){return t=t.toLowerCase(),F(this.getAxes(),function(e){return e.scale.type===t})},t.prototype.addAxis=function(t){var e=t.dim;this._axes[e]=t,this._dimList.push(e)},t}(),ek=["x","y"];function nk(t){return("interval"===t.type||"time"===t.type)&&!Xp(t)}var ik=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.type=BD,e.dimensions=ek,e}return n(e,t),e.prototype.calcAffineTransform=function(){this._transform=this._invTransform=null;var t=this.getAxis("x").scale,e=this.getAxis("y").scale;if(nk(t)&&nk(e)){var n=sw(t,null),i=sw(e,null),r=this.dataToPoint([n[0],i[0]]),o=this.dataToPoint([n[1],i[1]]),a=n[1]-n[0],s=i[1]-i[0];if(a&&s){var l=(o[0]-r[0])/a,u=(o[1]-r[1])/s,c=r[0]-n[0]*l,h=r[1]-i[0]*u,d=this._transform=[l,0,0,u,c,h];this._invTransform=Ae([],d)}}},e.prototype.getBaseAxis=function(){return this.getAxesByScale("ordinal")[0]||this.getAxesByScale("time")[0]||this.getAxis("x")},e.prototype.containPoint=function(t){var e=this.getAxis("x"),n=this.getAxis("y");return e.contain(e.toLocalCoord(t[0]))&&n.contain(n.toLocalCoord(t[1]))},e.prototype.containData=function(t){return this.getAxis("x").containData(t[0])&&this.getAxis("y").containData(t[1])},e.prototype.containZone=function(t,e){var n=this.dataToPoint(t),i=this.dataToPoint(e),r=this.getArea(),o=new Xe(n[0],n[1],i[0]-n[0],i[1]-n[1]);return r.intersect(o)},e.prototype.dataToPoint=function(t,e,n){n=n||[];var i=t[0],r=t[1];if(this._transform&&null!=i&&isFinite(i)&&null!=r&&isFinite(r))return Yt(n,t,this._transform);var o=this.getAxis("x"),a=this.getAxis("y");return n[0]=o.toGlobalCoord(o.dataToCoord(i,e)),n[1]=a.toGlobalCoord(a.dataToCoord(r,e)),n},e.prototype.clampData=function(t,e){var n=this.getAxis("x").scale,i=this.getAxis("y").scale,r=n.getExtent(),o=i.getExtent(),a=n.parse(t[0]),s=i.parse(t[1]);return(e=e||[])[0]=Math.min(Math.max(Math.min(r[0],r[1]),a),Math.max(r[0],r[1])),e[1]=Math.min(Math.max(Math.min(o[0],o[1]),s),Math.max(o[0],o[1])),e},e.prototype.pointToData=function(t,e,n){if(n=n||[],this._invTransform)return Yt(n,t,this._invTransform);var i=this.getAxis("x"),r=this.getAxis("y");return n[0]=i.coordToData(i.toLocalCoord(t[0]),e),n[1]=r.coordToData(r.toLocalCoord(t[1]),e),n},e.prototype.getOtherAxis=function(t){return this.getAxis("x"===t.dim?"y":"x")},e.prototype.getArea=function(t){t=t||0;var e=this.getAxis("x").getGlobalExtent(),n=this.getAxis("y").getGlobalExtent(),i=Math.min(e[0],e[1])-t,r=Math.min(n[0],n[1])-t,o=Math.max(e[0],e[1])-i+t,a=Math.max(n[0],n[1])-r+t;return new Xe(i,r,o,a)},e}(tk);function rk(t,e){var n=t.scale,i=t.model;var r,o,a,s=LS(n,i,i.ecModel,t,null),l=gw(n),u=gw(e)?e.intervalStub:e,c=l?n.intervalStub:n,h=n.base,d=u.getTicks(),p=u.getTicks({expandToNicedExtent:!0}),f=d.length-1;if(1===f)r=o=0,a=1;else if(2===f){var g=Ao(d[0].value-d[1].value),v=Ao(d[1].value-d[2].value);r=o=0,g===v?a=2:(a=1,g=A[1])return!0})):T[1]?(x=A[1],k(function(){if(R(),S=Ho(w-_*a,b),L(),m<=A[0])return!0})):k(function(){S=Ho(Po(A[0]/_)*_,b),w=Ho(Lo(A[1]/_)*_,b);var t=ko((w-S)/_);if(t<=a){var e=a-t,n=void 0,i=s.incl0||l;if(i&&0===A[0])n=[0,e];else if(i&&0===A[1])n=[e,0];else{var r=Lo(e/2);n=e%2==0?[r,r]:m+x=A[1])return!0}})}Qw(n,T,D,[m,x],C,{interval:_,intervalCount:a,intervalPrecision:b,niceExtent:[S,w]})}var ok,ak=[[3,1],[0,2]],sk=function(){function t(t,e,n){this.type="grid",this._coordsMap={},this._coordsList=[],this._axesMap={},this._axesList=[],this.axisPointerEnabled=!0,this.dimensions=ek,this._initCartesian(t,e,n),this.model=t}return t.prototype.getRect=function(){return this._rect},t.prototype.update=function(t,e){var n=this._axesMap;function i(t){for(var e=W(t),n=[],i=e.length-1;i>=0;i--){var r=t[+e[i]];r.__alignTo?n.push(r):RS(r)}B(n,function(t){var e,n;e=t,n=t.__alignTo,Xp(e.scale)||Xp(n.scale)||n.scale.getTicks().length<2?RS(t):rk(t,t.__alignTo.scale)})}B(this._axesList,function(t){CS(t,1);var e=t.scale;vw(e)&&e.setSortInfo(t.model.get("categorySortInfo"))}),i(n.x),i(n.y);var r={};B(n.x,function(t){lk(n,"y",t,r)}),B(n.y,function(t){lk(n,"x",t,r)}),this.resize(this.model,e)},t.prototype.resize=function(t,e,n){var i=eg(t,e),r=this._rect=Jf(t.getBoxLayoutParams(),i.refContainer),o=this._axesMap,a=this._coordsList,s=t.get("containLabel");if(hk(o,r),!n){var l=function(t,e,n,i,r){var o=new aD(gk);return B(n,function(n){return B(n,function(n){if($w(n.model)){var a=!i;n.axisBuilder=function(t,e,n,i,r,o){for(var a=MD(t,n),s=!1,l=!1,u=0;ue?Uw:Xw:Xw}(e.scale,0,!1),i=e&&"category"!==e.type&&"time"!==e.type&&n!==Xw;return i&&"auto"===t&&function(t){return Fw(t).noOnMyZero}(e)&&(i=!1),i}function ck(t){for(var e,n=W(t),i=[],r=n.length-1;r>=0;r--){var o=t[+n[r]];dw(o.scale)&&null==Jw(o.model,o.type,!0)&&(o.model.get("alignTicks")&&null==o.model.get("interval")?i.push(o):e=o)}e||(e=i.pop()),e&&B(i,function(t){t.__alignTo=e})}function hk(t,e){B(t.x,function(t){return dk(t,e.x,e.width)}),B(t.y,function(t){return dk(t,e.y,e.height)})}function dk(t,e,n){var i=[0,n],r=t.inverse?1:0;t.setExtent(i[r],i[1-r]),function(t,e){var n=t.getExtent(),i=n[0]+n[1];t.toGlobalCoord="x"===t.dim?function(t){return t+e}:function(t){return i-t+e},t.toLocalCoord="x"===t.dim?function(t){return t-e}:function(t){return i-t+e}}(t,e)}function pk(t,e,n,i,r,o,a){fk(i,r,aM,e,!1,a);var s=[0,0,0,0];u(0),u(1),c(i,0,NaN),c(i,1,NaN);var l=null==H(s,function(t){return t>0});return zd(i,s,!0,!0,n),hk(r,i),l;function u(t){B(r[dd[t]],function(e){if($w(e.model)){var n=o.ensureRecord(e.model),i=n.labelInfoList;if(i)for(var r=0;r0&&!rt(e)&&e>1e-4&&(t/=e),t}}function fk(t,e,n,i,r,o){var a=n===sM;B(e,function(e){return B(e,function(e){$w(e.model)&&(!function(t,e,n){var i=MD(e,n);t.updateCfg(i)}(e.axisBuilder,t,e.model),e.axisBuilder.build(a?{axisTickLabelDetermine:!0}:{axisTickLabelEstimate:!0},{noPxChange:r}))})});var s={x:0,y:0};function l(e){s[dd[1-e]]=t[pd[e]]<=.5*o.refContainer[pd[e]]?0:1-e==1?2:1}l(0),l(1),B(e,function(t,e){return B(t,function(t){$w(t.model)&&(("all"===i||a)&&t.axisBuilder.build({axisName:!0},{nameMarginLevel:s[e]}),a&&t.axisBuilder.build({axisLine:!0}))})})}var gk=function(t,e,n,i,r,o){var a="x"===n.axis.dim?"y":"x";uD(t,0,0,i,r,o),Kw(t.nameLocation)||B(e.recordMap[a],function(t){t&&t.labelInfoList&&t.dirVec&&hD(t.labelInfoList,t.dirVec,i,r)})};function vk(t,e){var n={axesInfo:{},seriesInvolved:!1,coordSysAxesInfo:{},coordSysMap:{}};return function(t,e,n){var i=e.getComponent("tooltip"),r=e.getComponent("axisPointer"),o=r.get("link",!0)||[],a=[];B(n.getCoordinateSystems(),function(n){if(n.axisPointerEnabled){var s=_k(n.model),l=t.coordSysAxesInfo[s]={};t.coordSysMap[s]=n;var u=n.model.getModel("tooltip",i);if(B(n.getAxes(),Y(p,!1,null)),n.getTooltipAxes&&i&&u.get("show")){var c="axis"===u.get("trigger"),h="cross"===u.get(["axisPointer","type"]),d=n.getTooltipAxes(u.get(["axisPointer","axis"]));(c||h)&&B(d.baseAxes,Y(p,!h||"cross",c)),h&&B(d.otherAxes,Y(p,"cross",!1))}}function p(i,s,c){var h=c.model.getModel("axisPointer",r),d=h.get("show");if(d&&("auto"!==d||i||xk(h))){null==s&&(s=h.get("triggerTooltip")),h=i?function(t,e,n,i,r,o){var a=e.getModel("axisPointer"),s={};B(["type","snap","lineStyle","shadowStyle","label","animation","animationDurationUpdate","animationEasingUpdate","z"],function(t){s[t]=C(a.get(t))}),s.snap="category"!==t.type&&!!o,"cross"===a.get("type")&&(s.type="line");var l=s.label||(s.label={});if(null==l.show&&(l.show=!1),"cross"===r){var u=a.get(["label","show"]);if(l.show=null==u||u,!o){var c=s.lineStyle=a.get("crossStyle");c&&P(l,c.textStyle)}}return t.model.getModel("axisPointer",new kp(s,n,i))}(c,u,r,e,i,s):h;var p=h.get("snap"),f=h.get("triggerEmphasis"),g=_k(c.model),v=s||p||"category"===c.type,y=t.axesInfo[g]={key:g,axis:c,coordSys:n,axisPointerModel:h,triggerTooltip:s,triggerEmphasis:f,involveSeries:v,snap:p,useHandle:xk(h),seriesModels:[],linkGroup:null};l[g]=y,t.seriesInvolved=t.seriesInvolved||v;var m=function(t,e){for(var n=e.model,i=e.dim,r=0;r=0||t===e}function mk(t){var e=(t.ecModel.getComponent("axisPointer")||{}).coordSysAxesInfo;return e&&e.axesInfo[_k(t)]}function xk(t){return!!t.get(["handle","show"])}function _k(t){return t.type+"||"+t.id}var bk={},wk=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(e,n,i,r){this.axisPointerClass&&function(t){var e=mk(t);if(e){var n=e.axisPointerModel,i=e.axis.scale,r=n.option,o=n.get("status"),a=n.get("value");null!=a&&(a=i.parse(a));var s=xk(n);null==o&&(r.status=s?"show":"hide");var l=i.getExtent();(null==a||a>l[1])&&(a=l[1]),a0))return n;var a=r.get("jitterOverlap"),s=r.get("jitterMargin")||0,l=vw(t.scale)?bM(t).w:null;return a?Vk(n,o,l,i):function(t,e,n,i,r,o){var a=zk(t);a.items||(a.items=[]);var s=a.items,l=Gk(s,e,n,i,r,o,1),u=Gk(s,e,n,i,r,o,-1),c=Math.abs(l-n)r/2||h&&d>h/2-i)return Vk(n,r,h,i);return s.push({fixedCoord:e,floatCoord:c,r:i}),c}(t,e,n,i,o,s)}function Vk(t,e,n,i){if(null===n)return t+(Math.random()-.5)*e;var r=n-2*i,o=Math.min(Math.max(0,e),r);return t+(Math.random()-.5)*o}function Gk(t,e,n,i,r,o,a){for(var s=n,l=0;lr/2)return Number.MAX_VALUE;if(1===a&&f>s||-1===a&&f0;return a&&s}(t,n);if(i){var r=n.dim,o=n.orient,a="horizontal"===o&&"category"!==n.type||"vertical"===o&&"category"===n.type,s="y"===r||"single"===r&&a,l="x"===r||"single"===r&&!a;if(s||l)return{progress:function(t,e){var i=e.getLayout("points"),r=!!i;for(var o=t.start;o0&&!d.min?d.min=0:null!=d.min&&d.min<0&&!d.max&&(d.max=0);var p=s;null!=d.color&&(p=P({color:d.color},s));var f=D(C(d),{boundaryGap:t,splitNumber:e,clockwise:n,scale:i,axisLine:r,axisTick:o,axisLabel:a,name:d.text,showName:l,nameLocation:"end",nameGap:c,nameTextStyle:p,triggerEvent:h},!1);if(j(u)){var g=f.name;f.name=u.replace("{value}",null!=g?g:"")}else Z(u)&&(f.name=u(f.name,f));var v=new kp(f,null,this.ecModel);return E(v,nS.prototype),v.mainType="radar",v.componentIndex=this.componentIndex,v.uid=Pp("ec_radar"),v},this);this._indicatorModels=d},e.prototype.getIndicatorModels=function(){return this._indicatorModels},e.type=jk,e.defaultOption={z:0,center:["50%","50%"],radius:"50%",startAngle:90,clockwise:!1,axisName:{show:!0,color:ug.color.axisLabel},boundaryGap:[0,0],splitNumber:5,axisNameGap:15,scale:!1,shape:"polygon",axisLine:D({lineStyle:{color:ug.color.neutral20}},Xk.axisLine),axisLabel:Kk(Xk.axisLabel,!1),axisTick:Kk(Xk.axisTick,!1),splitLine:Kk(Xk.splitLine,!0),splitArea:Kk(Xk.splitArea,!0),indicator:[]},e}(lg),Jk=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){this.group.removeAll(),this._buildAxes(t,n),this._buildSplitLineAndArea(t)},e.prototype._buildAxes=function(t,e){var n=t.coordinateSystem;B(V(n.getIndicatorAxes(),function(t){var i=t.model.get("showName")?t.name:"";return new dD(t.model,e,{axisName:i,position:[n.cx,n.cy],rotation:t.angle,labelDirection:-1,tickDirection:-1,nameDirection:1})}),function(t){t.build(),this.group.add(t.group)},this)},e.prototype._buildSplitLineAndArea=function(t){var e=t.coordinateSystem,n=e.getIndicatorAxes();if(n.length){var i=t.get("shape"),r=t.getModel("splitLine"),o=t.getModel("splitArea"),a=r.getModel("lineStyle"),s=o.getModel("areaStyle"),l=r.get("show"),u=o.get("show"),c=a.get("color"),h=s.get("color"),d=X(c)?c:[c],p=X(h)?h:[h],f=[],g=[];if("circle"===i)for(var v=n[0].getTicksCoords(),y=e.cx,m=e.cy,x=0;x3?1.4:r>1?1.2:1.1,l=i>0?s:1/s;this._checkTriggerMoveZoom(this,"zoom","zoomOnMouseWheel",t,{scale:l,originX:o,originY:a,isAvailableBehavior:null})}if(n){var u=Math.abs(i),c=(i>0?1:-1)*(u>3?.4:u>1?.15:.05);this._checkTriggerMoveZoom(this,"scrollMove","moveOnMouseWheel",t,{scrollDelta:c,originX:o,originY:a,isAvailableBehavior:null})}}}},e.prototype._pinchHandler=function(t){if(!aL(this._zr,"globalPan")&&!cL(t)){var e=t.pinchScale>1?1.1:1/1.1;this._checkTriggerMoveZoom(this,"zoom",null,t,{scale:e,originX:t.pinchX,originY:t.pinchY,isAvailableBehavior:null})}},e.prototype._checkTriggerMoveZoom=function(t,e,n,i,r){t._checkPointer(i,r.originX,r.originY)&&(ye(i.event),i.__ecRoamConsumed=!0,yL(t,e,n,i,r))},e}($t);function cL(t){return t.__ecRoamConsumed}var hL,dL=Pa();function pL(t){var e=dL(t);return e.roam=e.roam||{},e.uniform=e.uniform||{},e}function fL(t,e,n,i){for(var r=pL(t).roam,o=r[e]=r[e]||[],a=0;a=4&&(l={x:parseFloat(h[0]||0),y:parseFloat(h[1]||0),width:parseFloat(h[2]),height:parseFloat(h[3])})}if(l&&null!=a&&null!=s&&(u=VL(l,{x:0,y:0,width:a,height:s}),!e.ignoreViewBox)){var d=i;(i=new vo).add(d),d.scaleX=d.scaleY=u.scale,d.x=u.x,d.y=u.y}return e.ignoreRootClip||null==a||null==s||i.setClipPath(new lu({shape:{x:0,y:0,width:a,height:s}})),{root:i,width:a,height:s,viewBoxRect:l,viewBoxTransform:u,named:r}},t.prototype._parseNode=function(t,e,n,i,r,o){var a,s=t.nodeName.toLowerCase(),l=i;if("defs"===s&&(r=!0),"text"===s&&(o=!0),"defs"===s||"switch"===s)a=e;else{if(!r){var u=hL[s];if(u&&wt(hL,s)){a=u.call(this,t,e);var c=t.getAttribute("name");if(c){var h={name:c,namedFrom:null,svgNodeTagLower:s,el:a};n.push(h),"g"===s&&(l=h)}else i&&n.push({name:i.name,namedFrom:i,svgNodeTagLower:s,el:a});e.add(a)}}var d=IL[s];if(d&&wt(IL,s)){var p=d.call(this,t),f=t.getAttribute("id");f&&(this._defs[f]=p)}}if(a&&a.isGroup)for(var g=t.firstChild;g;)1===g.nodeType?this._parseNode(g,a,n,l,r,o):3===g.nodeType&&o&&this._parseText(g,a),g=g.nextSibling},t.prototype._parseText=function(t,e){var n=new Jl({style:{text:t.textContent},silent:!0,x:this._textX||0,y:this._textY||0});DL(e,n),kL(t,n,this._defsUsePending,!1,!1),function(t,e){var n=e.__selfStyle;if(n){var i=n.textBaseline,r=i;i&&"auto"!==i?"baseline"===i?r="alphabetic":"before-edge"===i||"text-before-edge"===i?r="top":"after-edge"===i||"text-after-edge"===i?r="bottom":"central"!==i&&"mathematical"!==i||(r="middle"):r="alphabetic",t.style.textBaseline=r}var o=e.__inheritedStyle;if(o){var a=o.textAlign,s=a;a&&("middle"===a&&(s="center"),t.style.textAlign=s)}}(n,e);var i=n.style,r=i.fontSize;r&&r<9&&(i.fontSize=9,n.scaleX*=r/9,n.scaleY*=r/9);var o=(i.fontSize||i.fontFamily)&&[i.fontStyle,i.fontWeight,(i.fontSize||12)+"px",i.fontFamily||"sans-serif"].join(" ");i.font=o;var a=n.getBoundingRect();return this._textX+=a.width,e.add(n),n},t.internalField=void(hL={g:function(t,e){var n=new vo;return DL(e,n),kL(t,n,this._defsUsePending,!1,!1),n},rect:function(t,e){var n=new lu;return DL(e,n),kL(t,n,this._defsUsePending,!1,!1),n.setShape({x:parseFloat(t.getAttribute("x")||"0"),y:parseFloat(t.getAttribute("y")||"0"),width:parseFloat(t.getAttribute("width")||"0"),height:parseFloat(t.getAttribute("height")||"0")}),n.silent=!0,n},circle:function(t,e){var n=new ah;return DL(e,n),kL(t,n,this._defsUsePending,!1,!1),n.setShape({cx:parseFloat(t.getAttribute("cx")||"0"),cy:parseFloat(t.getAttribute("cy")||"0"),r:parseFloat(t.getAttribute("r")||"0")}),n.silent=!0,n},line:function(t,e){var n=new Oh;return DL(e,n),kL(t,n,this._defsUsePending,!1,!1),n.setShape({x1:parseFloat(t.getAttribute("x1")||"0"),y1:parseFloat(t.getAttribute("y1")||"0"),x2:parseFloat(t.getAttribute("x2")||"0"),y2:parseFloat(t.getAttribute("y2")||"0")}),n.silent=!0,n},ellipse:function(t,e){var n=new lh;return DL(e,n),kL(t,n,this._defsUsePending,!1,!1),n.setShape({cx:parseFloat(t.getAttribute("cx")||"0"),cy:parseFloat(t.getAttribute("cy")||"0"),rx:parseFloat(t.getAttribute("rx")||"0"),ry:parseFloat(t.getAttribute("ry")||"0")}),n.silent=!0,n},polygon:function(t,e){var n,i=t.getAttribute("points");i&&(n=AL(i));var r=new Dh({shape:{points:n||[]},silent:!0});return DL(e,r),kL(t,r,this._defsUsePending,!1,!1),r},polyline:function(t,e){var n,i=t.getAttribute("points");i&&(n=AL(i));var r=new kh({shape:{points:n||[]},silent:!0});return DL(e,r),kL(t,r,this._defsUsePending,!1,!1),r},image:function(t,e){var n=new eu;return DL(e,n),kL(t,n,this._defsUsePending,!1,!1),n.setStyle({image:t.getAttribute("xlink:href")||t.getAttribute("href"),x:+t.getAttribute("x"),y:+t.getAttribute("y"),width:+t.getAttribute("width"),height:+t.getAttribute("height")}),n.silent=!0,n},text:function(t,e){var n=t.getAttribute("x")||"0",i=t.getAttribute("y")||"0",r=t.getAttribute("dx")||"0",o=t.getAttribute("dy")||"0";this._textX=parseFloat(n)+parseFloat(r),this._textY=parseFloat(i)+parseFloat(o);var a=new vo;return DL(e,a),kL(t,a,this._defsUsePending,!1,!0),a},tspan:function(t,e){var n=t.getAttribute("x"),i=t.getAttribute("y");null!=n&&(this._textX=parseFloat(n)),null!=i&&(this._textY=parseFloat(i));var r=t.getAttribute("dx")||"0",o=t.getAttribute("dy")||"0",a=new vo;return DL(e,a),kL(t,a,this._defsUsePending,!1,!0),this._textX+=parseFloat(r),this._textY+=parseFloat(o),a},path:function(t,e){var n=ih(t.getAttribute("d")||"");return DL(e,n),kL(t,n,this._defsUsePending,!1,!1),n.silent=!0,n}}),t}(),IL={lineargradient:function(t){var e=parseInt(t.getAttribute("x1")||"0",10),n=parseInt(t.getAttribute("y1")||"0",10),i=parseInt(t.getAttribute("x2")||"10",10),r=parseInt(t.getAttribute("y2")||"0",10),o=new Hh(e,n,i,r);return TL(t,o),CL(t,o),o},radialgradient:function(t){var e=parseInt(t.getAttribute("cx")||"0",10),n=parseInt(t.getAttribute("cy")||"0",10),i=parseInt(t.getAttribute("r")||"0",10),r=new Wh(e,n,i);return TL(t,r),CL(t,r),r}};function TL(t,e){"userSpaceOnUse"===t.getAttribute("gradientUnits")&&(e.global=!0)}function CL(t,e){for(var n=t.firstChild;n;){if(1===n.nodeType&&"stop"===n.nodeName.toLocaleLowerCase()){var i=n.getAttribute("offset"),r=void 0;r=i&&i.indexOf("%")>0?parseInt(i,10)/100:i?parseFloat(i):0;var o={};BL(n,o,o);var a=o.stopColor||n.getAttribute("stop-color")||"#000000",s=o.stopOpacity||n.getAttribute("stop-opacity");if(s){var l=gi(a);l&&l[3]&&(l[3]*=si(s),a=Mi(l,"rgba"))}e.colorStops.push({offset:r,color:a})}n=n.nextSibling}}function DL(t,e){t&&t.__inheritedStyle&&(e.__inheritedStyle||(e.__inheritedStyle={}),P(e.__inheritedStyle,t.__inheritedStyle))}function AL(t){for(var e=RL(t),n=[],i=0;i0;o-=2){var a=i[o],s=i[o-1],l=RL(a);switch(r=r||we(),s){case"translate":Te(r,r,[parseFloat(l[0]),parseFloat(l[1]||"0")]);break;case"scale":De(r,r,[parseFloat(l[0]),parseFloat(l[1]||l[0])]);break;case"rotate":Ce(r,r,-parseFloat(l[0])*EL,[parseFloat(l[1]||"0"),parseFloat(l[2]||"0")]);break;case"skewX":Ie(r,[1,0,Math.tan(parseFloat(l[0])*EL),1,0,0],r);break;case"skewY":Ie(r,[1,Math.tan(parseFloat(l[0])*EL),0,1,0,0],r);break;case"matrix":r[0]=parseFloat(l[0]),r[1]=parseFloat(l[1]),r[2]=parseFloat(l[2]),r[3]=parseFloat(l[3]),r[4]=parseFloat(l[4]),r[5]=parseFloat(l[5])}}e.setLocalTransform(r)}}(t,e),BL(t,a,s),i||function(t,e,n){for(var i=0;i1e-6;zP[0]=a?(r[0]-i.x)/o:r[0],zP[1]=a?(r[1]-i.y)/o:r[1],Yt(zP,zP,t.mtRawInv);var s=function(t,e){var n=t.centerOption,i=t.dataRect;0;return!n||t.lgCt?e.slice():[NP(0,e,n,i),NP(1,e,n,i)]}(t,zP);BP(e,s,o),B(n,function(t){t!==e&&BP(t,s.slice(),o)})}(r,e,n,AP)}var AP=Vr(),kP=Vr();function LP(t,e,n){var i=PP(e);i&&(DP(i,e,n,t),dP(i,e))}function PP(t){return t.__ownRoamView?t.__ownRoamView():null}function OP(t,e,n,i){n.setUpdatePayload($d(t));var r=Fu(i,e);r&&r.__updateOnOwnRoam&&r.__updateOnOwnRoam(t,e,i)}function RP(t){return t.scaleX}function NP(t,e,n,i){return n&&i&&i[pd[t]]&&(j(r=n[t])&&Fo(r))?(e[t]-i[dd[t]])/i[pd[t]]*100+"%":e[t];var r}function EP(t,e){return e&&t&&t.getShallow("legacyViewCoordSysCenterBase")?{w:e.getWidth(),h:e.getHeight()}:null}var zP=[];function BP(t,e,n){var i=t.option;i.center=e,i.zoom=n}function VP(t,e){if(e){var n=e.min||0,i=e.max||1/0;t=Math.max(Math.min(i,t),n)}return t}function GP(t,e){var n=e.getShallow("nodeScaleRatio",!0)||1,i=t;return((i.zoom-1)*n+1)/(i.trans[2].scaleX||1)}function FP(t,e,n,i,r,o,a,s){function l(n){var i=t.mainType,r=$d(P({type:YP(i,t.subType,Bu)},n));s&&(r.componentType=i),r[i+"Id"]=t.id,e.dispatchAction(r)}PP(t)?(n.enable(at(t.get("roam"),a),{api:e,zInfo:{component:t},triggerInfo:{roamTrigger:t.get("roamTrigger"),isInSelf:i,isInClip:function(t,e,n){return!r||r.contain(e,n)}}}),n.off("pan").off("zoom").on("pan",function(t){o&&o("pan"),l({dx:t.dx,dy:t.dy})}).on("zoom",function(t){o&&o("zoom"),l({zoom:t.scale,originX:t.originX,originY:t.originY})})):n.disable()}function HP(t){return function(e,n,i){return WP.copy(t.getBoundingRect()),WP.applyTransform(t.getComputedTransform()),WP.contain(n,i)}}var WP=new Xe(0,0,0,0);function UP(t,e,n){var i=YP(e,n,Bu);t.registerAction({type:i,event:i,update:"none"},function(t,i,r){i.eachComponent(Va(t,e,n),function(e){LP(t,e),OP(t,e,i,r)})})}function YP(t,e,n){return(t!==Du?t:"map"===e?"geo":e)+n}function XP(t){return null!=t.zoom}function ZP(t,e,n,i,r,o,a){var s=new nP(null,EP(t.ecModel,e));return pP(s,n,i,r,o),a?fP(s,a.x,a.y,a.width,a.height):fP(s,n,i,r,o),dP(s,t),s}var jP=["rect","circle","line","ellipse","polygon","polyline","path"],qP=mt(jP),KP=mt(jP.concat(["g"])),$P=mt(jP.concat(["g"])),JP=Pa();function QP(t){var e=t.getItemStyle(),n=t.get("areaColor");return null!=n&&(e.fill=n),e}function tO(t){var e=t.style;e&&(e.stroke=e.stroke||e.fill,e.fill=null)}var eO=function(){function t(t){var e=this.group=new vo,n=this._transformGroup=new vo;e.add(n),this.uid=Pp("ec_map_draw"),this._controller=new uL(t.getZr()),n.add(this._regionsGroup=new vo),n.add(this._svgGroup=new vo)}return t.prototype.draw=function(t,e,n,i,r){var o=this,a=t.getData&&t.getData();lO(t)&&e.eachComponent({mainType:"series",subType:"map"},function(e){a||e.getHostGeoModel()!==t||(a=e.getData())});var s,l=t.coordinateSystem,u=l.view,c=this._regionsGroup,h=this._transformGroup,d=!c.childAt(0)||r;l.shouldClip()?(s=sP(null,u),this.group.setClipPath(new lu({shape:s.clone()}))):this.group.removeClipPath(),CP(h,1,u,d?null:t);var p=a&&a.getVisual("visualMeta")&&a.getVisual("visualMeta").length>0;"geoJSON"===l.resourceType?this._buildGeoJSON(u,n,l,t,a,p):"geoSVG"===l.resourceType&&this._buildSVG(u,n,l,t,a,p),FP(t,n,this._controller,function(e,n,i){return t.coordinateSystem.containPoint([n,i])},s,function(){o._mouseDownFlag=!1},!1,!0),this._updateMapSelectHandler(t,c,n,i)},t.prototype.__updateOnOwnRoam=function(t){CP(this._transformGroup,1,t.coordinateSystem.view,null)},t.prototype._buildGeoJSON=function(t,e,n,i,r,o){var a=this._regionsGroupByName=mt(),s=mt(),l=this._regionsGroup,u=n.projection,c=u&&u.stream,h=Br(lP(null,t,0));function d(t,e){return e&&(t=e(t)),t&&Yt([],t,h)}function p(t){for(var e=[],n=!c&&u&&u.project,i=0;i=0)&&(c=t);var h=a?{normal:{align:"center",verticalAlign:"middle"}}:null;op(n,ap(r),{labelFetcher:c,labelDataIndex:u,defaultText:i},h);var d=n.getTextContent();if(d&&(JP(d).ignore=d.ignore,n.textConfig&&a)){var p=n.getBoundingRect().clone();n.textConfig.layoutRect=p,n.textConfig.position=[(a[0]-p.x)/p.width*100+"%",(a[1]-p.y)/p.height*100+"%"]}n.disableLabelAnimation=!0}else n.removeTextContent(),n.removeTextConfig(),n.disableLabelAnimation=null}function rO(t,e,n,i,r,o){e?e.setItemGraphicEl(o,n):Iu(n).eventData={componentType:"geo",componentIndex:t.componentIndex,geoIndex:t.componentIndex,name:i,region:r&&r.option||{}}}function oO(t,e,n,i,r){e||Gd({el:n,componentModel:t,itemName:i,itemTooltipOption:r.get("tooltip")})}function aO(t,e,n,i){e.highDownSilentOnTouch=!!t.get("selectedMode");var r=i.getModel("emphasis"),o=r.get("focus");return kc(e,o,r.get("blurScope"),r.get("disabled")),lO(t)&&function(t,e,n){var i=Iu(t);i.componentMainType=e.mainType,i.componentIndex=e.componentIndex,i.componentHighDownName=n}(e,t,n),o}function sO(t,e,n){var i,r=[];function o(){i=[]}function a(){i.length&&(r.push(i),i=[])}var s=e({polygonStart:o,polygonEnd:a,lineStart:o,lineEnd:a,point:function(t,e){isFinite(t)&&isFinite(e)&&i.push([t,e])},sphere:function(){}});return!n&&s.polygonStart(),B(t,function(t){s.lineStart();for(var e=0;e-1&&(n.style.stroke=n.style.fill,n.style.fill=ug.color.neutral00,n.style.lineWidth=2),n},e.prototype.__ownRoamView=function(){return dO(this)?this.coordinateSystem.view:null},e.type="series."+uO,e.dependencies=["geo"],e.layoutMode="box",e.defaultOption={z:2,coordinateSystem:"geo",map:"",left:"center",top:"center",aspectScale:null,showLegendSymbol:!0,boundingCoords:null,center:null,zoom:1,scaleLimit:null,selectedMode:!0,label:{show:!1,color:ug.color.tertiary},itemStyle:{borderWidth:.5,borderColor:ug.color.border,areaColor:ug.color.background},emphasis:{label:{show:!0,color:ug.color.primary},itemStyle:{areaColor:ug.color.highlight}},select:{label:{show:!0,color:ug.color.primary},itemStyle:{color:ug.color.highlight}},nameProperty:"name"},e}(Hy);function hO(t){return 0===t.indexOf("i")}function dO(t){return pO(t.seriesGroup)===t&&!t.getHostGeoModel()}function pO(t){return t.f[0]}function fO(t,e){var n={};return t.eachRawSeriesByType(uO,function(i){var r=i.getHostGeoModel(),o=r?"o"+r.id:"i"+i.getMapType(),a=n[o]=n[o]||{f:[],r:[]};t.isSeriesFiltered(i)||e||a.f.push(i),a.r.push(i)}),n}var gO=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.type=uO,e}return n(e,t),e.prototype.render=function(t,e,n,i){if(!i||"mapToggleSelect"!==i.type||i.from!==this.uid){var r=this.group;if(r.removeAll(),!t.getHostGeoModel()){var o=this._mapDraw;o&&i&&"geoRoam"===i.type&&o.resetForLabelLayout(),i&&"geoRoam"===i.type&&"series"===i.componentType&&i.seriesId===t.id?o&&r.add(o.group):dO(t)?(o=o||(this._mapDraw=new eO(n)),r.add(o.group),o.draw(t,e,n,this,i)):this._clearMapDraw(),t.get("showLegendSymbol")&&e.getComponent("legend")&&this._renderSymbols(t)}}},e.prototype.__updateOnOwnRoam=function(t,e,n){var i=this._mapDraw;dO(e)&&i&&i.__updateOnOwnRoam(e)},e.prototype.remove=function(){this._clearMapDraw(),this.group.removeAll()},e.prototype.dispose=function(){this._clearMapDraw()},e.prototype._clearMapDraw=function(){this._mapDraw&&this._mapDraw.remove(),this._mapDraw=null},e.prototype._renderSymbols=function(t){var e=t.originalData,n=this.group;e.each(e.mapDimension("value"),function(i,r){if(!isNaN(i)){var o=e.getItemLayout(r);if(o&&o.point){var a=o.point,s=o.offset,l=new ah({style:{fill:t.getData().getVisual("style").fill},shape:{cx:a[0]+9*s,cy:a[1],r:3},silent:!0,z2:8+(s?0:11)});if(!s){var u=pO(t.seriesGroup).getData(),c=e.getName(r),h=u.indexOfName(c),d=e.getItemModel(r),p=d.getModel("label"),f=u.getItemGraphicEl(h);op(l,ap(d),{labelFetcher:{getFormattedLabel:function(e,n){return t.getFormattedLabel(h,n)}},defaultText:c}),l.disableLabelAnimation=!0,p.get("position")||l.setTextConfig({position:"bottom"}),f.onHoverStateChange=function(t){hc(l,t)}}n.add(l)}}})},e.type=uO,e}(tm),vO={geoJSON:{aspectScale:.75,invertLongitute:!0},geoSVG:{aspectScale:1,invertLongitute:!1}},yO=["lng","lat"],mO=function(t){function e(e,n,i){var r=t.call(this)||this;r.dimensions=yO,r.type="geo",r._nameCoordMap=mt(),r.name=e;var o=i.projection,a=eP(n,i.nameMap,i.nameProperty),s=QL(n),l=(r.resourceType=s?s.type:null,r.regions=a.regions),u=vO[s.type];r._clip=i.clip;var c,h=!o&&u.invertLongitute;if(r.view=new nP(h,EP(i.ecModel,i.api),r),r.map=n,r._regionsMap=a.regionsMap,r.regions=a.regions,r.projection=o,o)for(var d=0;d1?(f.width=p,f.height=p/x):(f.height=p,f.width=p*x),f.y=d[1]-f.height/2,f.x=d[0]-f.width/2;else{var b=t.getBoxLayoutParams();b.aspect=x,f=Qf(t,f=Jf(b,m),x)}fP(n,f.x,f.y,f.width,f.height),dP(n,t)}var bO=function(){function t(){this.dimensions=yO}return t.prototype.create=function(t,e){var n=[];function i(t){return{nameProperty:t.get("nameProperty"),aspectScale:t.get("aspectScale"),projection:t.get("projection"),clip:t.getShallow("clip",!0)}}return t.eachComponent("geo",function(r,o){var a=r.get("map"),s=new mO(a+o,a,k({nameMap:r.get("nameMap"),api:e,ecModel:t},i(r)));n.push(s),r.coordinateSystem=s,s.model=r,s.resize=_O,s.resize(r,e)}),t.eachSeries(function(t){Wf({targetModel:t,coordSysType:"geo",coordSysProvider:function(){var e=t.subType===uO?t.getHostGeoModel():t.getReferringComponents("geo",Ea).models[0];return e&&e.coordinateSystem},allowNotFound:!0})}),B(fO(t,!0),function(r,o){if(hO(o)){var a=r.r[0],s=[];B(r.r,function(t){s.push(t.get("nameMap")),t.seriesGroup=null});var l,u=o.slice(1),c=new mO(u,u,k({nameMap:A(s),api:e,ecModel:t},i(a)));B(r.r,function(t){l=at(l,t.get("scaleLimit"))}),n.push(c),c.resize=_O,c.resize(a,e),B(r.r,function(t){t.coordinateSystem=c,function(t,e){B(e.get("geoCoord"),function(e,n){t.addGeoCoord(n,e)})}(c,t)})}}),n},t.prototype.getFilledRegions=function(t,e,n,i){for(var r=(t||[]).slice(),o=mt(),a=0;a=0;){var o=e[n];o.hierNode.prelim+=i,o.hierNode.modifier+=i,r+=o.hierNode.change,i+=o.hierNode.shift+r}}(t);var o=(n[0].hierNode.prelim+n[n.length-1].hierNode.prelim)/2;r?(t.hierNode.prelim=r.hierNode.prelim+e(t,r),t.hierNode.modifier=t.hierNode.prelim-o):t.hierNode.prelim=o}else r&&(t.hierNode.prelim=r.hierNode.prelim+e(t,r));t.parentNode.hierNode.defaultAncestor=function(t,e,n,i){if(e){for(var r=t,o=t,a=o.parentNode.children[0],s=e,l=r.hierNode.modifier,u=o.hierNode.modifier,c=a.hierNode.modifier,h=s.hierNode.modifier;s=OO(s),o=RO(o),s&&o;){r=OO(r),a=RO(a),r.hierNode.ancestor=t;var d=s.hierNode.prelim+h-o.hierNode.prelim-u+i(s,o);d>0&&(EO(NO(s,t,n),t,d),u+=d,l+=d),h+=s.hierNode.modifier,u+=o.hierNode.modifier,l+=r.hierNode.modifier,c+=a.hierNode.modifier}s&&!OO(r)&&(r.hierNode.thread=s,r.hierNode.modifier+=h-l),o&&!RO(a)&&(a.hierNode.thread=o,a.hierNode.modifier+=u-c,n=t)}return n}(t,r,t.parentNode.hierNode.defaultAncestor||i[0],e)}function kO(t){var e=t.hierNode.prelim+t.parentNode.hierNode.modifier;t.setLayout({x:e},!0),t.hierNode.modifier+=t.parentNode.hierNode.modifier}function LO(t){return arguments.length?t:zO}function PO(t,e){return t-=Math.PI/2,{x:e*Math.cos(t),y:e*Math.sin(t)}}function OO(t){var e=t.children;return e.length&&t.isExpand?e[e.length-1]:t.hierNode.thread}function RO(t){var e=t.children;return e.length&&t.isExpand?e[0]:t.hierNode.thread}function NO(t,e,n){return t.hierNode.ancestor.parentNode===e.parentNode?t.hierNode.ancestor:n}function EO(t,e,n){var i=n/(e.hierNode.i-t.hierNode.i);e.hierNode.change-=i,e.hierNode.shift+=n,e.hierNode.modifier+=n,e.hierNode.prelim+=n,t.hierNode.change+=i}function zO(t,e){return t.parentNode===e.parentNode?1:2}var BO=Pa();function VO(t){var e=t.mainData,n=t.datas;n||(n={main:e},t.datasAttr={main:"data"}),t.datas=t.mainData=null,YO(e,n,t),B(n,function(n){B(e.TRANSFERABLE_METHODS,function(e){n.wrapMethod(e,Y(GO,t))})}),e.wrapMethod("cloneShallow",Y(HO,t)),B(e.CHANGABLE_METHODS,function(n){e.wrapMethod(n,Y(FO,t))}),ct(n[e.dataType]===e)}function GO(t,e){if(BO(i=this).mainData===i){var n=k({},BO(this).datas);n[this.dataType]=e,YO(e,n,t)}else XO(e,this.dataType,BO(this).mainData,t);var i;return e}function FO(t,e){return t.struct&&t.struct.update(),e}function HO(t,e){return B(BO(e).datas,function(n,i){n!==e&&XO(n.cloneShallow(),i,e,t)}),e}function WO(t){var e=BO(this).mainData;return null==t||null==e?e:BO(e).datas[t]}function UO(){var t=BO(this).mainData;return null==t?[{data:t}]:V(W(BO(t).datas),function(e){return{type:e,data:BO(t).datas[e]}})}function YO(t,e,n){BO(t).datas={},B(e,function(e,i){XO(e,i,t,n)})}function XO(t,e,n,i){BO(n).datas[e]=t,BO(t).mainData=n,t.dataType=e,i.struct&&(t[i.structAttr]=i.struct,i.struct[i.datasAttr[e]]=t),t.getLinkedData=WO,t.getLinkedDataAll=UO}var ZO=function(){function t(t,e){this.depth=0,this.height=0,this.dataIndex=-1,this.children=[],this.viewChildren=[],this.isExpand=!1,this.name=t||"",this.hostTree=e}return t.prototype.isRemoved=function(){return this.dataIndex<0},t.prototype.eachNode=function(t,e,n){Z(t)&&(n=e,e=t,t=null),j(t=t||{})&&(t={order:t});var i,r=t.order||"preorder",o=this[t.attr||"children"];"preorder"===r&&(i=e.call(n,this));for(var a=0;!i&&ae&&(e=i.height)}this.height=e+1},t.prototype.getNodeById=function(t){if(this.getId()===t)return this;for(var e=0,n=this.children,i=n.length;e=0&&this.hostTree.data.setItemLayout(this.dataIndex,t,e)},t.prototype.getLayout=function(){return this.hostTree.data.getItemLayout(this.dataIndex)},t.prototype.getModel=function(t){if(!(this.dataIndex<0))return this.hostTree.data.getItemModel(this.dataIndex).getModel(t)},t.prototype.getLevelModel=function(){return(this.hostTree.levelModels||[])[this.depth]},t.prototype.setVisual=function(t,e){this.dataIndex>=0&&this.hostTree.data.setItemVisual(this.dataIndex,t,e)},t.prototype.getVisual=function(t){return this.hostTree.data.getItemVisual(this.dataIndex,t)},t.prototype.getRawIndex=function(){return this.hostTree.data.getRawIndex(this.dataIndex)},t.prototype.getId=function(){return this.hostTree.data.getId(this.dataIndex)},t.prototype.getChildIndex=function(){if(this.parentNode){for(var t=this.parentNode.children,e=0;e=0){var i=n.getData().tree.root,r=t.targetNode;if(j(r)&&(r=i.getNodeById(r)),r&&i.contains(r))return{node:r};var o=t.targetNodeId;if(null!=o&&(r=i.getNodeById(o)))return{node:r}}}function KO(t){for(var e=[];t;)(t=t.parentNode)&&e.push(t);return e.reverse()}function $O(t,e){return R(KO(t),e)>=0}function JO(t,e){for(var n=[];t;){var i=t.dataIndex;n.push({name:t.name,dataIndex:i,value:e.getRawValue(i)}),t=t.parentNode}return n.reverse(),n}var QO="tree",tR=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.hasSymbolVisual=!0,n.ignoreStyleOnData=!0,n}return n(e,t),e.prototype.getInitialData=function(t){var e={name:t.name,children:t.data},n=t.leaves||{},i=new kp(n,this,this.ecModel),r=jO.createTree(e,this,function(t){t.wrapMethod("getItemModel",function(t,e){var n=r.getNodeByDataIndex(e);return n&&n.children.length&&n.isExpand||(t.parentModel=i),t})});var o=0;r.eachNode("preorder",function(t){t.depth>o&&(o=t.depth)});var a=t.expandAndCollapse&&t.initialTreeDepth>=0?t.initialTreeDepth:o;return r.root.eachNode("preorder",function(t){var e=t.hostTree.data.getRawDataItem(t.dataIndex);t.isExpand=e&&null!=e.collapsed?!e.collapsed:t.depth<=a}),r.data},e.prototype.getOrient=function(){var t=this.get("orient");return"horizontal"===t?t="LR":"vertical"===t&&(t="TB"),t},e.prototype.formatTooltip=function(t,e,n){for(var i=this.getData().tree,r=i.root.children[0],o=i.getNodeByDataIndex(t),a=o.getValue(),s=o.name;o&&o!==r;)s=o.parentNode.name+"."+s,o=o.parentNode;return Ty("nameValue",{name:s,value:a,noValue:isNaN(a)||null==a})},e.prototype.getDataParams=function(e){var n=t.prototype.getDataParams.apply(this,arguments),i=this.getData().tree.getNodeByDataIndex(e);return n.treeAncestors=JO(i,this),n.collapsed=!i.isExpand,n},e.prototype.__ownRoamView=function(){return this.coordinateSystem},e.type="series."+QO,e.layoutMode="box",e.defaultOption={z:2,coordinateSystemUsage:"box",left:"12%",top:"12%",right:"12%",bottom:"12%",layout:"orthogonal",edgeShape:"curve",edgeForkPosition:"50%",roam:!1,roamTrigger:"global",nodeScaleRatio:.4,center:null,zoom:1,orient:"LR",symbol:"emptyCircle",symbolSize:7,expandAndCollapse:!0,initialTreeDepth:2,lineStyle:{color:ug.color.borderTint,width:1.5,curveness:.5},itemStyle:{color:"lightsteelblue",borderWidth:1.5},label:{show:!0},animationEasing:"linear",animationDuration:700,animationDurationUpdate:500},e}(Hy),eR=function(){this.parentPoint=[],this.childPoints=[]},nR=function(t){function e(e){return t.call(this,e)||this}return n(e,t),e.prototype.getDefaultStyle=function(){return{stroke:ug.color.neutral99,fill:null}},e.prototype.getDefaultShape=function(){return new eR},e.prototype.buildPath=function(t,e){var n=e.childPoints,i=n.length,r=e.parentPoint,o=n[0],a=n[i-1];if(1===i)return t.moveTo(r[0],r[1]),void t.lineTo(o[0],o[1]);var s=e.orient,l="TB"===s||"BT"===s?0:1,u=1-l,c=Vo(e.forkPosition,1),h=[];h[l]=r[l],h[u]=r[u]+(a[u]-r[u])*c,t.moveTo(r[0],r[1]),t.lineTo(h[0],h[1]),t.moveTo(o[0],o[1]),h[l]=o[l],t.lineTo(h[0],h[1]),h[l]=a[l],t.lineTo(h[0],h[1]),t.lineTo(a[0],a[1]);for(var d=1;dm.x)||(_-=Math.PI);var S=b?"left":"right",M=s.getModel("label"),I=M.get("rotate"),T=I*(Math.PI/180),C=v.getTextContent();C&&(v.setTextConfig({position:M.get("position")||S,rotation:null==I?-_:T,origin:"center"}),C.setStyle("verticalAlign","middle"))}var D=s.get(["emphasis","focus"]),A="relative"===D?xt(a.getAncestorsIndices(),a.getDescendantIndices()):"ancestor"===D?a.getAncestorsIndices():"descendant"===D?a.getDescendantIndices():null;A&&(Iu(n).focus=A),function(t,e,n,i,r,o,a,s){var l=e.getModel(),u=t.get("edgeShape"),c=t.get("layout"),h=t.getOrient(),d=t.get(["lineStyle","curveness"]),p=t.get("edgeForkPosition"),f=l.getModel("lineStyle").getLineStyle(),g=i.__edge;if("curve"===u)e.parentNode&&e.parentNode!==n&&(g||(g=i.__edge=new zh({shape:uR(c,h,d,r,r)})),rd(g,{shape:uR(c,h,d,o,a)},t));else if("polyline"===u)if("orthogonal"===c){if(e!==n&&e.children&&0!==e.children.length&&!0===e.isExpand){for(var v=e.children,y=[],m=0;m=0;o--)i.push(r[o])}}function hR(t,e){t.eachSeriesByType("tree",function(t){!function(t,e){var n=eg(t,e).refContainer,i=Jf(t.getBoxLayoutParams(),n);t.layoutInfo=i;var r=t.get("layout"),o=0,a=0,s=null;"radial"===r?(o=2*Math.PI,a=Math.min(i.height,i.width)/2,s=LO(function(t,e){return(t.parentNode===e.parentNode?1:2)/t.depth})):(o=i.width,a=i.height,s=LO());var l=t.getData().tree.root,u=l.children[0];if(u){!function(t){var e=t;e.hierNode={defaultAncestor:null,ancestor:e,prelim:0,modifier:0,change:0,shift:0,i:0,thread:null};for(var n,i,r=[e];n=r.pop();)if(i=n.children,n.isExpand&&i.length)for(var o=i.length-1;o>=0;o--){var a=i[o];a.hierNode={defaultAncestor:null,ancestor:a,prelim:0,modifier:0,change:0,shift:0,i:o,thread:null},r.push(a)}}(l),function(t,e,n){for(var i,r=[t],o=[];i=r.pop();)if(o.push(i),i.isExpand){var a=i.children;if(a.length)for(var s=0;sh.getLayout().x&&(h=t),t.depth>d.depth&&(d=t)});var p=c===h?1:s(c,h)/2,f=p-c.getLayout().x,g=0,v=0,y=0,m=0;if("radial"===r)g=o/(h.getLayout().x+p+f),v=a/(d.depth-1||1),cR(u,function(t){y=(t.getLayout().x+f)*g,m=(t.depth-1)*v;var e=PO(y,m);t.setLayout({x:e.x,y:e.y,rawX:y,rawY:m},!0)});else{var x=t.getOrient();"RL"===x||"LR"===x?(v=a/(h.getLayout().x+p+f),g=o/(d.depth-1||1),cR(u,function(t){m=(t.getLayout().x+f)*v,y="LR"===x?(t.depth-1)*g:o-(t.depth-1)*g,t.setLayout({x:y,y:m},!0)})):"TB"!==x&&"BT"!==x||(g=o/(h.getLayout().x+p+f),v=a/(d.depth-1||1),cR(u,function(t){y=(t.getLayout().x+f)*g,m="TB"===x?(t.depth-1)*v:a-(t.depth-1)*v,t.setLayout({x:y,y:m},!0)}))}}}(t,e)})}var dR=os(QO,function(t){t.eachSeriesByType(QO,function(t){var e=t.getData();e.tree.eachNode(function(t){var n=t.getModel().getModel("itemStyle").getItemStyle();k(e.ensureUniqueItemVisual(t.dataIndex,"style"),n)})})});var pR=["treemapZoomToNode","treemapRender","treemapMove"];function fR(t){var e=t.getData().tree,n={};e.eachNode(function(e){for(var i=e;i&&i.depth>1;)i=i.parentNode;var r=Rg(t.ecModel,i.name||i.dataIndex+"",n);e.setVisual("decal",r)})}var gR=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.preventUsingHoverLayer=!0,n}return n(e,t),e.prototype.getInitialData=function(t,e){var n={name:t.name,children:t.data};vR(n);var i=t.levels||[],r=this.designatedVisualItemStyle={},o=new kp({itemStyle:r},this,e);i=t.levels=function(t,e){var n,i,r=_a(e.get("color")),o=_a(e.get(["aria","decal","decals"]));if(!r)return;t=t||[],B(t,function(t){var e=new kp(t),r=e.get("color"),o=e.get("decal");(e.get(["itemStyle","color"])||r&&"none"!==r)&&(n=!0),(e.get(["itemStyle","decal"])||o&&"none"!==o)&&(i=!0)});var a=t[0]||(t[0]={});n||(a.color=r.slice());!i&&o&&(a.decal=o.slice());return t}(i,e);var a=V(i||[],function(t){return new kp(t,o,e)},this),s=jO.createTree(n,this,function(t){t.wrapMethod("getItemModel",function(t,e){var n=s.getNodeByDataIndex(e),i=n?a[n.depth]:null;return t.parentModel=i||o,t})});return s.data},e.prototype.optionUpdated=function(){this.resetViewRoot()},e.prototype.formatTooltip=function(t,e,n){var i=this.getData(),r=this.getRawValue(t);return Ty("nameValue",{name:i.getName(t),value:r})},e.prototype.getDataParams=function(e){var n=t.prototype.getDataParams.apply(this,arguments),i=this.getData().tree.getNodeByDataIndex(e);return n.treeAncestors=JO(i,this),n.treePathInfo=n.treeAncestors,n},e.prototype.setLayoutInfo=function(t){this.layoutInfo=this.layoutInfo||{},k(this.layoutInfo,t)},e.prototype.mapIdToIndex=function(t){var e=this._idIndexMap;e||(e=this._idIndexMap=mt(),this._idIndexMapCount=0);var n=e.get(t);return null==n&&e.set(t,n=this._idIndexMapCount++),n},e.prototype.getViewRoot=function(){return this._viewRoot},e.prototype.resetViewRoot=function(t){t?this._viewRoot=t:t=this._viewRoot;var e=this.getRawData().tree.root;t&&(t===e||e.contains(t))||(this._viewRoot=e)},e.prototype.enableAriaDecal=function(){fR(this)},e.type="series.treemap",e.layoutMode="box",e.defaultOption={progressive:0,coordinateSystemUsage:"box",left:ug.size.l,top:ug.size.xxxl,right:ug.size.l,bottom:ug.size.xxxl,sort:!0,clipWindow:"origin",squareRatio:.5*(1+Math.sqrt(5)),leafDepth:null,drillDownIcon:"▶",zoomToNodeRatio:.1024,scaleLimit:{max:5,min:.2},roam:!0,roamTrigger:"global",nodeClick:"zoomToNode",animation:!0,animationDurationUpdate:900,animationEasing:"quinticInOut",breadcrumb:{show:!0,height:22,left:"center",bottom:ug.size.m,emptyItemWidth:25,itemStyle:{color:ug.color.backgroundShade,textStyle:{color:ug.color.secondary}},emphasis:{itemStyle:{color:ug.color.background}}},label:{show:!0,distance:0,padding:5,position:"inside",color:ug.color.neutral00,overflow:"truncate"},upperLabel:{show:!1,position:[0,"50%"],height:20,overflow:"truncate",verticalAlign:"middle"},itemStyle:{color:null,colorAlpha:null,colorSaturation:null,borderWidth:0,gapWidth:0,borderColor:ug.color.neutral00,borderColorSaturation:null},emphasis:{upperLabel:{show:!0,position:[0,"50%"],overflow:"truncate",verticalAlign:"middle"}},visualDimension:0,visualMin:null,visualMax:null,color:[],colorAlpha:null,colorSaturation:null,colorMappingBy:"index",visibleMin:10,childrenVisibleMin:null,levels:[]},e}(Hy);function vR(t){var e=0;B(t.children,function(t){vR(t);var n=t.value;X(n)&&(n=n[0]),e+=n});var n=t.value;X(n)&&(n=n[0]),(null==n||isNaN(n))&&(n=e),n<0&&(n=0),X(t.value)?t.value[0]=n:t.value=n}var yR=function(){function t(t){this.group=new vo,t.add(this.group)}return t.prototype.render=function(t,e,n,i){var r=t.getModel("breadcrumb"),o=this.group;if(o.removeAll(),r.get("show")&&n){var a=r.getModel("itemStyle"),s=r.getModel("emphasis"),l=a.getModel("textStyle"),u=s.getModel(["itemStyle","textStyle"]),c=eg(t,e).refContainer,h={left:r.get("left"),right:r.get("right"),top:r.get("top"),bottom:r.get("bottom")},d={emptyItemWidth:r.get("emptyItemWidth"),totalWidth:0,renderList:[]},p=Jf(h,c);this._prepare(n,d,l),this._renderContent(t,d,p,a,s,l,u,i),ng(o,h,c)}},t.prototype._prepare=function(t,e,n){for(var i=t;i;i=i.parentNode){var r=Da(i.getModel().get("name"),""),o=n.getTextRect(r),a=Math.max(o.width+16,e.emptyItemWidth);e.totalWidth+=a+8,e.renderList.push({node:i,text:r,width:a})}},t.prototype._renderContent=function(t,e,n,i,r,o,a,s){for(var l=0,u=e.emptyItemWidth,c=t.get(["breadcrumb","height"]),h=e.totalWidth,d=e.renderList,p=r.getModel("itemStyle").getItemStyle(),f=d.length-1;f>=0;f--){var g=d[f],v=g.node,y=g.width,m=g.text;h>n.width&&(h-=y-u,y=u,m=null);var x=new Dh({shape:{points:mR(l,0,y,c,f===d.length-1,0===f)},style:P(i.getItemStyle(),{lineJoin:"bevel"}),textContent:new du({style:sp(o,{text:m})}),textConfig:{position:"inside"},z2:1e5,onclick:Y(s,v)});x.disableLabelAnimation=!0,x.getTextContent().ensureState("emphasis").style=sp(a,{text:m}),x.ensureState("emphasis").style=p,kc(x,r.get("focus"),r.get("blurScope"),r.get("disabled")),this.group.add(x),xR(x,t,v),l+=y+8}},t.prototype.remove=function(){this.group.removeAll()},t}();function mR(t,e,n,i,r,o){var a=[[r?t:t-5,e],[t+n,e],[t+n,e+i],[r?t:t-5,e+i]];return!o&&a.splice(2,0,[t+n+5,e+i/2]),!r&&a.push([t,e+i/2]),a}function xR(t,e,n){Iu(t).eventData={componentType:"series",componentSubType:"treemap",componentIndex:e.componentIndex,seriesIndex:e.seriesIndex,seriesName:e.name,seriesType:"treemap",selfType:"breadcrumb",nodeData:{dataIndex:n&&n.dataIndex,name:n&&n.name},treePathInfo:n&&JO(n,e)}}var _R=function(){function t(){this._storage=[],this._elExistsMap={}}return t.prototype.add=function(t,e,n,i,r){return!this._elExistsMap[t.id]&&(this._elExistsMap[t.id]=!0,this._storage.push({el:t,target:e,duration:n,delay:i,easing:r}),!0)},t.prototype.finished=function(t){return this._finishedCallback=t,this},t.prototype.start=function(){for(var t=this,e=this._storage.length,n=function(){--e<=0&&(t._storage.length=0,t._elExistsMap={},t._finishedCallback&&t._finishedCallback())},i=0,r=this._storage.length;iqo&&(c=qo),o=r}ca[1]&&(a[1]=e)})):a=[NaN,NaN];return{sum:i,dataExtent:a}}(e,a,s);if(0===u.sum)return t.viewChildren=[];if(u.sum=function(t,e,n,i,r){if(!i)return n;for(var o=t.get("visibleMin"),a=r.length,s=a,l=a-1;l>=0;l--){var u=r["asc"===i?a-l-1:l].getValue();u/n*ei&&(i=a));var l=t.area*t.area,u=e*e*n;return l?bR(u*i/l,l/(u*r)):1/0}function LR(t,e,n,i,r){var o=e===n.width?0:1,a=1-o,s=["x","y"],l=["width","height"],u=n[s[o]],c=e?t.area/e:0;(r||c>n[l[a]])&&(c=n[l[a]]);for(var h=0,d=t.length;h3||Math.abs(t.dy)>3)){var e=this.seriesModel.getData().tree.root;if(!e)return;var n=e.getLayout();if(!n)return;this.api.dispatchAction({type:"treemapMove",from:this.uid,seriesId:this.seriesModel.id,rootRect:{x:n.x+t.dx,y:n.y+t.dy,width:n.width,height:n.height}})}},e.prototype._onZoom=function(t){var e=t.originX,n=t.originY,i=t.scale,r=this.seriesModel;if("animating"!==this._state){var o=r.getData().tree.root;if(!o)return;var a=o.getLayout();if(!a)return;var s=new Xe(a.x,a.y,a.width,a.height),l=r.layoutInfo,u=RR(l,a),c=u*i,h=(c=NR(c,r))/u,d=[1,0,0,1,0,0];Te(d,d,[-(e-=l.x),-(n-=l.y)]),De(d,d,[h,h]),Te(d,d,[e,n]),s.applyTransform(d),this.api.dispatchAction({type:"treemapRender",from:this.uid,seriesId:this.seriesModel.id,rootRect:{x:s.x,y:s.y,width:s.width,height:s.height}})}},e.prototype._initEvents=function(t){var e=this;t.on("click",function(t){if("ready"===e._state){var n=e.seriesModel.get("nodeClick",!0);if(n){var i=e.findTarget(t.offsetX,t.offsetY);if(i){var r=i.node;if(r.getLayout().isLeafRoot)e._rootToNode(i);else if("zoomToNode"===n)e._zoomToNode(i);else if("link"===n){var o=r.hostTree.data.getItemModel(r.dataIndex),a=o.get("link",!0),s=o.get("target",!0)||"blank";a&&Ef(a,s)}}}}},this)},e.prototype._renderBreadcrumb=function(t,e,n){var i=this;n||(n=null!=t.get("leafDepth",!0)?{node:t.getViewRoot()}:this.findTarget(e.getWidth()/2,e.getHeight()/2))||(n={node:t.getData().tree.root}),(this._breadcrumb||(this._breadcrumb=new yR(this.group))).render(t,e,n.node,function(e){"animating"!==i._state&&($O(t.getViewRoot(),e)?i._rootToNode({node:e}):i._zoomToNode({node:e}))})},e.prototype.remove=function(){this._clearController(),this._containerGroup&&this._containerGroup.removeAll(),this._storage={nodeGroup:[],background:[],content:[]},this._state="ready",this._breadcrumb&&this._breadcrumb.remove()},e.prototype.dispose=function(){this._clearController()},e.prototype._zoomToNode=function(t){this.api.dispatchAction({type:"treemapZoomToNode",from:this.uid,seriesId:this.seriesModel.id,targetNode:t.node})},e.prototype._rootToNode=function(t){this.api.dispatchAction({type:"treemapRootToNode",from:this.uid,seriesId:this.seriesModel.id,targetNode:t.node})},e.prototype.findTarget=function(t,e){var n;return this.seriesModel.getViewRoot().eachNode({attr:"viewChildren",order:"preorder"},function(i){var r=this._storage.background[i.getRawIndex()];if(r){var o=r.transformCoordToLocal(t,e),a=r.shape;if(!(a.x<=o[0]&&o[0]<=a.x+a.width&&a.y<=o[1]&&o[1]<=a.y+a.height))return!1;n={node:i,offsetX:o[0],offsetY:o[1]}}},this),n},e.type="treemap",e}(tm);var UR=B,YR=$,XR=-1,ZR=function(){function t(e){var n=e.mappingMethod,i=e.type,r=this.option=C(e);this.type=i,this.mappingMethod=n,this._normalizeData=iN[n];var o=t.visualHandlers[i];this.applyVisual=o.applyVisual,this.getColorMapper=o.getColorMapper,this._normalizedToVisual=o._normalizedToVisual[n],"piecewise"===n?(jR(r),function(t){var e=t.pieceList;t.hasSpecialVisual=!1,B(e,function(e,n){e.originIndex=n,null!=e.visual&&(t.hasSpecialVisual=!0)})}(r)):"category"===n?r.categories?function(t){var e=t.categories,n=t.categoryMap={},i=t.visual;if(UR(e,function(t,e){n[t]=e}),!X(i)){var r=[];$(i)?UR(i,function(t,e){var i=n[e];r[null!=i?i:XR]=t}):r[-1]=i,i=nN(t,r)}for(var o=e.length-1;o>=0;o--)null==i[o]&&(delete n[e[o]],e.pop())}(r):jR(r,!0):(ct("linear"!==n||r.dataExtent),jR(r))}return t.prototype.mapValueToVisual=function(t){var e=this._normalizeData(t);return this._normalizedToVisual(e,t)},t.prototype.getNormalizer=function(){return U(this._normalizeData,this)},t.listVisualTypes=function(){return W(t.visualHandlers)},t.isValidType=function(e){return t.visualHandlers.hasOwnProperty(e)},t.eachVisual=function(t,e,n){$(t)?B(t,e,n):e.call(n,t)},t.mapVisual=function(e,n,i){var r,o=X(e)?[]:$(e)?{}:(r=!0,null);return t.eachVisual(e,function(t,e){var a=n.call(i,t,e);r?o=a:o[e]=a}),o},t.retrieveVisuals=function(e){var n,i={};return e&&UR(t.visualHandlers,function(t,r){e.hasOwnProperty(r)&&(i[r]=e[r],n=!0)}),n?i:null},t.prepareVisualTypes=function(t){if(X(t))t=t.slice();else{if(!YR(t))return[];var e=[];UR(t,function(t,n){e.push(n)}),t=e}return t.sort(function(t,e){return"color"===e&&"color"!==t&&0===t.indexOf("color")?1:-1}),t},t.dependsOn=function(t,e){return"color"===e?!(!t||0!==t.indexOf(e)):t===e},t.findPieceIndex=function(t,e,n){for(var i,r=1/0,o=0,a=e.length;ou[1]&&(u[1]=l);var c=e.get("colorMappingBy"),h={type:a.name,dataExtent:u,visual:a.range};"color"!==h.type||"index"!==c&&"id"!==c?h.mappingMethod="linear":(h.mappingMethod="category",h.loop=!0);var d=new ZR(h);return oN(d).drColorMappingBy=c,d}(0,r,o,0,u,p);B(p,function(t,e){if(t.depth>=n.length||t===n[t.depth]){var o=function(t,e,n,i,r,o){var a=k({},e);if(r){var s=r.type,l="color"===s&&oN(r).drColorMappingBy,u="index"===l?i:"id"===l?o.mapIdToIndex(n.getId()):n.getValue(t.get("visualDimension"));a[s]=r.mapValueToVisual(u)}return a}(r,u,t,e,f,i);sN(t,o,n,i)}})}else s=lN(u),c.fill=s}}function lN(t){var e=uN(t,"color");if(e){var n=uN(t,"colorAlpha"),i=uN(t,"colorSaturation");return i&&(e=wi(e,null,null,i)),n&&(e=Si(e,n)),e}}function uN(t,e){var n=t[e];if(null!=n&&"none"!==n)return n}function cN(t,e){var n=t.get(e);return X(n)&&n.length?{name:e,range:n}:null}function hN(t){return"_EC_"+t}var dN=function(){function t(t){this.type="graph",this.nodes=[],this.edges=[],this._nodesMap={},this._edgesMap={},this._directed=t||!1}return t.prototype.isDirected=function(){return this._directed},t.prototype.addNode=function(t,e){t=null==t?""+e:""+t;var n=this._nodesMap;if(!n[hN(t)]){var i=new pN(t,e);return i.hostGraph=this,this.nodes.push(i),n[hN(t)]=i,i}},t.prototype.getNodeByIndex=function(t){var e=this.data.getRawIndex(t);return this.nodes[e]},t.prototype.getNodeById=function(t){return this._nodesMap[hN(t)]},t.prototype.addEdge=function(t,e,n){var i=this._nodesMap,r=this._edgesMap;if(K(t)&&(t=this.nodes[t]),K(e)&&(e=this.nodes[e]),t instanceof pN||(t=i[hN(t)]),e instanceof pN||(e=i[hN(e)]),t&&e){var o=t.id+"-"+e.id,a=new fN(t,e,n);return a.hostGraph=this,this._directed&&(t.outEdges.push(a),e.inEdges.push(a)),t.edges.push(a),t!==e&&e.edges.push(a),this.edges.push(a),r[o]=a,a}},t.prototype.getEdgeByIndex=function(t){var e=this.edgeData.getRawIndex(t);return this.edges[e]},t.prototype.getEdge=function(t,e){t instanceof pN&&(t=t.id),e instanceof pN&&(e=e.id);var n=this._edgesMap;return this._directed?n[t+"-"+e]:n[t+"-"+e]||n[e+"-"+t]},t.prototype.eachNode=function(t,e){for(var n=this.nodes,i=n.length,r=0;r=0&&t.call(e,n[r],r)},t.prototype.eachEdge=function(t,e){for(var n=this.edges,i=n.length,r=0;r=0&&n[r].node1.dataIndex>=0&&n[r].node2.dataIndex>=0&&t.call(e,n[r],r)},t.prototype.breadthFirstTraverse=function(t,e,n,i){if(e instanceof pN||(e=this._nodesMap[hN(e)]),e){for(var r="out"===n?"outEdges":"in"===n?"inEdges":"edges",o=0;o=0&&n.node2.dataIndex>=0});for(r=0,o=i.length;r=0&&!t.hasKey(p)&&(t.set(p,!0),o.push(d.node1))}for(s=0;s=0&&!t.hasKey(m)&&(t.set(m,!0),a.push(y.node2))}}}return{edge:t.keys(),node:e.keys()}},t}(),fN=function(){function t(t,e,n){this.dataIndex=-1,this.node1=t,this.node2=e,this.dataIndex=null==n?-1:n}return t.prototype.getModel=function(t){if(!(this.dataIndex<0))return this.hostGraph.edgeData.getItemModel(this.dataIndex).getModel(t)},t.prototype.getAdjacentDataIndices=function(){return{edge:[this.dataIndex],node:[this.node1.dataIndex,this.node2.dataIndex]}},t.prototype.getTrajectoryDataIndices=function(){var t=mt(),e=mt();t.set(this.dataIndex,!0);for(var n=[this.node1],i=[this.node2],r=0;r=0&&!t.hasKey(u)&&(t.set(u,!0),n.push(l.node1))}for(r=0;r=0&&!t.hasKey(d)&&(t.set(d,!0),i.push(h.node2))}return{edge:t.keys(),node:e.keys()}},t}();function gN(t,e){return{getValue:function(n){var i=this[t][e];return i.getStore().get(i.getDimensionIndex(n||"value"),this.dataIndex)},setVisual:function(n,i){this.dataIndex>=0&&this[t][e].setItemVisual(this.dataIndex,n,i)},getVisual:function(n){return this[t][e].getItemVisual(this.dataIndex,n)},setLayout:function(n,i){this.dataIndex>=0&&this[t][e].setItemLayout(this.dataIndex,n,i)},getLayout:function(){return this[t][e].getItemLayout(this.dataIndex)},getGraphicEl:function(){return this[t][e].getItemGraphicEl(this.dataIndex)},getRawIndex:function(){return this[t][e].getRawIndex(this.dataIndex)}}}function vN(t,e,n,i,r){for(var o=new dN(i),a=0;a "+d)),u++)}var p,f=n.get("coordinateSystem");if("cartesian2d"===f||"polar"===f||"matrix"===f)p=$b(t,n);else{var g=Vf.get(f),v=g&&g.dimensions||[];R(v,"value")<0&&v.concat(["value"]);var y=Wb(t,{coordDimensions:v,encodeDefine:n.getEncode()}).dimensions;(p=new Hb(y,n)).initData(t)}var m=new Hb(["value"],n);return m.initData(l,s),r&&r(p,m),VO({mainData:p,struct:o,structAttr:"graph",datas:{node:p,edge:m},datasAttr:{node:"data",edge:"edgeData"}}),o.update(),o}E(pN,gN("hostGraph","data")),E(fN,gN("hostGraph","edgeData"));var yN="--\x3e",mN=function(t){return t.get("autoCurveness")||null},xN=function(t,e){var n=mN(t),i=20,r=[];if(K(n))i=n;else if(X(n))return void(t.__curvenessList=n);e>i&&(i=e);var o=i%2?i+2:i+3;r=[];for(var a=0;a "),value:r.value,noValue:null==r.value})}return By({series:this,dataIndex:t,multipleSeries:e})},e.prototype._updateCategoriesData=function(){var t=V(this.option.categories||[],function(t){return null!=t.value?t:k({value:0},t)}),e=new Hb(["value"],this);e.initData(t),this._categoriesData=e,this._categoriesModels=e.mapArray(function(t){return e.getItemModel(t)})},e.prototype.isAnimationEnabled=function(){return t.prototype.isAnimationEnabled.call(this)&&!("force"===this.get("layout")&&this.get(["force","layoutAnimation"]))},e.prototype.__ownRoamView=function(){var t=this.coordinateSystem;return TP(t)&&t},e.type="series."+MN,e.dependencies=["grid","polar","geo","singleAxis","calendar"],e.defaultOption={z:2,coordinateSystem:"view",legendHoverLink:!0,layout:null,circular:{rotateLabel:!1},force:{initLayout:null,repulsion:[0,50],gravity:.1,friction:.6,edgeLength:30,layoutAnimation:!0},left:"center",top:"center",symbol:"circle",symbolSize:10,edgeSymbol:["none","none"],edgeSymbolSize:10,edgeLabel:{position:"middle",distance:5},draggable:!1,roam:!1,center:null,zoom:1,nodeScaleRatio:.6,label:{show:!1,formatter:"{b}"},itemStyle:{},lineStyle:{color:ug.color.neutral50,width:1,opacity:.5},emphasis:{scale:!0,label:{show:!0}},select:{itemStyle:{borderColor:ug.color.primary}}},e}(Hy);function TN(t){return t instanceof Array||(t=[t,t]),t}var CN=os(MN,function(t){t.eachSeriesByType(MN,function(t){var e=t.getGraph(),n=t.getEdgeData(),i=TN(t.get("edgeSymbol")),r=TN(t.get("edgeSymbolSize"));n.setVisual("fromSymbol",i&&i[0]),n.setVisual("toSymbol",i&&i[1]),n.setVisual("fromSymbolSize",r&&r[0]),n.setVisual("toSymbolSize",r&&r[1]),n.setVisual("style",t.getModel("lineStyle").getLineStyle()),n.each(function(t){var i=n.getItemModel(t),r=e.getEdgeByIndex(t),o=TN(i.getShallow("symbol",!0)),a=TN(i.getShallow("symbolSize",!0)),s=i.getModel("lineStyle").getLineStyle(),l=n.ensureUniqueItemVisual(t,"style");switch(k(l,s),l.stroke){case"source":var u=r.node1.getVisual("style");l.stroke=u&&u.fill;break;case"target":u=r.node2.getVisual("style");l.stroke=u&&u.fill}o[0]&&r.setVisual("fromSymbol",o[0]),o[1]&&r.setVisual("toSymbol",o[1]),a[0]&&r.setVisual("fromSymbolSize",a[0]),a[1]&&r.setVisual("toSymbolSize",a[1])})})});function DN(t){var e=t.coordinateSystem;if(!e||"view"===e.type){var n=t.getGraph();n.eachNode(function(t){var e=t.getModel();t.setLayout([+e.get("x"),+e.get("y")])}),AN(n,t)}}function AN(t,e){t.eachEdge(function(t,n){var i=st(t.getModel().get(["lineStyle","curveness"]),-SN(t,e,n,!0),0),r=At(t.node1.getLayout()),o=At(t.node2.getLayout()),a=[r,o];+i&&a.push([(r[0]+o[0])/2-(r[1]-o[1])*i,(r[1]+o[1])/2-(o[0]-r[0])*i]),t.setLayout(a)})}var kN=os(MN,function(t,e){t.eachSeriesByType(MN,function(t){var e=t.get("layout"),n=t.coordinateSystem;if(n&&"view"!==n.type){var i=t.getData(),r=[];B(n.dimensions,function(t){r=r.concat(i.mapDimensionsAll(t))});for(var o=0;o0&&(m[0]=-m[0],m[1]=-m[1]);var _=y[0]<0?-1:1;if("start"!==i.__position&&"end"!==i.__position){var b=-Math.atan2(y[1],y[0]);u[0].8?"left":c[0]<-.8?"right":"center",d=c[1]>.8?"top":c[1]<-.8?"bottom":"middle";break;case"start":i.x=-c[0]*f+l[0],i.y=-c[1]*g+l[1],h=c[0]>.8?"right":c[0]<-.8?"left":"center",d=c[1]>.8?"bottom":c[1]<-.8?"top":"middle";break;case"insideStartTop":case"insideStart":case"insideStartBottom":i.x=f*_+l[0],i.y=l[1]+w,h=y[0]<0?"right":"left",i.originX=-f*_,i.originY=-w;break;case"insideMiddleTop":case"insideMiddle":case"insideMiddleBottom":case"middle":i.x=x[0],i.y=x[1]+w,h="center",i.originY=-w;break;case"insideEndTop":case"insideEnd":case"insideEndBottom":i.x=-f*_+u[0],i.y=u[1]+w,h=y[0]>=0?"right":"left",i.originX=f*_,i.originY=-w}i.scaleX=i.scaleY=r,i.setStyle({verticalAlign:i.__verticalAlign||d,align:i.__align||h})}}}function S(t,e){var n=t.__specifiedRotation;if(null==n){var i=a.tangentAt(e);t.attr("rotation",(1===e?-1:1)*Math.PI/2-Math.atan2(i[1],i[0]))}else t.attr("rotation",n)}},e}(vo),QN=function(){function t(t){this.group=new vo,this._LineCtor=t||JN}return t.prototype.updateData=function(t){var e=this;this._progressiveEls=null;var n=this,i=n.group,r=n._lineData;n._lineData=t,r||i.removeAll();var o=tE(t);t.diff(r).add(function(n){e._doAdd(t,n,o)}).update(function(n,i){e._doUpdate(r,t,i,n,o)}).remove(function(t){i.remove(r.getItemGraphicEl(t))}).execute()},t.prototype.updateLayout=function(){var t=this._lineData;t&&t.eachItemGraphicEl(function(e,n){e.updateLayout(t,n)},this)},t.prototype.incrementalPrepareUpdate=function(t){this._seriesScope=tE(t),this._lineData=null,this.group.removeAll()},t.prototype.incrementalUpdate=function(t,e,n){function i(t){t.isGroup||function(t){return t.animators&&t.animators.length>0}(t)||(t.incremental=n,t.ensureState("emphasis").hoverLayer=2)}this._progressiveEls=[];for(var r=t.start;r=0?i+=u:i-=u:f>=0?i-=u:i+=u}return i}function cE(t,e){var n=[],i=jn,r=[[],[],[]],o=[[],[]],a=[];e/=2,t.eachEdge(function(t,s){var l=t.getLayout(),u=t.getVisual("fromSymbol"),c=t.getVisual("toSymbol");l.__original||(l.__original=[At(l[0]),At(l[1])],l[2]&&l.__original.push(At(l[2])));var h=l.__original;if(null!=l[2]){if(Dt(r[0],h[0]),Dt(r[1],h[2]),Dt(r[2],h[1]),u&&"none"!==u){var d=PN(t.node1),p=uE(r,h[0],d*e);i(r[0][0],r[1][0],r[2][0],p,n),r[0][0]=n[3],r[1][0]=n[4],i(r[0][1],r[1][1],r[2][1],p,n),r[0][1]=n[3],r[1][1]=n[4]}if(c&&"none"!==c){d=PN(t.node2),p=uE(r,h[1],d*e);i(r[0][0],r[1][0],r[2][0],p,n),r[1][0]=n[1],r[2][0]=n[2],i(r[0][1],r[1][1],r[2][1],p,n),r[1][1]=n[1],r[2][1]=n[2]}Dt(l[0],r[0]),Dt(l[1],r[2]),Dt(l[2],r[1])}else{if(Dt(o[0],h[0]),Dt(o[1],h[1]),Ot(a,o[1],o[0]),Vt(a,a),u&&"none"!==u){d=PN(t.node1);Pt(o[0],o[0],a,d*e)}if(c&&"none"!==c){d=PN(t.node2);Pt(o[1],o[1],a,-d*e)}Dt(l[0],o[0]),Dt(l[1],o[1])}})}var hE=Pa();function dE(t,e){t&&(hE(t).bridge=e)}var pE=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.type=MN,e}return n(e,t),e.prototype.init=function(t,e){var n=new nC,i=new QN,r=this.group,o=new vo;this._controller=new uL(e.getZr()),o.add(n.group),o.add(i.group),r.add(o),this._symbolDraw=n,this._lineDraw=i,this._mainGroup=o,this._firstRender=!0},e.prototype.render=function(t,e,n){var i=this,r=PP(t),o=!1;this._model=t,this._api=n,this._active=!0;var a=this._mainGroup,s=this._getThumbnailInfo();s&&s.bridge.reset(n);var l=this._symbolDraw,u=this._lineDraw;r&&CP(a,2,r,this._firstRender?null:t),cE(t.getGraph(),LN(t));var c=t.getData();l.updateData(c);var h=t.getEdgeData();u.updateData(h),this._updateNodeAndLinkScale(),r&&FP(t,n,this._controller,function(e,n,i){return t.coordinateSystem.containPoint([n,i])},null),clearTimeout(this._layoutTimeout);var d=t.forceLayout,p=t.get(["force","layoutAnimation"]);d&&(o=!0,this._startForceLayoutIteration(d,n,p));var f=t.get("layout");c.graph.eachNode(function(e){var r=e.dataIndex,o=e.getGraphicEl(),a=e.getModel();if(o){o.off("drag").off("dragend");var s=a.get("draggable");s&&o.on("drag",function(a){switch(f){case"force":d.warmUp(),!i._layouting&&i._startForceLayoutIteration(d,n,p),d.setFixed(r),c.setItemLayout(r,[o.x,o.y]);break;case"circular":c.setItemLayout(r,[o.x,o.y]),e.setLayout({fixed:!0},!0),NN(t,"symbolSize",e,[a.offsetX,a.offsetY]),i.updateLayout(t);break;default:c.setItemLayout(r,[o.x,o.y]),AN(t.getGraph(),t),i.updateLayout(t)}}).on("dragend",function(){d&&d.setUnfixed(r)}),o.setDraggable(s,!!a.get("cursor")),"adjacency"===a.get(["emphasis","focus"])&&(Iu(o).focus=e.getAdjacentDataIndices())}}),c.graph.eachEdge(function(t){var e=t.getGraphicEl(),n=t.getModel().get(["emphasis","focus"]);e&&"adjacency"===n&&(Iu(e).focus={edge:[t.dataIndex],node:[t.node1.dataIndex,t.node2.dataIndex]})});var g="circular"===t.get("layout")&&t.get(["circular","rotateLabel"]),v=c.getLayout("cx"),y=c.getLayout("cy");c.graph.eachNode(function(t){zN(t,g,v,y)}),this._firstRender=!1,o||this._renderThumbnail(t,n,this._symbolDraw,this._lineDraw)},e.prototype.dispose=function(){this.remove(),this._controller&&this._controller.dispose()},e.prototype._startForceLayoutIteration=function(t,e,n){var i=this,r=!1;!function o(){t.step(function(t){i.updateLayout(i._model),!t&&r||(r=!0,i._renderThumbnail(i._model,e,i._symbolDraw,i._lineDraw)),(i._layouting=!t)&&(n?i._layoutTimeout=setTimeout(o,16):o())})}()},e.prototype.__updateOnOwnRoam=function(t,e,n){var i=PP(e);this._active&&i&&(CP(this._mainGroup,2,i,null),XP(t)&&(this._updateNodeAndLinkScale(),cE(e.getGraph(),LN(e)),this._lineDraw.updateLayout(),n.updateLabelLayout()),this._updateThumbnailWindow())},e.prototype._updateNodeAndLinkScale=function(){var t=this._model,e=t.getData(),n=LN(t);e.eachItemGraphicEl(function(t,e){t&&t.setSymbolScale(n)})},e.prototype.updateLayout=function(t){this._active&&(cE(t.getGraph(),LN(t)),this._symbolDraw.updateLayout(),this._lineDraw.updateLayout())},e.prototype.remove=function(){this._active=!1,clearTimeout(this._layoutTimeout),this._layouting=!1,this._layoutTimeout=null,this._symbolDraw&&this._symbolDraw.remove(),this._lineDraw&&this._lineDraw.remove(),this._controller&&this._controller.disable()},e.prototype._getThumbnailInfo=function(){var t=this._model,e=t.coordinateSystem;if("view"===e.type){var n=function(t){if(t)return hE(t).bridge}(t);if(n)return{bridge:n,coordSys:e}}},e.prototype._updateThumbnailWindow=function(){var t=this._getThumbnailInfo();t&&t.bridge.updateWindow(rP(null,t.coordSys),this._api)},e.prototype._renderThumbnail=function(t,e,n,i){var r=this._getThumbnailInfo();if(r){var o=new vo,a=n.group.children(),s=i.group.children(),l=new vo,u=new vo;o.add(u),o.add(l);for(var c=0;c "),value:i.value,noValue:null==i.value})}return Ty("nameValue",{name:i.name,value:i.value,noValue:null==i.value})},e.prototype.getDataParams=function(e,n){var i=t.prototype.getDataParams.call(this,e,n);if("node"===n){var r=this.getData(),o=this.getGraph().getNodeByIndex(e);if(null==i.name&&(i.name=r.getName(e)),null==i.value){var a=o.getLayout().value;i.value=a}}return i},e.type="series."+vE,e.defaultOption={z:2,coordinateSystem:"none",legendHoverLink:!0,colorBy:"data",left:0,top:0,right:0,bottom:0,width:null,height:null,center:["50%","50%"],radius:["70%","80%"],clockwise:!0,startAngle:90,endAngle:"auto",minAngle:0,padAngle:3,itemStyle:{borderRadius:[0,0,5,5]},lineStyle:{width:0,color:"source",opacity:.2},label:{show:!0,position:"outside",distance:5},emphasis:{focus:"adjacency",lineStyle:{opacity:.5}}},e}(Hy),mE=function(t){function e(e,n,i){var r=t.call(this)||this;Iu(r).dataType="node",r.z2=2;var o=new du;return r.setTextContent(o),r.updateData(e,n,i,!0),r}return n(e,t),e.prototype.updateData=function(t,e,n,i){var r=this,o=t.graph.getNodeByIndex(e),a=t.hostModel,s=o.getModel(),l=s.getModel("emphasis"),u=t.getItemLayout(e),c=k(eA(s.getModel("itemStyle"),u,!0),u),h=this;if(isNaN(c.startAngle))h.setShape(c);else{i?h.setShape(c):rd(h,{shape:c},a,e);var d=k(eA(s.getModel("itemStyle"),u,!0),u);r.setShape(d),r.useStyle(t.getItemVisual(e,"style")),Rc(r,s),this._updateLabel(a,s,o),t.setItemGraphicEl(e,h),Rc(h,s,"itemStyle");var p=l.get("focus");kc(this,"adjacency"===p?o.getAdjacentDataIndices():p,l.get("blurScope"),l.get("disabled"))}},e.prototype._updateLabel=function(t,e,n){var i=this.getTextContent(),r=n.getLayout(),o=(r.startAngle+r.endAngle)/2,a=Math.cos(o),s=Math.sin(o),l=e.getModel("label");i.ignore=!l.get("show");var u=ap(e),c=n.getVisual("style");op(i,u,{labelFetcher:{getFormattedLabel:function(n,i,r,o,a,s){return t.getFormattedLabel(n,i,"node",o,st(a,u.normal&&u.normal.get("formatter"),e.get("name")),s)}},labelDataIndex:n.dataIndex,defaultText:n.dataIndex+"",inheritColor:c.fill,defaultOpacity:c.opacity,defaultOutsidePosition:"startArc"});var h,d=l.get("position")||"outside",p=l.get("distance")||0;h="outside"===d?r.r+p:(r.r+r.r0)/2,this.textConfig={inside:"outside"!==d};var f="outside"!==d?l.get("align")||"center":a>0?"left":"right",g="outside"!==d?l.get("verticalAlign")||"middle":s>0?"top":"bottom";i.attr({x:a*h+r.cx,y:s*h+r.cy,rotation:0,style:{align:f,verticalAlign:g}})},e}(Sh),xE=function(t){function e(e,n,i,r){var o=t.call(this)||this;return Iu(o).dataType="edge",o.updateData(e,n,i,r,!0),o}return n(e,t),e.prototype.buildPath=function(t,e){t.moveTo(e.s1[0],e.s1[1]);var n=.7,i=e.clockwise;t.arc(e.cx,e.cy,e.r,e.sStartAngle,e.sEndAngle,!i),t.bezierCurveTo((e.cx-e.s2[0])*n+e.s2[0],(e.cy-e.s2[1])*n+e.s2[1],(e.cx-e.t1[0])*n+e.t1[0],(e.cy-e.t1[1])*n+e.t1[1],e.t1[0],e.t1[1]),t.arc(e.cx,e.cy,e.r,e.tStartAngle,e.tEndAngle,!i),t.bezierCurveTo((e.cx-e.t2[0])*n+e.t2[0],(e.cy-e.t2[1])*n+e.t2[1],(e.cx-e.s1[0])*n+e.s1[0],(e.cy-e.s1[1])*n+e.s1[1],e.s1[0],e.s1[1]),t.closePath()},e.prototype.updateData=function(t,e,n,i,r){var o=t.hostModel,a=e.graph.getEdgeByIndex(n),s=a.getLayout(),l=a.node1.getModel(),u=e.getItemModel(a.dataIndex),c=u.getModel("lineStyle"),h=u.getModel("emphasis"),d=h.get("focus"),p=k(eA(l.getModel("itemStyle"),s,!0),s),f=this;isNaN(p.sStartAngle)||isNaN(p.tStartAngle)?f.setShape(p):(r?(f.setShape(p),_E(f,a,t,c)):(cd(f),_E(f,a,t,c),rd(f,{shape:p},o,n)),kc(this,"adjacency"===d?a.getAdjacentDataIndices():d,h.get("blurScope"),h.get("disabled")),Rc(f,u,"lineStyle"),e.setItemGraphicEl(a.dataIndex,f))},e}(Kl);function _E(t,e,n,i){var r=e.node1,o=e.node2,a=t.style;switch(t.setStyle(i.getLineStyle()),i.get("color")){case"source":a.fill=n.getItemVisual(r.dataIndex,"style").fill,a.decal=r.getVisual("style").decal;break;case"target":a.fill=n.getItemVisual(o.dataIndex,"style").fill,a.decal=o.getVisual("style").decal;break;case"gradient":var s=n.getItemVisual(r.dataIndex,"style").fill,l=n.getItemVisual(o.dataIndex,"style").fill;if(j(s)&&j(l)){var u=t.shape,c=(u.s1[0]+u.s2[0])/2,h=(u.s1[1]+u.s2[1])/2,d=(u.t1[0]+u.t2[0])/2,p=(u.t1[1]+u.t2[1])/2;a.fill=new Hh(c,h,d,p,[{offset:0,color:s},{offset:1,color:l}],!0)}}}var bE=Math.PI/180,wE=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.type=vE,e}return n(e,t),e.prototype.init=function(t,e){},e.prototype.render=function(t,e,n){var i=t.getData(),r=this._data,o=this.group,a=-t.get("startAngle")*bE;if(i.diff(r).add(function(t){if(i.getItemLayout(t)){var e=new mE(i,t,a);Iu(e).dataIndex=t,o.add(e)}}).update(function(e,n){var s=r.getItemGraphicEl(n);i.getItemLayout(e)?(s?s.updateData(i,e,a):s=new mE(i,e,a),o.add(s)):s&&ud(s,t,n)}).remove(function(e){var n=r.getItemGraphicEl(e);n&&ud(n,t,e)}).execute(),!r){var s=t.get("center");this.group.scaleX=.01,this.group.scaleY=.01,this.group.originX=Vo(s[0],n.getWidth()),this.group.originY=Vo(s[1],n.getHeight()),od(this.group,{scaleX:1,scaleY:1},t)}this._data=i,this.renderEdges(t,a)},e.prototype.renderEdges=function(t,e){var n=t.getData(),i=t.getEdgeData(),r=this._edgeData,o=this.group;i.diff(r).add(function(t){var r=new xE(n,i,t,e);Iu(r).dataIndex=t,o.add(r)}).update(function(t,a){var s=r.getItemGraphicEl(a);s.updateData(n,i,t,e),o.add(s)}).remove(function(e){var n=r.getItemGraphicEl(e);n&&ud(n,t,e)}).execute(),this._edgeData=i},e.prototype.dispose=function(){},e.type=vE,e}(tm),SE=Math.PI/180,ME=os(vE,function(t,e){t.eachSeriesByType(vE,function(t){!function(t,e){var n=t.getData(),i=n.graph,r=t.getEdgeData();if(!r.count())return;var o=$f(t,e),a=o.cx,s=o.cy,l=o.r,u=o.r0,c=Math.max((t.get("padAngle")||0)*SE,0),h=Math.max((t.get("minAngle")||0)*SE,0),d=-t.get("startAngle")*SE,p=d+2*Math.PI,f=t.get("clockwise"),g=f?1:-1,v=[d,p];Dl(v,!f);var y=v[0],m=v[1]-y,x=0===n.getSum("value")&&0===r.getSum("value"),_=[],b=0;i.eachEdge(function(t){var e=x?1:t.getValue("value");x&&(e>0||h)&&(b+=2);var n=t.node1.dataIndex,i=t.node2.dataIndex;_[n]=(_[n]||0)+e,_[i]=(_[i]||0)+e});var w=0;if(i.eachNode(function(t){var e=t.getValue("value");isNaN(e)||(_[t.dataIndex]=Math.max(e,_[t.dataIndex]||0)),!x&&(_[t.dataIndex]>0||h)&&b++,w+=_[t.dataIndex]||0}),0===b||0===w)return;c*b>=Math.abs(m)&&(c=Math.max(0,(Math.abs(m)-h*b)/b));(c+h)*b>=Math.abs(m)&&(h=(Math.abs(m)-c*b)/b);var S=(m-c*b*g)/w,M=0,I=0,T=0;i.eachNode(function(t){var e=_[t.dataIndex]||0,n=S*(w?e:1)*g;Math.abs(n)I){var D=M/I;i.eachNode(function(t){var e=t.getLayout().angle;Math.abs(e)>=h?t.setLayout({angle:e*D,ratio:D},!0):t.setLayout({angle:h,ratio:0===h?1:e/h},!0)})}else i.eachNode(function(t){if(!C){var e=t.getLayout().angle;e-Math.min(e/T,1)*Mh&&h>0){var n=C?1:Math.min(e/T,1),i=e-h,r=Math.min(i,Math.min(A,M*n));A-=r,t.setLayout({angle:e-r,ratio:(e-r)/e},!0)}else h>0&&t.setLayout({angle:h,ratio:0===e?1:h/e},!0)}});var k=y,L=[];i.eachNode(function(t){var e=Math.max(t.getLayout().angle,h);t.setLayout({cx:a,cy:s,r0:u,r:l,startAngle:k,endAngle:k+e*g,clockwise:f},!0),L[t.dataIndex]=k,k+=(e+c)*g}),i.eachEdge(function(t){var e=x?1:t.getValue("value"),n=S*(w?e:1)*g,i=t.node1.dataIndex,r=L[i]||0,o=r+Math.abs((t.node1.getLayout().ratio||1)*n)*g,l=[a+u*Math.cos(r),s+u*Math.sin(r)],c=[a+u*Math.cos(o),s+u*Math.sin(o)],h=t.node2.dataIndex,d=L[h]||0,p=d+Math.abs((t.node2.getLayout().ratio||1)*n)*g,v=[a+u*Math.cos(d),s+u*Math.sin(d)],y=[a+u*Math.cos(p),s+u*Math.sin(p)];t.setLayout({s1:l,s2:c,sStartAngle:r,sEndAngle:o,t1:v,t2:y,tStartAngle:d,tEndAngle:p,cx:a,cy:s,r:u,value:e,clockwise:f}),L[i]=o,L[h]=p})}(t,e)})});var IE=function(){this.angle=0,this.width=10,this.r=10,this.x=0,this.y=0},TE=function(t){function e(e){var n=t.call(this,e)||this;return n.type="pointer",n}return n(e,t),e.prototype.getDefaultShape=function(){return new IE},e.prototype.buildPath=function(t,e){var n=Math.cos,i=Math.sin,r=e.r,o=e.width,a=e.angle,s=e.x-n(a)*o*(o>=r/3?1:2),l=e.y-i(a)*o*(o>=r/3?1:2);a=e.angle-Math.PI/2,t.moveTo(s,l),t.lineTo(e.x+n(a)*o,e.y+i(a)*o),t.lineTo(e.x+n(e.angle)*r,e.y+i(e.angle)*r),t.lineTo(e.x-n(a)*o,e.y-i(a)*o),t.lineTo(s,l)},e}(Kl);function CE(t,e){var n=null==t?"":t+"";return e&&(j(e)?n=e.replace("{value}",n):Z(e)&&(n=e(t))),n}var DE=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){this.group.removeAll();var i=t.get(["axisLine","lineStyle","color"]),r=function(t,e){var n=t.get("center"),i=e.getWidth(),r=e.getHeight(),o=Math.min(i,r);return{cx:Vo(n[0],e.getWidth()),cy:Vo(n[1],e.getHeight()),r:Vo(t.get("radius"),o/2)}}(t,n);this._renderMain(t,e,n,i,r),this._data=t.getData()},e.prototype.dispose=function(){},e.prototype._renderMain=function(t,e,n,i,r){var o=this.group,a=t.get("clockwise"),s=-t.get("startAngle")/180*Math.PI,l=-t.get("endAngle")/180*Math.PI,u=t.getModel("axisLine"),c=u.get("roundCap")?JD:Sh,h=u.get("show"),d=u.getModel("lineStyle"),p=d.get("width"),f=[s,l];Dl(f,!a);for(var g=(l=f[1])-(s=f[0]),v=s,y=[],m=0;h&&m=t&&(0===e?0:i[e-1][0])Math.PI/2&&(B+=Math.PI):"tangential"===z?B=-M-Math.PI/2:K(z)&&(B=z*Math.PI/180),0===B?h.add(new du({style:sp(x,{text:O,x:N,y:E,verticalAlign:c<-.8?"top":c>.8?"bottom":"middle",align:u<-.4?"left":u>.4?"right":"center"},{inheritColor:R}),silent:!0})):h.add(new du({style:sp(x,{text:O,x:N,y:E,verticalAlign:"middle",align:"center"},{inheritColor:R}),silent:!0,originX:N,originY:E,rotation:B}))}if(m.get("show")&&k!==_){P=(P=m.get("distance"))?P+l:l;for(var V=0;V<=b;V++){u=Math.cos(M),c=Math.sin(M);var G=new Oh({shape:{x1:u*(f-P)+d,y1:c*(f-P)+p,x2:u*(f-S-P)+d,y2:c*(f-S-P)+p},silent:!0,style:D});"auto"===D.stroke&&G.setStyle({stroke:i((k+V/b)/_)}),h.add(G),M+=T}M-=T}else M+=I}},e.prototype._renderPointer=function(t,e,n,i,r,o,a,s,l){var u=this.group,c=this._data,h=this._progressEls,d=[],p=t.get(["pointer","show"]),f=t.getModel("progress"),g=f.get("show"),v=t.getData(),y=v.mapDimension("value"),m=+t.get("min"),x=+t.get("max"),_=[m,x],b=[o,a];function w(e,n){var i,o=v.getItemModel(e).getModel("pointer"),a=Vo(o.get("width"),r.r),s=Vo(o.get("length"),r.r),l=t.get(["pointer","icon"]),u=o.get("offsetCenter"),c=Vo(u[0],r.r),h=Vo(u[1],r.r),d=o.get("keepAspect");return(i=l?xx(l,c-a/2,h-s,a,s,null,d):new TE({shape:{angle:-Math.PI/2,width:a,r:s,x:c,y:h}})).rotation=-(n+Math.PI/2),i.x=r.cx,i.y=r.cy,i}function S(t,e){var n=f.get("roundCap")?JD:Sh,i=f.get("overlap"),a=i?f.get("width"):l/v.count(),u=i?r.r-a:r.r-(t+1)*a,c=i?r.r:r.r-t*a,h=new n({shape:{startAngle:o,endAngle:e,cx:r.cx,cy:r.cy,clockwise:s,r0:u,r:c}});return i&&(h.z2=Bo(v.get(y,t),[m,x],[100,0],!0)),h}(g||p)&&(v.diff(c).add(function(e){var n=v.get(y,e);if(p){var i=w(e,o);od(i,{rotation:-((isNaN(+n)?b[0]:Bo(n,_,b,!0))+Math.PI/2)},t),u.add(i),v.setItemGraphicEl(e,i)}if(g){var r=S(e,o),a=f.get("clip");od(r,{shape:{endAngle:Bo(n,_,b,a)}},t),u.add(r),Tu(t.seriesIndex,v.dataType,e,r),d[e]=r}}).update(function(e,n){var i=v.get(y,e);if(p){var r=c.getItemGraphicEl(n),a=r?r.rotation:o,s=w(e,a);s.rotation=a,rd(s,{rotation:-((isNaN(+i)?b[0]:Bo(i,_,b,!0))+Math.PI/2)},t),u.add(s),v.setItemGraphicEl(e,s)}if(g){var l=h[n],m=S(e,l?l.shape.endAngle:o),x=f.get("clip");rd(m,{shape:{endAngle:Bo(i,_,b,x)}},t),u.add(m),Tu(t.seriesIndex,v.dataType,e,m),d[e]=m}}).execute(),v.each(function(t){var e=v.getItemModel(t),n=e.getModel("emphasis"),r=n.get("focus"),o=n.get("blurScope"),a=n.get("disabled"),s=i(Bo(v.get(y,t),_,[0,1],!0));if(p){var l=v.getItemGraphicEl(t),u=v.getItemVisual(t,"style"),c=u.fill;if(l instanceof eu){var h=l.style;l.useStyle(k({image:h.image,x:h.x,y:h.y,width:h.width,height:h.height},u))}else l.useStyle(u),"pointer"!==l.type&&l.setColor(c);l.setStyle(e.getModel(["pointer","itemStyle"]).getItemStyle()),"auto"===l.style.fill&&l.setStyle("fill",s),l.z2EmphasisLift=0,Rc(l,e),kc(l,r,o,a)}if(g){var f=d[t];f.useStyle(v.getItemVisual(t,"style")),f.setStyle(e.getModel(["progress","itemStyle"]).getItemStyle()),"auto"===f.style.fill&&f.setStyle("fill",s),f.z2EmphasisLift=0,Rc(f,e),kc(f,r,o,a)}}),this._progressEls=d)},e.prototype._renderAnchor=function(t,e){var n=t.getModel("anchor");if(n.get("show")){var i=n.get("size"),r=n.get("icon"),o=n.get("offsetCenter"),a=n.get("keepAspect"),s=xx(r,e.cx-i/2+Vo(o[0],e.r),e.cy-i/2+Vo(o[1],e.r),i,i,null,a);s.z2=n.get("showAbove")?1:0,s.setStyle(n.getModel("itemStyle").getItemStyle()),this.group.add(s)}},e.prototype._renderTitleAndDetail=function(t,e,n,i,r){var o=this,a=t.getData(),s=a.mapDimension("value"),l=+t.get("min"),u=+t.get("max"),c=new vo,h=[],d=[],p=t.isAnimationEnabled(),f=t.get(["pointer","showAbove"]);a.diff(this._data).add(function(t){h[t]=new du({silent:!0}),d[t]=new du({silent:!0})}).update(function(t,e){h[t]=o._titleEls[e],d[t]=o._detailEls[e]}).execute(),a.each(function(e){var n=a.getItemModel(e),o=a.get(s,e),g=new vo,v=i(Bo(o,[l,u],[0,1],!0)),y=n.getModel("title");if(y.get("show")){var m=y.get("offsetCenter"),x=r.cx+Vo(m[0],r.r),_=r.cy+Vo(m[1],r.r);(D=h[e]).attr({z2:f?0:2,style:sp(y,{x:x,y:_,text:a.getName(e),align:"center",verticalAlign:"middle"},{inheritColor:v})}),g.add(D)}var b=n.getModel("detail");if(b.get("show")){var w=b.get("offsetCenter"),S=r.cx+Vo(w[0],r.r),M=r.cy+Vo(w[1],r.r),I=Vo(b.get("width"),r.r),T=Vo(b.get("height"),r.r),C=t.get(["progress","show"])?a.getItemVisual(e,"style").fill:v,D=d[e],A=b.get("formatter");D.attr({z2:f?0:2,style:sp(b,{x:S,y:M,text:CE(o,A),width:isNaN(I)?null:I,height:isNaN(T)?null:T,align:"center",verticalAlign:"middle"},{inheritColor:C})}),gp(D,{normal:b},o,function(t){return CE(t,A)}),p&&vp(D,e,a,t,{getFormattedLabel:function(t,e,n,i,r,a){return CE(a?a.interpolatedValue:o,A)}}),g.add(D)}c.add(g)}),this.group.add(c),this._titleEls=h,this._detailEls=d},e.type="gauge",e}(tm),AE=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.visualStyleAccessPath="itemStyle",n}return n(e,t),e.prototype.getInitialData=function(t,e){return bA(this,["value"])},e.type="series.gauge",e.defaultOption={z:2,colorBy:"data",center:["50%","50%"],legendHoverLink:!0,radius:"75%",startAngle:225,endAngle:-45,clockwise:!0,min:0,max:100,splitNumber:10,axisLine:{show:!0,roundCap:!1,lineStyle:{color:[[1,ug.color.neutral10]],width:10}},progress:{show:!1,overlap:!0,width:10,roundCap:!1,clip:!0},splitLine:{show:!0,length:10,distance:10,lineStyle:{color:ug.color.axisTick,width:3,type:"solid"}},axisTick:{show:!0,splitNumber:5,length:6,distance:10,lineStyle:{color:ug.color.axisTickMinor,width:1,type:"solid"}},axisLabel:{show:!0,distance:15,color:ug.color.axisLabel,fontSize:12,rotate:0},pointer:{icon:null,offsetCenter:[0,0],show:!0,showAbove:!0,length:"60%",width:6,keepAspect:!1},anchor:{show:!1,showAbove:!1,size:6,icon:"circle",offsetCenter:[0,0],keepAspect:!1,itemStyle:{color:ug.color.neutral00,borderWidth:0,borderColor:ug.color.theme[0]}},title:{show:!0,offsetCenter:[0,"20%"],color:ug.color.secondary,fontSize:16,valueAnimation:!1},detail:{show:!0,backgroundColor:ug.color.transparent,borderWidth:0,borderColor:ug.color.neutral40,width:100,height:null,padding:[5,10],offsetCenter:[0,"40%"],color:ug.color.primary,fontSize:30,fontWeight:"bold",lineHeight:30,valueAnimation:!1}},e}(Hy);var kE="funnel",LE=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(e){t.prototype.init.apply(this,arguments),this.legendVisualProvider=new SA(U(this.getData,this),U(this.getRawData,this)),this._defaultLabelLine(e)},e.prototype.getInitialData=function(t,e){return bA(this,{coordDimensions:["value"],encodeDefaulter:Y(Sg,this)})},e.prototype._defaultLabelLine=function(t){ba(t,"labelLine",["show"]);var e=t.labelLine,n=t.emphasis.labelLine;e.show=e.show&&t.label.show,n.show=n.show&&t.emphasis.label.show},e.prototype.getDataParams=function(e){var n=this.getData(),i=t.prototype.getDataParams.call(this,e),r=n.mapDimension("value"),o=n.getSum(r);return i.percent=o?+(n.get(r,e)/o*100).toFixed(2):0,i.$vars.push("percent"),i},e.type="series."+kE,e.defaultOption={coordinateSystemUsage:"box",z:2,legendHoverLink:!0,colorBy:"data",left:80,top:60,right:80,bottom:65,minSize:"0%",maxSize:"100%",sort:"descending",orient:"vertical",gap:0,funnelAlign:"center",label:{show:!0,position:"outer"},labelLine:{show:!0,length:20,lineStyle:{width:1}},itemStyle:{borderColor:ug.color.neutral00,borderWidth:1},emphasis:{label:{show:!0}},select:{itemStyle:{borderColor:ug.color.primary}}},e}(Hy),PE=["itemStyle","opacity"],OE=function(t){function e(e,n){var i=t.call(this)||this,r=i,o=new kh,a=new du;return r.setTextContent(a),i.setTextGuideLine(o),i.updateData(e,n,!0),i}return n(e,t),e.prototype.updateData=function(t,e,n){var i=this,r=t.hostModel,o=t.getItemModel(e),a=t.getItemLayout(e),s=o.getModel("emphasis"),l=o.get(PE);l=null==l?1:l,n||cd(i),i.useStyle(t.getItemVisual(e,"style")),i.style.lineJoin="round",n?(i.setShape({points:a.points}),i.style.opacity=0,od(i,{style:{opacity:l}},r,e)):rd(i,{style:{opacity:l},shape:{points:a.points}},r,e),Rc(i,o),this._updateLabel(t,e),kc(this,s.get("focus"),s.get("blurScope"),s.get("disabled"))},e.prototype._updateLabel=function(t,e){var n=this,i=this.getTextGuideLine(),r=n.getTextContent(),o=t.hostModel,a=t.getItemModel(e),s=t.getItemLayout(e).label,l=t.getItemVisual(e,"style"),u=l.fill;op(r,ap(a),{labelFetcher:t.hostModel,labelDataIndex:e,defaultOpacity:l.opacity,defaultText:t.getName(e)},{normal:{align:s.textAlign,verticalAlign:s.verticalAlign}});var c="inherit"===a.getModel("label").get("color")?u:null;n.setTextConfig({local:!0,inside:!!s.inside,insideStroke:c,outsideFill:c});var h=s.linePoints;i.setShape({points:h}),n.textGuideLineConfig={anchor:h?new Pe(h[0][0],h[0][1]):null},rd(r,{style:{x:s.x,y:s.y}},o,e),r.attr({rotation:s.rotation,originX:s.x,originY:s.y,z2:10}),ZM(n,jM(a),{stroke:u})},e}(Dh),RE=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.type=kE,e.ignoreLabelLineUpdate=!0,e}return n(e,t),e.prototype.render=function(t,e,n){var i=t.getData(),r=this._data,o=this.group;i.diff(r).add(function(t){var e=new OE(i,t);i.setItemGraphicEl(t,e),o.add(e)}).update(function(t,e){var n=r.getItemGraphicEl(e);n.updateData(i,t),o.add(n),i.setItemGraphicEl(t,n)}).remove(function(e){ud(r.getItemGraphicEl(e),t,e)}).execute(),this._data=i},e.prototype.remove=function(){this.group.removeAll(),this._data=null},e.prototype.dispose=function(){},e.type=kE,e}(tm);var NE=os(kE,function(t,e){t.eachSeriesByType(kE,function(t){var n=t.getData(),i=n.mapDimension("value"),r=t.get("sort"),o=eg(t,e),a=Jf(t.getBoxLayoutParams(),o.refContainer),s=EE(t),l=a.width,u=a.height,c=function(t,e){for(var n=t.mapDimension("value"),i=t.mapArray(n,function(t){return t}),r=[],o="ascending"===e,a=0,s=t.count();a-1&&(l="left"),e&&R(["left","right"],l)>-1&&(l="bottom")),"left"===l?(p=(h[3][0]+h[0][0])/2,f=(h[3][1]+h[0][1])/2,r=(g=p-y)-5,i="right"):"right"===l?(p=(h[1][0]+h[2][0])/2,f=(h[1][1]+h[2][1])/2,r=(g=p+y)+5,i="left"):"top"===l?(p=(h[3][0]+h[0][0])/2,o=(v=(f=(h[3][1]+h[0][1])/2)-y)-5,i="center"):"bottom"===l?(p=(h[1][0]+h[2][0])/2,o=(v=(f=(h[1][1]+h[2][1])/2)+y)+5,i="center"):"rightTop"===l?(p=e?h[3][0]:h[1][0],f=e?h[3][1]:h[1][1],e?(o=(v=f-y)-5,i="center"):(r=(g=p+y)+5,i="top")):"rightBottom"===l?(p=h[2][0],f=h[2][1],e?(o=(v=f+y)+5,i="center"):(r=(g=p+y)+5,i="bottom")):"leftTop"===l?(p=h[0][0],f=e?h[0][1]:h[1][1],e?(o=(v=f-y)-5,i="center"):(r=(g=p-y)-5,i="right")):"leftBottom"===l?(p=e?h[1][0]:h[3][0],f=e?h[1][1]:h[2][1],e?(o=(v=f+y)+5,i="center"):(r=(g=p-y)-5,i="right")):(p=(h[1][0]+h[2][0])/2,f=(h[1][1]+h[2][1])/2,e?(o=(v=f+y)+5,i="center"):(r=(g=p+y)+5,i="left")),e?r=g=p:o=v=f,a=[[p,f],[g,v]]}c.label={linePoints:a,x:r,y:o,verticalAlign:"middle",textAlign:i,inside:d}})}(n)})});function EE(t){return"horizontal"===t.get("orient")}var zE=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n._dataGroup=new vo,n._initialized=!1,n}return n(e,t),e.prototype.init=function(){this.group.add(this._dataGroup)},e.prototype.render=function(t,e,n,i){var r=this._dataGroup;this._progressiveEls=[],r.removeAll();var o=t.getData(),a=this._data,s=t.coordinateSystem,l=s.dimensions,u=GE(t);if(o.diff(a).add(function(t){FE(VE(o,r,t,l,s),o,t,u)}).update(function(e,n){var i=a.getItemGraphicEl(n);r.add(i);var c=BE(o,e,l,s);o.setItemGraphicEl(e,i),rd(i,{shape:{points:c}},t,e),cd(i),FE(i,o,e,u)}).remove(function(t){var e=a.getItemGraphicEl(t);r.remove(e)}).execute(),!this._initialized){this._initialized=!0;var c=function(t,e,n){var i=t.model,r=t.getRect(),o=new lu({shape:{x:r.x,y:r.y,width:r.width,height:r.height}}),a="horizontal"===i.get("layout")?"width":"height";return o.setShape(a,0),od(o,{shape:{width:r.width,height:r.height}},e,n),o}(s,t,function(){setTimeout(function(){r.removeClipPath()})});r.setClipPath(c)}this._data=o},e.prototype.incrementalPrepareRender=function(t,e,n){this._initialized=!0,this._data=null,this._dataGroup.removeAll()},e.prototype.incrementalRender=function(t,e,n){for(var i=e.getData(),r=e.coordinateSystem,o=this._progressiveEls=[],a=t.start;a5)return;var i=this._model.coordinateSystem.getSlidedAxisExpandWindow([t.offsetX,t.offsetY]);"none"!==i.behavior&&this._dispatchExpand({axisExpandWindow:i.axisExpandWindow})}this._mouseDownPoint=null},mousemove:function(t){if(!this._mouseDownPoint&&KE(this,"mousemove")){var e=this._model,n=e.coordinateSystem.getSlidedAxisExpandWindow([t.offsetX,t.offsetY]),i=n.behavior;"jump"===i&&this._throttledDispatchExpand.debounceNextCall(e.get("axisExpandDebounce")),this._throttledDispatchExpand("none"===i?null:{axisExpandWindow:n.axisExpandWindow,animation:"jump"===i?null:{duration:0}})}}};function KE(t,e){var n=t._model;return n.get("axisExpandable")&&n.get("axisExpandTriggerOn")===e}var $E="parallel",JE=$E,QE=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(){t.prototype.init.apply(this,arguments),this.mergeOption({})},e.prototype.mergeOption=function(t){var e=this.option;t&&D(e,t,!0),this._initDimensions()},e.prototype.contains=function(t,e){var n=t.get("parallelIndex");return null!=n&&e.getComponent("parallel",n)===this},e.prototype.setAxisExpand=function(t){B(["axisExpandable","axisExpandCenter","axisExpandCount","axisExpandWidth","axisExpandWindow"],function(e){t.hasOwnProperty(e)&&(this.option[e]=t[e])},this)},e.prototype._initDimensions=function(){var t=this.dimensions=[],e=this.parallelAxisIndex=[];B(F(this.ecModel.queryComponents({mainType:"parallelAxis"}),function(t){return(t.get("parallelIndex")||0)===this.componentIndex},this),function(n){t.push("dim"+n.get("dim")),e.push(n.componentIndex)})},e.type=JE,e.dependencies=["parallelAxis"],e.layoutMode="box",e.defaultOption={z:0,left:80,top:60,right:80,bottom:60,layout:"horizontal",axisExpandable:!1,axisExpandCenter:null,axisExpandCount:0,axisExpandWidth:50,axisExpandRate:17,axisExpandDebounce:50,axisExpandSlideTriggerArea:[-.15,.05,.4],axisExpandTriggerOn:"click",parallelAxisDefault:null},e}(lg),tz=function(t){function e(e,n,i,r,o){var a=t.call(this,e,n,i)||this;return a.type=r||"value",a.axisIndex=o,a}return n(e,t),e.prototype.isHorizontal=function(){return"horizontal"!==this.coordinateSystem.getModel().get("layout")},e}(SM);function ez(t,e,n,i,r,o){t=t||0;var a=jo(n[1],-n[0]);if(null!=r&&(r=iz(r,[0,a])),null!=o&&(o=Math.max(o,null!=r?r:0)),"all"===i){var s=Math.abs(jo(e[1],-e[0]));s=iz(s,[0,a]),r=o=iz(s,[r,o]),i=0}e[0]=iz(e[0],n),e[1]=iz(e[1],n);var l=nz(e,i);e[i]+=t;var u,c=r||0,h=n.slice();return l.sign<0?h[0]=jo(h[0],c):h[1]=jo(h[1],-c),e[i]=iz(e[i],h),u=nz(e,i),null!=r&&(u.sign!==l.sign||u.spano&&(e[1-i]=jo(e[i],u.sign*o)),e}function nz(t,e){var n=t[e]-t[1-e];return{span:Math.abs(n),sign:n>0?-1:n<0?1:e?-1:1}}function iz(t,e){return Math.min(null!=e[1]?e[1]:1/0,Math.max(null!=e[0]?e[0]:-1/0,t))}var rz=function(){function t(t,e,n){this.type=$E,this._axesMap=mt(),this._axesLayout={},this.dimensions=t.dimensions,this._model=t,this._init(t,e,n)}return t.prototype._init=function(t,e,n){var i=t.dimensions,r=t.parallelAxisIndex;B(i,function(t,n){var i=r[n],o=e.getComponent("parallelAxis",i),a=Hw(o),s=this._axesMap.set(t,new tz(t,Ww(o,a,!1),[0,0],a,i));s.onBand=eS(s.scale,o),s.inverse=o.get("inverse"),o.axis=s,s.model=o,s.coordinateSystem=o.coordinateSystem=this},this)},t.prototype.update=function(t,e){B(this.dimensions,function(t){var e=this._axesMap.get(t);CS(e,1),RS(e)},this)},t.prototype.containPoint=function(t){var e=this._makeLayoutInfo(),n=e.axisBase,i=e.layoutBase,r=e.pixelDimIndex,o=t[1-r],a=t[r];return o>=n&&o<=n+e.axisLength&&a>=i&&a<=i+e.layoutLength},t.prototype.getModel=function(){return this._model},t.prototype.resize=function(t,e){var n=eg(t,e).refContainer;this._rect=Jf(t.getBoxLayoutParams(),n),this._layoutAxes()},t.prototype.getRect=function(){return this._rect},t.prototype._makeLayoutInfo=function(){var t,e=this._model,n=this._rect,i=["x","y"],r=["width","height"],o=e.get("layout"),a="horizontal"===o?0:1,s=n[r[a]],l=[0,s],u=this.dimensions.length,c=oz(e.get("axisExpandWidth"),l),h=oz(e.get("axisExpandCount")||0,[0,u]),d=e.get("axisExpandable")&&u>3&&u>h&&h>1&&c>0&&s>0,p=e.get("axisExpandWindow");p?(t=oz(p[1]-p[0],l),p[1]=p[0]+t):(t=oz(c*(h-1),l),(p=[c*(e.get("axisExpandCenter")||Lo(u/2))-t/2])[1]=p[0]+t);var f=(s-t)/(u-h);f<3&&(f=0);var g=[Lo(Ho(p[0]/c,1))+1,Po(Ho(p[1]/c,1))-1],v=f/c*p[0];return{layout:o,pixelDimIndex:a,layoutBase:n[i[a]],layoutLength:s,axisBase:n[i[1-a]],axisLength:n[r[1-a]],axisExpandable:d,axisExpandWidth:c,axisCollapseWidth:f,axisExpandWindow:p,axisCount:u,winInnerIndices:g,axisExpandWindow0Pos:v}},t.prototype._layoutAxes=function(){var t=this._rect,e=this._axesMap,n=this.dimensions,i=this._makeLayoutInfo(),r=i.layout;e.each(function(t){var e=[0,i.axisLength],n=t.inverse?1:0;t.setExtent(e[n],e[1-n])}),B(n,function(e,n){var o=(i.axisExpandable?sz:az)(n,i),a={horizontal:{x:o.position,y:i.axisLength},vertical:{x:0,y:o.position}},s={horizontal:Eo/2,vertical:0},l=[a[r].x+t.x,a[r].y+t.y],u=s[r],c=[1,0,0,1,0,0];Ce(c,c,u),Te(c,c,l),this._axesLayout[e]={position:l,rotation:u,transform:c,axisNameAvailableWidth:o.axisNameAvailableWidth,axisLabelShow:o.axisLabelShow,nameTruncateMaxWidth:o.nameTruncateMaxWidth,tickDirection:1,labelDirection:1}},this)},t.prototype.getAxis=function(t){return this._axesMap.get(t)},t.prototype.dataToPoint=function(t,e){return this.axisCoordToPoint(this._axesMap.get(e).dataToCoord(t),e)},t.prototype.eachActiveState=function(t,e,n,i){null==n&&(n=0),null==i&&(i=t.count());var r=this._axesMap,o=this.dimensions,a=[],s=[];B(o,function(e){a.push(t.mapDimension(e)),s.push(r.get(e).model)});for(var l=this.hasAxisBrushed(),u=n;ur*(1-c[0])?(l="jump",a=s-r*(1-c[2])):(a=s-r*c[1])>=0&&(a=s-r*(1-c[1]))<=0&&(a=0),(a*=e.axisExpandWidth/u)?ez(a,i,o,"all"):l="none";else{var d=i[1]-i[0];(i=[Do(0,o[1]*s/d-d/2)])[1]=Co(o[1],i[0]+d),i[0]=i[1]-d}return{axisExpandWindow:i,behavior:l}},t}();function oz(t,e){return Co(Do(t,e[0]),e[1])}function az(t,e){var n=e.layoutLength/(e.axisCount-1);return{position:n*t,axisNameAvailableWidth:n,axisLabelShow:!0}}function sz(t,e){var n,i,r=e.layoutLength,o=e.axisExpandWidth,a=e.axisCount,s=e.axisCollapseWidth,l=e.winInnerIndices,u=s,c=!1;return t=0;n--)Wo(e[n])},e.prototype.getActiveState=function(t){var e=this.activeIntervals;if(!e.length)return"normal";if(null==t||isNaN(+t))return"inactive";if(1===e.length){var n=e[0];if(n[0]<=t&&t<=n[1])return"active"}else for(var i=0,r=e.length;i6}(t)||o){if(a&&!o){"single"===s.brushMode&&Dz(t);var l=C(s);l.brushType=Yz(l.brushType,a),l.panelId=a===cz?null:a.panelId,o=t._creatingCover=_z(t,l),t._covers.push(o)}if(o){var u=jz[Yz(t._brushType,a)];o.__brushOption.range=u.getCreatingRange(Fz(t,o,t._track)),i&&(bz(t,o),u.updateCommon(t,o)),wz(t,o),r={isEnd:i}}}else i&&"single"===s.brushMode&&s.removeOnClick&&Tz(t,e,n)&&Dz(t)&&(r={isEnd:i,removeOnClick:!0});return r}function Yz(t,e){return"auto"===t?e.defaultBrushType:t}var Xz={mousedown:function(t){if(this._dragging)Zz(this,t);else if(!t.target||!t.target.draggable){Hz(t);var e=this.group.transformCoordToLocal(t.offsetX,t.offsetY);this._creatingCover=null,(this._creatingPanel=Tz(this,t,e))&&(this._dragging=!0,this._track=[e.slice()])}},mousemove:function(t){var e=t.offsetX,n=t.offsetY,i=this.group.transformCoordToLocal(e,n);if(function(t,e,n){if(t._brushType&&!function(t,e,n){var i=t._zr;return e<0||e>i.getWidth()||n<0||n>i.getHeight()}(t,e.offsetX,e.offsetY)){var i=t._zr,r=t._covers,o=Tz(t,e,n);if(!t._dragging)for(var a=0;a=0&&(o[r[a].depth]=new kp(r[a],this,e));var s=vN(i,n,this,!0,function(t,e){t.wrapMethod("getItemModel",function(t,e){var n=t.parentModel,i=n.getData().getItemLayout(e);if(i){var r=i.depth,o=n.levelModels[r];o&&(t.parentModel=o)}return t}),e.wrapMethod("getItemModel",function(t,e){var n=t.parentModel,i=n.getGraph().getEdgeByIndex(e).node1.getLayout();if(i){var r=i.depth,o=n.levelModels[r];o&&(t.parentModel=o)}return t})});return s.data},e.prototype.setNodePosition=function(t,e){var n=(this.option.data||this.option.nodes)[t];n.localX=e[0],n.localY=e[1]},e.prototype.getGraph=function(){return this.getData().graph},e.prototype.getEdgeData=function(){return this.getGraph().edgeData},e.prototype.formatTooltip=function(t,e,n){function i(t){return isNaN(t)||null==t}if("edge"===n){var r=this.getDataParams(t,n),o=r.data,a=r.value;return Ty("nameValue",{name:o.source+" -- "+o.target,value:a,noValue:i(a)})}var s=this.getGraph().getNodeByIndex(t).getLayout().value,l=this.getDataParams(t,n).data.name;return Ty("nameValue",{name:null!=l?l+"":null,value:s,noValue:i(s)})},e.prototype.optionUpdated=function(){},e.prototype.getDataParams=function(e,n){var i=t.prototype.getDataParams.call(this,e,n);if(null==i.value&&"node"===n){var r=this.getGraph().getNodeByIndex(e).getLayout().value;i.value=r}return i},e.prototype.__ownRoamView=function(){return this.coordinateSystem},e.type="series."+rB,e.layoutMode="box",e.defaultOption={z:2,coordinateSystemUsage:"box",left:"5%",top:"5%",right:"20%",bottom:"5%",orient:"horizontal",nodeWidth:20,nodeGap:8,draggable:!0,layoutIterations:32,roam:!1,roamTrigger:"global",center:null,zoom:1,label:{show:!0,position:"right",fontSize:12},edgeLabel:{show:!1,fontSize:12},levels:[],nodeAlign:"justify",lineStyle:{color:ug.color.neutral50,opacity:.2,curveness:.5},emphasis:{label:{show:!0},lineStyle:{opacity:.5}},select:{itemStyle:{borderColor:ug.color.primary}},animationEasing:"linear",animationDuration:1e3},e}(Hy),aB=function(){this.x1=0,this.y1=0,this.x2=0,this.y2=0,this.cpx1=0,this.cpy1=0,this.cpx2=0,this.cpy2=0,this.extent=0},sB=function(t){function e(e){return t.call(this,e)||this}return n(e,t),e.prototype.getDefaultShape=function(){return new aB},e.prototype.buildPath=function(t,e){var n=e.extent;t.moveTo(e.x1,e.y1),t.bezierCurveTo(e.cpx1,e.cpy1,e.cpx2,e.cpy2,e.x2,e.y2),"vertical"===e.orient?(t.lineTo(e.x2+n,e.y2),t.bezierCurveTo(e.cpx2+n,e.cpy2,e.cpx1+n,e.cpy1,e.x1+n,e.y1)):(t.lineTo(e.x2,e.y2+n),t.bezierCurveTo(e.cpx2,e.cpy2+n,e.cpx1,e.cpy1+n,e.x1,e.y1+n)),t.closePath()},e.prototype.highlight=function(){vc(this)},e.prototype.downplay=function(){yc(this)},e}(Kl),lB=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.type=rB,e._mainGroup=new vo,e}return n(e,t),e.prototype.init=function(t,e){this._controller=new uL(e.getZr()),this.group.add(this._mainGroup),this._firstRender=!0},e.prototype.render=function(t,e,n){var i=t.getGraph(),r=this._mainGroup,o=t.layoutInfo,a=o.width,s=o.height,l=t.getData(),u=t.getData("edge"),c=t.get("orient");r.removeAll(),r.x=o.x,r.y=o.y,this._updateViewCoordSys(t,n),FP(t,n,this._controller,HP(r),null),i.eachEdge(function(e){var n=new sB,i=Iu(n);i.dataIndex=e.dataIndex,i.seriesIndex=t.seriesIndex,i.dataType="edge";var o,l,h,d,p,f,g,v,y=e.getModel(),m=y.getModel("lineStyle"),x=m.get("curveness"),_=e.node1.getLayout(),b=e.node1.getModel(),w=b.get("localX"),S=b.get("localY"),M=e.node2.getLayout(),I=e.node2.getModel(),T=I.get("localX"),C=I.get("localY"),D=e.getLayout();n.shape.extent=Math.max(1,D.dy),n.shape.orient=c,"vertical"===c?(o=(null!=w?w*a:_.x)+D.sy,l=(null!=S?S*s:_.y)+_.dy,h=(null!=T?T*a:M.x)+D.ty,p=o,f=l*(1-x)+(d=null!=C?C*s:M.y)*x,g=h,v=l*x+d*(1-x)):(o=(null!=w?w*a:_.x)+_.dx,l=(null!=S?S*s:_.y)+D.sy,p=o*(1-x)+(h=null!=T?T*a:M.x)*x,f=l,g=o*x+h*(1-x),v=d=(null!=C?C*s:M.y)+D.ty),n.setShape({x1:o,y1:l,x2:h,y2:d,cpx1:p,cpy1:f,cpx2:g,cpy2:v}),n.useStyle(m.getItemStyle()),uB(n.style,c,e);var A=""+y.get("value"),k=ap(y,"edgeLabel");op(n,k,{labelFetcher:{getFormattedLabel:function(e,n,i,r,o,a){return t.getFormattedLabel(e,n,"edge",r,st(o,k.normal&&k.normal.get("formatter"),A),a)}},labelDataIndex:e.dataIndex,defaultText:A}),n.setTextConfig({position:"inside"});var L=y.getModel("emphasis");Rc(n,y,"lineStyle",function(t){var n=t.getItemStyle();return uB(n,c,e),n}),r.add(n),u.setItemGraphicEl(e.dataIndex,n);var P=L.get("focus");kc(n,"adjacency"===P?e.getAdjacentDataIndices():"trajectory"===P?e.getTrajectoryDataIndices():P,L.get("blurScope"),L.get("disabled"))}),i.eachNode(function(e){var n=e.getLayout(),i=e.getModel(),o=i.get("localX"),u=i.get("localY"),c=i.getModel("emphasis"),h=i.get(["itemStyle","borderRadius"])||0,d=new lu({shape:{x:null!=o?o*a:n.x,y:null!=u?u*s:n.y,width:n.dx,height:n.dy,r:h},style:i.getModel("itemStyle").getItemStyle(),z2:10});op(d,ap(i),{labelFetcher:{getFormattedLabel:function(e,n){return t.getFormattedLabel(e,n,"node")}},labelDataIndex:e.dataIndex,defaultText:e.id}),d.disableLabelAnimation=!0,d.setStyle("fill",e.getVisual("color")),d.setStyle("decal",e.getVisual("style").decal),Rc(d,i),r.add(d),l.setItemGraphicEl(e.dataIndex,d),Iu(d).dataType="node";var p=c.get("focus");kc(d,"adjacency"===p?e.getAdjacentDataIndices():"trajectory"===p?e.getTrajectoryDataIndices():p,c.get("blurScope"),c.get("disabled"))}),l.eachItemGraphicEl(function(e,i){l.getItemModel(i).get("draggable")&&(e.drift=function(e,r){this.shape.x+=e,this.shape.y+=r,this.dirty(),n.dispatchAction({type:"dragNode",seriesId:t.id,dataIndex:l.getRawIndex(i),localX:this.shape.x/a,localY:this.shape.y/s})},e.draggable=!0,e.cursor="move")}),!this._data&&t.isAnimationEnabled()&&r.setClipPath(function(t,e,n){var i=new lu({shape:{x:t.x-10,y:t.y-10,width:0,height:t.height+20}});return od(i,{shape:{width:t.width+20}},e,n),i}(r.getBoundingRect(),t,function(){r.removeClipPath()})),this._data=t.getData(),this._firstRender=!1},e.prototype.__updateOnOwnRoam=function(t,e,n){CP(this.group,2,e.coordinateSystem,null)},e.prototype.dispose=function(){this._controller&&this._controller.dispose()},e.prototype._updateViewCoordSys=function(t,e){var n=t.layoutInfo,i=t.coordinateSystem=ZP(t,e,n.x,n.y,n.width,n.height);CP(this.group,2,i,this._firstRender?null:t)},e.type=rB,e}(tm);function uB(t,e,n){switch(t.fill){case"source":t.fill=n.node1.getVisual("color"),t.decal=n.node1.getVisual("style").decal;break;case"target":t.fill=n.node2.getVisual("color"),t.decal=n.node2.getVisual("style").decal;break;case"gradient":var i=n.node1.getVisual("color"),r=n.node2.getVisual("color");j(i)&&j(r)&&(t.fill=new Hh(0,0,+("horizontal"===e),+("vertical"===e),[{color:i,offset:0},{color:r,offset:1}]))}}var cB=os(rB,function(t,e){t.eachSeriesByType(rB,function(t){var n=t.get("nodeWidth"),i=t.get("nodeGap"),r=eg(t,e).refContainer,o=Jf(t.getBoxLayoutParams(),r);t.layoutInfo=o;var a=o.width,s=o.height,l=t.getGraph(),u=l.nodes,c=l.edges;!function(t){B(t,function(t){var e=_B(t.outEdges,xB),n=_B(t.inEdges,xB),i=t.getValue()||0,r=Math.max(e,n,i);t.setLayout({value:r},!0)})}(u),function(t,e,n,i,r,o,a,s,l){(function(t,e,n,i,r,o,a){for(var s=[],l=[],u=[],c=[],h=0,d=0;d=0;y&&v.depth>p&&(p=v.depth),g.setLayout({depth:y?v.depth:h},!0),"vertical"===o?g.setLayout({dy:n},!0):g.setLayout({dx:n},!0);for(var m=0;mh-1?p:h-1;a&&"left"!==a&&function(t,e,n,i){if("right"===e){for(var r=[],o=t,a=0;o.length;){for(var s=0;s0;o--)pB(s,l*=.99,a),dB(s,r,n,i,a),bB(s,l,a),dB(s,r,n,i,a)}(t,e,o,r,i,a,s),function(t,e){var n="vertical"===e?"x":"y";B(t,function(t){t.outEdges.sort(function(t,e){return t.node2.getLayout()[n]-e.node2.getLayout()[n]}),t.inEdges.sort(function(t,e){return t.node1.getLayout()[n]-e.node1.getLayout()[n]})}),B(t,function(t){var e=0,n=0;B(t.outEdges,function(t){t.setLayout({sy:e},!0),e+=t.getLayout().dy}),B(t.inEdges,function(t){t.setLayout({ty:n},!0),n+=t.getLayout().dy})})}(t,s)}(u,c,n,i,a,s,0!==F(u,function(t){return 0===t.getLayout().value}).length?0:t.get("layoutIterations"),t.get("orient"),t.get("nodeAlign"))})});function hB(t){var e=t.hostGraph.data.getRawDataItem(t.dataIndex);return null!=e.depth&&e.depth>=0}function dB(t,e,n,i,r){var o="vertical"===r?"x":"y";B(t,function(t){var a,s,l;t.sort(function(t,e){return t.getLayout()[o]-e.getLayout()[o]});for(var u=0,c=t.length,h="vertical"===r?"dx":"dy",d=0;d0&&(a=s.getLayout()[o]+l,"vertical"===r?s.setLayout({x:a},!0):s.setLayout({y:a},!0)),u=s.getLayout()[o]+s.getLayout()[h]+e;if((l=u-e-("vertical"===r?i:n))>0){a=s.getLayout()[o]-l,"vertical"===r?s.setLayout({x:a},!0):s.setLayout({y:a},!0),u=a;for(d=c-2;d>=0;--d)(l=(s=t[d]).getLayout()[o]+s.getLayout()[h]+e-u)>0&&(a=s.getLayout()[o]-l,"vertical"===r?s.setLayout({x:a},!0):s.setLayout({y:a},!0)),u=s.getLayout()[o]}})}function pB(t,e,n){B(t.slice().reverse(),function(t){B(t,function(t){if(t.outEdges.length){var i=_B(t.outEdges,fB,n)/_B(t.outEdges,xB);if(isNaN(i)){var r=t.outEdges.length;i=r?_B(t.outEdges,gB,n)/r:0}if("vertical"===n){var o=t.getLayout().x+(i-mB(t,n))*e;t.setLayout({x:o},!0)}else{var a=t.getLayout().y+(i-mB(t,n))*e;t.setLayout({y:a},!0)}}})})}function fB(t,e){return mB(t.node2,e)*t.getValue()}function gB(t,e){return mB(t.node2,e)}function vB(t,e){return mB(t.node1,e)*t.getValue()}function yB(t,e){return mB(t.node1,e)}function mB(t,e){return"vertical"===e?t.getLayout().x+t.getLayout().dx/2:t.getLayout().y+t.getLayout().dy/2}function xB(t){return t.getValue()}function _B(t,e,n){for(var i=0,r=t.length,o=-1;++oo&&(o=e)}),B(n,function(e){var n=new ZR({type:"color",mappingMethod:"linear",dataExtent:[r,o],visual:t.get("color")}).mapValueToVisual(e.getLayout().value),i=e.getModel().get(["itemStyle","color"]);null!=i?(e.setVisual("color",i),e.setVisual("style",{fill:i})):(e.setVisual("color",n),e.setVisual("style",{fill:n}))})}i.length&&B(i,function(t){var e=t.getModel().get("lineStyle");t.setVisual("style",e)})})});var SB=function(){function t(){}return t.prototype._hasEncodeRule=function(t){var e=this.getEncode();return e&&null!=e.get(t)},t.prototype.getInitialData=function(t,e){var n,i,r=e.getComponent("xAxis",this.get("xAxisIndex")),o=e.getComponent("yAxis",this.get("yAxisIndex")),a=r.get("type"),s=o.get("type"),l=t.layout;"category"===a?(l="horizontal",n=r.getOrdinalMeta(),i=!this._hasEncodeRule("x")):"category"===s&&(l="vertical",n=o.getOrdinalMeta(),i=!this._hasEncodeRule("y")),l||(l="time"===s?"vertical":"horizontal"),this._layout=l;var u=["x","y"],c="horizontal"===l?0:1,h=this._baseAxisDim=u[c],d=u[1-c],p=[r,o],f=p[c].get("type"),g=p[1-c].get("type"),v=t.data;if(v&&i){var y=[];B(v,function(t,e){var n;X(t)?(n=t.slice(),t.unshift(e)):X(t.value)?((n=k({},t)).value=n.value.slice(),t.value.unshift(e)):n=t,y.push(n)}),t.data=y}var m=this.defaultValueDimensions,x=[{name:h,type:bb(f),ordinalMeta:n,otherDims:{tooltip:!1,itemName:0},dimsDef:["base"]},{name:d,type:bb(g),dimsDef:m.slice()}];return bA(this,{coordDimensions:x,dimensionsCount:m.length+1,encodeDefaulter:Y(wg,x,this)})},t.prototype.getBaseAxis=function(){var t=this._baseAxisDim;return this.ecModel.getComponent(t+"Axis",this.get(t+"AxisIndex")).axis},t.prototype.getWhiskerBoxesLayout=function(){return this._layout},t}();function MB(t,e){for(var n=e.ends.length,i=0,r=0;rg){var _=[y,x];i.push(_)}}}return{boxData:n,outliers:i}}(e.getRawData(),t.config);return[{dimensions:["ItemName","Low","Q1","Q2","Q3","High"],data:i.boxData},{data:i.outliers}]}};var EB="candlestick",zB=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.defaultValueDimensions=[{name:"open",defaultTooltip:!0},{name:"close",defaultTooltip:!0},{name:"lowest",defaultTooltip:!0},{name:"highest",defaultTooltip:!0}],n}return n(e,t),e.prototype.getShadowDim=function(){return"open"},e.prototype.brushSelector=function(t,e,n){var i=e.getItemLayout(t);return i&&n.rect(i.brushRect)},e.type="series."+EB,e.dependencies=["xAxis","yAxis","grid"],e.defaultOption={z:2,coordinateSystem:"cartesian2d",legendHoverLink:!0,layout:null,clip:!0,itemStyle:{color:"#eb5454",color0:"#47b262",borderColor:"#eb5454",borderColor0:"#47b262",borderColorDoji:null,borderWidth:1},emphasis:{itemStyle:{borderWidth:2}},barMaxWidth:null,barMinWidth:null,barWidth:null,large:!0,largeThreshold:600,progressive:3e3,progressiveThreshold:1e4,progressiveChunkMode:"mod",animationEasing:"linear",animationDuration:300},e}(Hy);E(zB,SB,!0);var BB=["itemStyle","borderColor"],VB=["itemStyle","borderColor0"],GB=["itemStyle","borderColorDoji"],FB=["itemStyle","color"],HB=["itemStyle","color0"];function WB(t,e){return e.get(t>0?FB:HB)}function UB(t,e){return e.get(0===t?GB:t>0?BB:VB)}var YB={seriesType:EB,plan:$y(),performRawSeries:!0,reset:function(t,e){if(!e.isSeriesFiltered(t))return!t.pipelineContext.large&&{progress:function(t,e){for(var n;null!=(n=t.next());){var i=e.getItemModel(n),r=e.getItemLayout(n).sign,o=i.getItemStyle();o.fill=WB(r,i),o.stroke=UB(r,i)||o.fill,k(e.ensureUniqueItemVisual(n,"style"),o)}}}}},XB=["color","borderColor"],ZB=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){this.group.removeClipPath(),this._progressiveEls=null,this._updateDrawMode(t),this._isLargeDraw?this._renderLarge(t):this._renderNormal(t)},e.prototype.incrementalPrepareRender=function(t,e,n){this._clear(),this._updateDrawMode(t)},e.prototype.incrementalRender=function(t,e,n,i){this._progressiveEls=[],this._isLargeDraw?this._incrementalRenderLarge(t,e):this._incrementalRenderNormal(t,e)},e.prototype.eachRendered=function(t){Hd(this._progressiveEls||this.group,t)},e.prototype._updateDrawMode=function(t){var e=t.pipelineContext.large;null!=this._isLargeDraw&&e===this._isLargeDraw||(this._isLargeDraw=e,this._clear())},e.prototype._renderNormal=function(t){var e=t.getData(),n=this._data,i=this.group,r=e.getLayout("isSimpleBox"),o=t.get("clip",!0),a=t.coordinateSystem,s=a.getArea&&a.getArea(),l=o&&xC(a,!1,t);this._data||i.removeAll();var u=QB(t);e.diff(n).add(function(n){if(e.hasValue(n)){var a=e.getItemLayout(n),c=o?MB(s,a):0;if(2===c)return;var h=KB(a,n,u,!0);od(h,{shape:{points:a.ends}},t,n),_C(1===c,h,l),$B(h,e,n,r),i.add(h),e.setItemGraphicEl(n,h)}}).update(function(a,c){var h=n.getItemGraphicEl(c);if(e.hasValue(a)){var d=e.getItemLayout(a),p=o?MB(s,d):0;2!==p?(h?(rd(h,{shape:{points:d.ends}},t,a),cd(h)):h=KB(d,a,u),$B(h,e,a,r),_C(1===p,h,l),i.add(h),e.setItemGraphicEl(a,h)):i.remove(h)}else i.remove(h)}).remove(function(t){var e=n.getItemGraphicEl(t);e&&i.remove(e)}).execute(),this._data=e},e.prototype._renderLarge=function(t){this._clear(),nV(t,this.group);var e=t.get("clip",!0)?xC(t.coordinateSystem,!1,t):null;_C(!!e,this.group,e)},e.prototype._incrementalRenderNormal=function(t,e){for(var n,i=e.getData(),r=i.getLayout("isSimpleBox"),o=QB(e);null!=(n=t.next());){var a=KB(i.getItemLayout(n),n,o);$B(a,i,n,r),a.incremental=is(e),this.group.add(a),this._progressiveEls.push(a)}},e.prototype._incrementalRenderLarge=function(t,e){nV(e,this.group,this._progressiveEls,!0)},e.prototype.remove=function(t){this._clear()},e.prototype._clear=function(){this.group.removeAll(),_C(!1,this.group,null),this._data=null},e.type=EB,e}(tm),jB=function(){},qB=function(t){function e(e){var n=t.call(this,e)||this;return n.type="normalCandlestickBox",n}return n(e,t),e.prototype.getDefaultShape=function(){return new jB},e.prototype.buildPath=function(t,e){var n=e.points;this.__simpleBox?(t.moveTo(n[4][0],n[4][1]),t.lineTo(n[6][0],n[6][1])):(t.moveTo(n[0][0],n[0][1]),t.lineTo(n[1][0],n[1][1]),t.lineTo(n[2][0],n[2][1]),t.lineTo(n[3][0],n[3][1]),t.closePath(),t.moveTo(n[4][0],n[4][1]),t.lineTo(n[5][0],n[5][1]),t.moveTo(n[6][0],n[6][1]),t.lineTo(n[7][0],n[7][1]))},e}(Kl);function KB(t,e,n,i){var r=t.ends;return new qB({shape:{points:i?JB(r,n,t):r},z2:100})}function $B(t,e,n,i){var r=e.getItemModel(n);t.useStyle(e.getItemVisual(n,"style")),t.style.strokeNoScale=!0;var o=r.getShallow("cursor");o&&t.attr("cursor",o),t.__simpleBox=i,Rc(t,r);var a=e.getItemLayout(n).sign;B(t.states,function(t,e){var n=r.getModel(e),i=WB(a,n),o=UB(a,n)||i,s=t.style||(t.style={});i&&(s.fill=i),o&&(s.stroke=o)});var s=r.getModel("emphasis");kc(t,s.get("focus"),s.get("blurScope"),s.get("disabled"))}function JB(t,e,n){return V(t,function(t){return(t=t.slice())[e]=n.initBaseline,t})}function QB(t){return"horizontal"===t.getWhiskerBoxesLayout()?1:0}var tV=function(){},eV=function(t){function e(e){var n=t.call(this,e)||this;return n.type="largeCandlestickBox",n}return n(e,t),e.prototype.getDefaultShape=function(){return new tV},e.prototype.buildPath=function(t,e){for(var n=e.points,i=0;ig?b[o]:_[o],ends:M,brushRect:D(v,y,p)})}function T(t,n){var i=[];return i[r]=n,i[o]=t,isNaN(n)||isNaN(t)?[NaN,NaN]:e.dataToPoint(i)}function C(t,e,n){var o=e.slice(),a=e.slice();o[r]=Id(o[r]+i/2,1,!1),a[r]=Id(a[r]-i/2,1,!0),n?t.push(o,a):t.push(a,o)}function D(t,e,n){var a=T(t,n),s=T(e,n);return a[r]-=i/2,s[r]-=i/2,{x:a[0],y:a[1],width:o?i:s[0]-a[0],height:o?s[1]-a[1]:i}}function A(t){return t[r]=Id(t[r],1),t}}}}};function sV(t,e,n,i,r,o){return n>i?-1:n0?t.get(r,e-1)<=i?1:-1:1}function lV(t,e){var n=e.rippleEffectColor||e.color;t.eachChild(function(t){t.attr({z:e.z,zlevel:e.zlevel,style:{stroke:"stroke"===e.brushType?n:null,fill:"fill"===e.brushType?n:null}})})}var uV=function(t){function e(e,n){var i=t.call(this)||this,r=new KT(e,n),o=new vo;return i.add(r),i.add(o),i.updateData(e,n),i}return n(e,t),e.prototype.stopEffectAnimation=function(){this.childAt(1).removeAll()},e.prototype.startEffectAnimation=function(t){for(var e=t.symbolType,n=t.color,i=t.rippleNumber,r=this.childAt(1),o=0;o0&&(o=this._getLineLength(i)/l*1e3),o!==this._period||a!==this._loop||s!==this._roundTrip){i.stopAnimation();var c=void 0;c=Z(u)?u(n):u,i.__t>0&&(c=-o*i.__t),this._animateSymbol(i,o,c,a,s)}this._period=o,this._loop=a,this._roundTrip=s}},e.prototype._animateSymbol=function(t,e,n,i,r){if(e>0){t.__t=0;var o=this,a=t.animate("",i).when(r?2*e:e,{__t:r?2:1}).delay(n).during(function(){o._updateSymbolPosition(t)});i||a.done(function(){o.remove(t)}),a.start()}},e.prototype._getLineLength=function(t){return Ft(t.__p1,t.__cp1)+Ft(t.__cp1,t.__p2)},e.prototype._updateAnimationPoints=function(t,e){t.__p1=e[0],t.__p2=e[1],t.__cp1=e[2]||[(e[0][0]+e[1][0])/2,(e[0][1]+e[1][1])/2]},e.prototype.updateData=function(t,e,n){this.childAt(0).updateData(t,e,n),this._updateEffectSymbol(t,e)},e.prototype._updateSymbolPosition=function(t){var e=t.__p1,n=t.__p2,i=t.__cp1,r=t.__t<=1?t.__t:2-t.__t,o=[t.x,t.y],a=o.slice(),s=Yn,l=Xn;o[0]=s(e[0],i[0],n[0],r),o[1]=s(e[1],i[1],n[1],r);var u=t.__t<=1?l(e[0],i[0],n[0],r):l(n[0],i[0],e[0],1-r),c=t.__t<=1?l(e[1],i[1],n[1],r):l(n[1],i[1],e[1],1-r);t.rotation=-Math.atan2(c,u)-Math.PI/2,"line"!==this._symbolType&&"rect"!==this._symbolType&&"roundRect"!==this._symbolType||(void 0!==t.__lastT&&t.__lastT=0&&!(i[o]<=e);o--);o=Math.min(o,r-2)}else{for(o=a;oe);o++);o=Math.min(o-1,r-2)}var s=(e-i[o])/(i[o+1]-i[o]),l=n[o],u=n[o+1];t.x=l[0]*(1-s)+s*u[0],t.y=l[1]*(1-s)+s*u[1];var c=t.__t<=1?u[0]-l[0]:l[0]-u[0],h=t.__t<=1?u[1]-l[1]:l[1]-u[1];t.rotation=-Math.atan2(h,c)-Math.PI/2,this._lastFrame=o,this._lastFramePercent=e,t.ignore=!1}},e}(pV),vV=function(){this.polyline=!1,this.curveness=0,this.segs=[]},yV=function(t){function e(e){var n=t.call(this,e)||this;return n._off=0,n.hoverDataIdx=-1,n}return n(e,t),e.prototype.reset=function(){this.notClear=!1,this._off=0},e.prototype.beforeBrush=function(t){t&&!t.contentRetained&&this.reset()},e.prototype.getDefaultStyle=function(){return{stroke:ug.color.neutral99,fill:null}},e.prototype.getDefaultShape=function(){return new vV},e.prototype.buildPath=function(t,e){var n,i=e.segs,r=e.curveness;if(e.polyline)for(n=this._off;n0){t.moveTo(i[n++],i[n++]);for(var a=1;a0){var h=(s+u)/2-(l-c)*r,d=(l+c)/2-(u-s)*r;t.quadraticCurveTo(h,d,u,c)}else t.lineTo(u,c)}this.incremental&&(this._off=n,this.notClear=!0)},e.prototype.findDataIndex=function(t,e){var n=this.shape,i=n.segs,r=n.curveness,o=this.style.lineWidth;if(n.polyline)for(var a=0,s=0;s0)for(var u=i[s++],c=i[s++],h=1;h0){if(Pl(u,c,(u+d)/2-(c-p)*r,(c+p)/2-(d-u)*r,d,p,o,t,e))return a}else if(kl(u,c,d,p,o,t,e))return a;a++}return-1},e.prototype.contain=function(t,e){var n=this.transformCoordToLocal(t,e),i=this.getBoundingRect();return t=n[0],e=n[1],i.contain(t,e)?(this.hoverDataIdx=this.findDataIndex(t,e))>=0:(this.hoverDataIdx=-1,!1)},e.prototype.getBoundingRect=function(){var t=this._rect;if(!t){for(var e=this.shape.segs,n=1/0,i=1/0,r=-1/0,o=-1/0,a=0;a0&&(o.dataIndex=n+t.__startIndex)})},t.prototype._clear=function(){this._newAdded=[],this.group.removeAll()},t}(),xV={seriesType:"lines",plan:$y(),reset:function(t){var e=t.coordinateSystem;if(e){var n=t.get("polyline"),i=t.pipelineContext.large;return{progress:function(r,o){var a=[];if(i){var s=void 0,l=r.end-r.start;if(n){for(var u=0,c=r.start;c0&&l&&s.configLayer(o,{motionBlur:!0,lastFrameAlpha:Math.max(Math.min(a/10+.9,1),0)}),r.updateData(i);var u=t.get("clip",!0)&&xC(t.coordinateSystem,!1,t);u?this.group.setClipPath(u):this.group.removeClipPath(),this._lastZlevel=o,this._finished=!0},e.prototype.incrementalPrepareRender=function(t,e,n){var i=t.getData();this._updateLineDraw(i,t).incrementalPrepareUpdate(i),this._clearLayer(n),this._finished=!1},e.prototype.incrementalRender=function(t,e,n){this._lineDraw.incrementalUpdate(t,e.getData(),is(e)),this._finished=t.end===e.getData().count()},e.prototype.eachRendered=function(t){this._lineDraw&&this._lineDraw.eachRendered(t)},e.prototype.updateTransform=function(t,e,n){var i=t.getData(),r=this._lineDraw;if(!(this._finished&&r&&r.updateLayout))return{update:!0};var o=xV.reset(t,e,n);o.progress&&o.progress({start:0,end:i.count(),count:i.count()},i),r.updateLayout(),this._clearLayer(n)},e.prototype._updateLineDraw=function(t,e){var n=this._lineDraw,i=this._showEffect(e),r=!!e.get("polyline"),o=e.pipelineContext.large;return n&&i===this._hasEffet&&r===this._isPolyline&&o===this._isLargeDraw||(n&&n.remove(),n=this._lineDraw=o?new mV:new QN(r?i?gV:fV:i?pV:JN),this._hasEffet=i,this._isPolyline=r,this._isLargeDraw=o),this.group.add(n.group),n},e.prototype._showEffect=function(t){return!!t.get(["effect","show"])},e.prototype._clearLayer=function(t){var e=tp(t);e&&null!=this._lastZlevel&&e.getLayer(this._lastZlevel).clear(!0)},e.prototype.remove=function(t,e){this._lineDraw&&this._lineDraw.remove(),this._lineDraw=null,this._clearLayer(e)},e.prototype.dispose=function(t,e){this.remove(t,e)},e.type="lines",e}(tm),bV="undefined"==typeof Uint32Array?Array:Uint32Array,wV="undefined"==typeof Float64Array?Array:Float64Array;function SV(t){var e=t.data;e&&e[0]&&e[0][0]&&e[0][0].coord&&(t.data=V(e,function(t){var e={coords:[t[0].coord,t[1].coord]};return t[0].name&&(e.fromName=t[0].name),t[1].name&&(e.toName=t[1].name),A([e,t[0],t[1]])}))}var MV=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.visualStyleAccessPath="lineStyle",n.visualDrawType="stroke",n}return n(e,t),e.prototype.init=function(e){e.data=e.data||[],SV(e);var n=this._processFlatCoordsArray(e.data);this._flatCoords=n.flatCoords,this._flatCoordsOffset=n.flatCoordsOffset,n.flatCoords&&(e.data=new Float32Array(n.count)),t.prototype.init.apply(this,arguments)},e.prototype.mergeOption=function(e){if(SV(e),e.data){var n=this._processFlatCoordsArray(e.data);this._flatCoords=n.flatCoords,this._flatCoordsOffset=n.flatCoordsOffset,n.flatCoords&&(e.data=new Float32Array(n.count))}t.prototype.mergeOption.apply(this,arguments)},e.prototype.appendData=function(t){var e=this._processFlatCoordsArray(t.data);e.flatCoords&&(this._flatCoords?(this._flatCoords=xt(this._flatCoords,e.flatCoords),this._flatCoordsOffset=xt(this._flatCoordsOffset,e.flatCoordsOffset)):(this._flatCoords=e.flatCoords,this._flatCoordsOffset=e.flatCoordsOffset),t.data=new Float32Array(e.count)),this.getRawData().appendData(t.data)},e.prototype._getCoordsFromItemModel=function(t){var e=this.getData().getItemModel(t),n=e.option instanceof Array?e.option:e.getShallow("coords");return n},e.prototype.getLineCoordsCount=function(t){return this._flatCoordsOffset?this._flatCoordsOffset[2*t+1]:this._getCoordsFromItemModel(t).length},e.prototype.getLineCoords=function(t,e){if(this._flatCoordsOffset){for(var n=this._flatCoordsOffset[2*t],i=this._flatCoordsOffset[2*t+1],r=0;r ")}return Ty("nameValue",{name:a,value:r,noValue:null==r||isNaN(r)})},e.prototype.preventIncremental=function(){return!!this.get(["effect","show"])},e.prototype.getProgressive=function(){var t=this.option.progressive;return null==t?this.option.large?1e4:this.get("progressive"):t},e.prototype.getProgressiveThreshold=function(){var t=this.option.progressiveThreshold;return null==t?this.option.large?2e4:this.get("progressiveThreshold"):t},e.prototype.getZLevelKey=function(){var t=this.getModel("effect"),e=t.get("trailLength");return this.getData().count()>this.getProgressiveThreshold()?this.id:t.get("show")&&e>0?e+"":""},e.type="series.lines",e.dependencies=["grid","polar","geo","calendar"],e.defaultOption={coordinateSystem:"geo",z:2,legendHoverLink:!0,xAxisIndex:0,yAxisIndex:0,symbol:["none","none"],symbolSize:[10,10],geoIndex:0,effect:{show:!1,period:4,constantSpeed:0,symbol:"circle",symbolSize:3,loop:!0,trailLength:.2},large:!1,largeThreshold:2e3,polyline:!1,clip:!0,label:{show:!1,position:"end"},lineStyle:{opacity:.5}},e}(Hy);function IV(t){return t instanceof Array||(t=[t,t]),t}var TV={seriesType:"lines",reset:function(t){var e=IV(t.get("symbol")),n=IV(t.get("symbolSize")),i=t.getData();return i.setVisual("fromSymbol",e&&e[0]),i.setVisual("toSymbol",e&&e[1]),i.setVisual("fromSymbolSize",n&&n[0]),i.setVisual("toSymbolSize",n&&n[1]),{dataEach:i.hasItemOption?function(t,e){var n=t.getItemModel(e),i=IV(n.getShallow("symbol",!0)),r=IV(n.getShallow("symbolSize",!0));i[0]&&t.setItemVisual(e,"fromSymbol",i[0]),i[1]&&t.setItemVisual(e,"toSymbol",i[1]),r[0]&&t.setItemVisual(e,"fromSymbolSize",r[0]),r[1]&&t.setItemVisual(e,"toSymbolSize",r[1])}:null}}};var CV=function(){function t(){this.blurSize=30,this.pointSize=20,this.maxOpacity=1,this.minOpacity=0,this._gradientPixels={inRange:null,outOfRange:null};var t=c.createCanvas();this.canvas=t}return t.prototype.update=function(t,e,n,i,r,o){var a=this._getBrush(),s=this._getGradient(r,"inRange"),l=this._getGradient(r,"outOfRange"),u=this.pointSize+this.blurSize,c=this.canvas,h=c.getContext("2d"),d=t.length;c.width=e,c.height=n;for(var p=0;p0){var I=o(y)?s:l;y>0&&(y=y*S+w),x[_++]=I[M],x[_++]=I[M+1],x[_++]=I[M+2],x[_++]=I[M+3]*y*256}else _+=4}return h.putImageData(m,0,0),c},t.prototype._getBrush=function(){var t=this._brushCanvas||(this._brushCanvas=c.createCanvas()),e=this.pointSize+this.blurSize,n=2*e;t.width=n,t.height=n;var i=t.getContext("2d");return i.clearRect(0,0,n,n),i.shadowOffsetX=n,i.shadowBlur=this.blurSize,i.shadowColor=ug.color.neutral99,i.beginPath(),i.arc(-e,e,this.pointSize,0,2*Math.PI,!0),i.closePath(),i.fill(),t},t.prototype._getGradient=function(t,e){for(var n=this._gradientPixels,i=n[e]||(n[e]=new Uint8ClampedArray(1024)),r=[0,0,0,0],o=0,a=0;a<256;a++)t[e](a/255,!0,r),i[o++]=r[0],i[o++]=r[1],i[o++]=r[2],i[o++]=r[3];return i},t}();var DV=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){var i;e.eachComponent("visualMap",function(e){e.eachTargetSeries(function(n){n===t&&(i=e)})}),this._progressiveEls=null,this.group.removeAll();var r=t.coordinateSystem;"cartesian2d"===r.type||"calendar"===r.type||"matrix"===r.type?this._renderOnGridLike(t,n,0,t.getData().count()):SC(r)&&this._renderOnGeo(r,t,i,n)},e.prototype.incrementalPrepareRender=function(t,e,n){this.group.removeAll()},e.prototype.incrementalRender=function(t,e,n,i){var r=e.coordinateSystem;r&&(SC(r)?this.render(e,n,i):(this._progressiveEls=[],this._renderOnGridLike(e,i,t.start,t.end,!0)))},e.prototype.eachRendered=function(t){Hd(this._progressiveEls||this.group,t)},e.prototype._renderOnGridLike=function(t,e,n,i,r){var o,a,s,l,u=t.coordinateSystem,c=wC(u,"cartesian2d"),h=wC(u,"matrix");if(c){var d=u.getAxis("x"),p=u.getAxis("y");0,o=bM(d).w+.5,a=bM(p).w+.5,s=d.scale.getExtent(),l=p.scale.getExtent()}for(var f=this.group,g=t.getData(),v=t.getModel(["emphasis","itemStyle"]).getItemStyle(),y=t.getModel(["blur","itemStyle"]).getItemStyle(),m=t.getModel(["select","itemStyle"]).getItemStyle(),x=t.get(["itemStyle","borderRadius"]),_=ap(t),b=t.getModel("emphasis"),w=b.get("focus"),S=b.get("blurScope"),M=b.get("disabled"),I=c||h?[g.mapDimension("x"),g.mapDimension("y"),g.mapDimension("value")]:[g.mapDimension("time"),g.mapDimension("value")],T=n;Ts[1]||kl[1])continue;var L=u.dataToPoint([A,k]);C=new lu({shape:{x:L[0]-o/2,y:L[1]-a/2,width:o,height:a},style:D})}else if(h){if(rt((P=u.dataToLayout([g.get(I[0],T),g.get(I[1],T)]).rect).x))continue;C=new lu({z2:1,shape:P,style:D})}else{if(isNaN(g.get(I[1],T)))continue;var P,O=u.dataToLayout([g.get(I[0],T)]);if(rt((P=O.contentRect||O.rect).x)||rt(P.y))continue;C=new lu({z2:1,shape:P,style:D})}if(g.hasItemOption){var R=g.getItemModel(T),N=R.getModel("emphasis");v=N.getModel("itemStyle").getItemStyle(),y=R.getModel(["blur","itemStyle"]).getItemStyle(),m=R.getModel(["select","itemStyle"]).getItemStyle(),x=R.get(["itemStyle","borderRadius"]),w=N.get("focus"),S=N.get("blurScope"),M=N.get("disabled"),_=ap(R)}C.shape.r=x;var E=t.getRawValue(T),z="-";E&&null!=E[2]&&(z=E[2]+""),op(C,_,{labelFetcher:t,labelDataIndex:T,defaultOpacity:D.opacity,defaultText:z}),C.ensureState("emphasis").style=v,C.ensureState("blur").style=y,C.ensureState("select").style=m,kc(C,w,S,M),C.incremental=is(t,r),r&&(C.states.emphasis.hoverLayer=2),f.add(C),g.setItemGraphicEl(T,C),this._progressiveEls&&this._progressiveEls.push(C)}},e.prototype._renderOnGeo=function(t,e,n,i){var r=n.targetVisuals.inRange,o=n.targetVisuals.outOfRange,a=e.getData(),s=this._hmLayer||this._hmLayer||new CV;s.blurSize=e.get("blurSize"),s.pointSize=e.get("pointSize"),s.minOpacity=e.get("minOpacity"),s.maxOpacity=e.get("maxOpacity");var l=t.getViewRect().clone(),u=t.getRoamTransform();l.applyTransform(u);var c=Math.max(l.x,0),h=Math.max(l.y,0),d=Math.min(l.width+l.x,i.getWidth()),p=Math.min(l.height+l.y,i.getHeight()),f=d-c,g=p-h,v=[a.mapDimension("lng"),a.mapDimension("lat"),a.mapDimension("value")],y=a.mapArray(v,function(e,n,i){var r=t.dataToPoint([e,n]);return r[0]-=c,r[1]-=h,r.push(i),r}),m=n.getExtent(),x="visualMap.continuous"===n.type?function(t,e){var n=t[1]-t[0];return e=[(e[0]-t[0])/n,(e[1]-t[0])/n],function(t){return t>=e[0]&&t<=e[1]}}(m,n.option.range):function(t,e,n){var i=t[1]-t[0],r=(e=V(e,function(e){return{interval:[(e.interval[0]-t[0])/i,(e.interval[1]-t[0])/i]}})).length,o=0;return function(t){var i;for(i=o;i=0;i--){var a;if((a=e[i].interval)[0]<=t&&t<=a[1]){o=i;break}}return i>=0&&i=0?1:-1:o>0?1:-1}(n,o,r,i,h),function(t,e,n,i,r,o,a,s,l,u){var c,h=l.valueDim,d=l.categoryDim,p=Math.abs(n[d.wh]),f=t.getItemVisual(e,"symbolSize");c=X(f)?f.slice():null==f?["100%","100%"]:[f,f];c[d.index]=Vo(c[d.index],p),c[h.index]=Vo(c[h.index],i?p:Math.abs(o)),u.symbolSize=c;var g=u.symbolScale=[c[0]/s,c[1]/s];g[h.index]*=(l.isHorizontal?-1:1)*a}(t,e,r,o,0,h.boundingLength,h.pxSign,u,i,h),function(t,e,n,i,r){var o=t.get(kV)||0;o&&(PV.attr({scaleX:e[0],scaleY:e[1],rotation:n}),PV.updateTransform(),o/=PV.getLineScale(),o*=e[i.valueDim.index]);r.valueLineWidth=o||0}(n,h.symbolScale,l,i,h);var d=h.symbolSize,p=bx(n.get("symbolOffset"),d);return function(t,e,n,i,r,o,a,s,l,u,c,h){var d=c.categoryDim,p=c.valueDim,f=h.pxSign,g=Math.max(e[p.index]+s,0),v=g;if(i){var y=Math.abs(l),m=ot(t.get("symbolMargin"),"15%")+"",x=!1;m.lastIndexOf("!")===m.length-1&&(x=!0,m=m.slice(0,m.length-1));var _=Vo(m,e[p.index]),b=Math.max(g+2*_,0),w=x?0:2*_,S=aa(i),M=S?i:KV((y+w)/b);b=g+2*(_=(y-M*g)/2/(x?M:Math.max(M-1,1))),w=x?0:2*_,S||"fixed"===i||(M=u?KV((Math.abs(u)+w)/b):0),v=M*b-w,h.repeatTimes=M,h.symbolMargin=_}var I=f*(v/2),T=h.pathPosition=[];T[d.index]=n[d.wh]/2,T[p.index]="start"===a?I:"end"===a?l-I:l/2,o&&(T[0]+=o[0],T[1]+=o[1]);var C=h.bundlePosition=[];C[d.index]=n[d.xy],C[p.index]=n[p.xy];var D=h.barRectShape=k({},n);D[p.wh]=f*Math.max(Math.abs(n[p.wh]),Math.abs(T[p.index]+I)),D[d.wh]=n[d.wh];var A=h.clipShape={};A[d.xy]=-n[d.xy],A[d.wh]=c.ecSize[d.wh],A[p.xy]=0,A[p.wh]=n[p.wh]}(n,d,r,o,0,p,s,h.valueLineWidth,h.boundingLength,h.repeatCutLength,i,h),h}function NV(t,e){return t.toGlobalCoord(t.dataToCoord(t.scale.parse(e)))}function EV(t){var e=t.symbolPatternSize,n=xx(t.symbolType,-e/2,-e/2,e,e);return n.attr({culling:!0}),"image"!==n.type&&n.setStyle({strokeNoScale:!0}),n}function zV(t,e,n,i){var r=t.__pictorialBundle,o=n.symbolSize,a=n.valueLineWidth,s=n.pathPosition,l=e.valueDim,u=n.repeatTimes||0,c=0,h=o[e.valueDim.index]+a+2*n.symbolMargin;for(ZV(t,function(t){t.__pictorialAnimationIndex=c,t.__pictorialRepeatTimes=u,c0:i<0)&&(r=u-1-t),e[l.index]=h*(r-u/2+.5)+s[l.index],{x:e[0],y:e[1],scaleX:n.symbolScale[0],scaleY:n.symbolScale[1],rotation:n.rotation}}}function BV(t,e,n,i){var r=t.__pictorialBundle,o=t.__pictorialMainPath;o?jV(o,null,{x:n.pathPosition[0],y:n.pathPosition[1],scaleX:n.symbolScale[0],scaleY:n.symbolScale[1],rotation:n.rotation},n,i):(o=t.__pictorialMainPath=EV(n),r.add(o),jV(o,{x:n.pathPosition[0],y:n.pathPosition[1],scaleX:0,scaleY:0,rotation:n.rotation},{scaleX:n.symbolScale[0],scaleY:n.symbolScale[1]},n,i))}function VV(t,e,n){var i=k({},e.barRectShape),r=t.__pictorialBarRect;r?jV(r,null,{shape:i},e,n):((r=t.__pictorialBarRect=new lu({z2:2,shape:i,silent:!0,style:{stroke:"transparent",fill:"transparent",lineWidth:0}})).disableMorphing=!0,t.add(r))}function GV(t,e,n,i){if(n.symbolClip){var r=t.__pictorialClipPath,o=k({},n.clipShape),a=e.valueDim,s=n.animationModel,l=n.dataIndex;if(r)rd(r,{shape:o},s,l);else{o[a.wh]=0,r=new lu({shape:o}),t.__pictorialBundle.setClipPath(r),t.__pictorialClipPath=r;var u={};u[a.wh]=n.clipShape[a.wh],ep[i?"updateProps":"initProps"](r,{shape:u},s,l)}}}function FV(t,e){var n=t.getItemModel(e);return n.getAnimationDelayParams=HV,n.isAnimationEnabled=WV,n}function HV(t){return{index:t.__pictorialAnimationIndex,count:t.__pictorialRepeatTimes}}function WV(){return this.parentModel.isAnimationEnabled()&&!!this.getShallow("animation")}function UV(t,e,n,i){var r=new vo,o=new vo;return r.add(o),r.__pictorialBundle=o,o.x=n.bundlePosition[0],o.y=n.bundlePosition[1],n.symbolRepeat?zV(r,e,n):BV(r,0,n),VV(r,n,i),GV(r,e,n,i),r.__pictorialShapeStr=XV(t,n),r.__pictorialSymbolMeta=n,r}function YV(t,e,n,i){var r=i.__pictorialBarRect;r&&r.removeTextContent();var o=[];ZV(i,function(t){o.push(t)}),i.__pictorialMainPath&&o.push(i.__pictorialMainPath),i.__pictorialClipPath&&(n=null),B(o,function(t){sd(t,{scaleX:0,scaleY:0},n,e,function(){i.parent&&i.parent.remove(i)})}),t.setItemGraphicEl(e,null)}function XV(t,e){return[t.getItemVisual(e.dataIndex,"symbol")||"none",!!e.symbolRepeat,!!e.symbolClip].join(":")}function ZV(t,e,n){B(t.__pictorialBundle.children(),function(i){i!==t.__pictorialBarRect&&e.call(n,i)})}function jV(t,e,n,i,r,o){e&&t.attr(e),i.symbolClip&&!r?n&&t.attr(n):n&&ep[r?"updateProps":"initProps"](t,n,i.animationModel,i.dataIndex,o)}function qV(t,e,n){var i=n.dataIndex,r=n.itemModel,o=r.getModel("emphasis"),a=o.getModel("itemStyle").getItemStyle(),s=r.getModel(["blur","itemStyle"]).getItemStyle(),l=r.getModel(["select","itemStyle"]).getItemStyle(),u=r.getShallow("cursor"),c=o.get("focus"),h=o.get("blurScope"),d=o.get("scale");ZV(t,function(t){if(t instanceof eu){var e=t.style;t.useStyle(k({image:e.image,x:e.x,y:e.y,width:e.width,height:e.height},n.style))}else t.useStyle(n.style);var i=t.ensureState("emphasis");i.style=a,d&&(i.scaleX=1.1*t.scaleX,i.scaleY=1.1*t.scaleY),t.ensureState("blur").style=s,t.ensureState("select").style=l,u&&(t.cursor=u),t.z2=n.z2});var p=e.valueDim.posDesc[+(n.boundingLength>0)],f=t.__pictorialBarRect;f.ignoreClip=!0,op(f,ap(r),{labelFetcher:e.seriesModel,labelDataIndex:i,defaultText:jT(e.seriesModel.getData(),i),inheritColor:n.style.fill,defaultOpacity:n.style.opacity,defaultOutsidePosition:p}),kc(t,c,h,o.get("disabled"))}function KV(t){var e=Math.round(t);return Math.abs(t-e)<1e-4?e:Math.ceil(t)}var $V=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.hasSymbolVisual=!0,n.defaultSymbol="roundRect",n}return n(e,t),e.prototype.getInitialData=function(e){return e.stack=null,t.prototype.getInitialData.apply(this,arguments)},e.type="series."+OD,e.dependencies=["grid"],e.defaultOption=Op(qD.defaultOption,{symbol:"circle",symbolSize:null,symbolRotate:null,symbolPosition:null,symbolOffset:null,symbolMargin:null,symbolRepeat:!1,symbolRepeatDirection:"end",symbolClip:!1,symbolBoundingData:null,symbolPatternSize:400,barGap:"-100%",clip:!1,progressive:0,emphasis:{scale:!1},select:{itemStyle:{borderColor:ug.color.primary}}}),e}(qD);var JV="themeRiver",QV=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(e){t.prototype.init.apply(this,arguments),this.legendVisualProvider=new SA(U(this.getData,this),U(this.getRawData,this))},e.prototype.fixData=function(t){var e=t.length,n={},i=Fa(t,function(t){return n.hasOwnProperty(t[0]+"")||(n[t[0]+""]=-1),t[2]}),r=[];i.buckets.each(function(t,e){r.push({name:e,dataList:t})});for(var o=r.length,a=0;ao&&(o=s),i.push(s)}for(var u=0;uo&&(o=h)}return{y0:r,max:o}}(l),c=u.y0,h=n/u.max,d=o.length,p=o[0].indices.length,f=0;fI&&!$o(C-I)&&C0?(r.virtualPiece?r.virtualPiece.updateData(!1,i,t,e,n):(r.virtualPiece=new iG(i,t,e,n),l.add(r.virtualPiece)),o.piece.off("click"),r.virtualPiece.on("click",function(t){r._rootToNode(o.parentNode)})):r.virtualPiece&&(l.remove(r.virtualPiece),r.virtualPiece=null)}(a,s),this._initEvents(),this._oldChildren=c},e.prototype._initEvents=function(){var t=this;this.group.off("click"),this.group.on("click",function(e){var n=!1;t.seriesModel.getViewRoot().eachNode(function(i){if(!n&&i.piece&&i.piece===e.target){var r=i.getModel().get("nodeClick");if("rootToNode"===r)t._rootToNode(i);else if("link"===r){var o=i.getModel(),a=o.get("link");if(a)Ef(a,o.get("target",!0)||"_blank")}n=!0}})})},e.prototype._rootToNode=function(t){t!==this.seriesModel.getViewRoot()&&this.api.dispatchAction({type:sG,from:this.uid,seriesId:this.seriesModel.id,targetNode:t})},e.prototype.containPoint=function(t,e){var n=e.getData().getItemLayout(0);if(n){var i=t[0]-n.cx,r=t[1]-n.cy,o=Math.sqrt(i*i+r*r);return o<=n.r&&o>=n.r0}},e.type=rG,e}(tm),cG=os(rG,function(t){var e={};t.eachSeriesByType(rG,function(t){var n=t.getData(),i=n.tree;i.eachNode(function(r){var o=r.getModel().getModel("itemStyle").getItemStyle();o.fill||(o.fill=function(t,n,i){if(0===t.depth)return ug.color.neutral50;for(var r=t;r&&r.depth>1;)r=r.parentNode;var o=n.getColorFromPalette(r.name||r.dataIndex+"",e);return t.depth>1&&j(o)&&(o=yi(o,(t.depth-1)/(i-1)*.5)),o}(r,t,i.root.height)),k(n.ensureUniqueItemVisual(r.dataIndex,"style"),o)})})});var hG=Math.PI/180,dG=os(rG,function(t,e){t.eachSeriesByType(rG,function(t){var n=t.get("center"),i=t.get("radius");X(i)||(i=[0,i]),X(n)||(n=[n,n]);var r=e.getWidth(),o=e.getHeight(),a=Math.min(r,o),s=Vo(n[0],r),l=Vo(n[1],o),u=Vo(i[0],a/2),c=Vo(i[1],a/2),h=-t.get("startAngle")*hG,d=t.get("minAngle")*hG,p=t.getData().tree.root,f=t.getViewRoot(),g=f.depth,v=t.get("sort");null!=v&&pG(f,v);var y=0;B(f.children,function(t){!isNaN(t.getValue())&&y++});var m=f.getValue(),x=Math.PI/(m||y)*2,_=f.depth>0,b=f.height-(_?-1:1),w=(c-u)/(b||1),S=t.get("clockwise"),M=t.get("stillShowZeroSum"),I=S?1:-1,T=function(e,n){if(e){var i=n;if(e!==p){var r=e.getValue(),o=0===m&&M?x:r*x;o0&&(a.during=s?U(OG,{el:e,userDuring:s}):null,a.setToFinal=!0,a.scope=t),k(a,n[o]),a}function CG(t,e,n,i){var r=(i=i||{}).dataIndex,o=i.isInit,a=i.clearStyle,s=n.isAnimationEnabled(),l=IG(t),u=e.style;l.userDuring=e.during;var c={},h={};if(function(t,e,n){for(var i=0;i=0)){var h=t.getAnimationStyleProps(),d=h?h.style:null;if(d){!r&&(r=i.style={});var p=W(n);for(u=0;u0&&t.animateFrom(g,v)}else!function(t,e,n,i,r){if(r){var o=TG("update",t,e,i,n);o.duration>0&&t.animateFrom(r,o)}}(t,e,r||0,n,c);DG(t,e),u?t.dirty():t.markRedraw()}function DG(t,e){for(var n=IG(t).leaveToProps,i=0;i=0){!o&&(o=i[t]={});var d=W(a);for(c=0;ci[1]&&i.reverse(),{coordSys:{type:"polar",cx:t.cx,cy:t.cy,r:i[1],r0:i[0]},api:{coord:function(i){var r=e.dataToRadius(i[0]),o=n.dataToAngle(i[1]),a=t.coordToPoint([r,o]);return a.push(r,o*Math.PI/180),a},size:U(bG,t)}}},calendar:function(t){var e=t.getRect(),n=t.getRangeInfo();return{coordSys:{type:"calendar",x:e.x,y:e.y,width:e.width,height:e.height,cellWidth:t.getCellWidth(),cellHeight:t.getCellHeight(),rangeInfo:{start:n.start,end:n.end,weeks:n.weeks,dayCount:n.allDay}},api:{coord:function(e,n){return t.dataToPoint(e,n)},layout:function(e,n){return t.dataToLayout(e,n)}}}},matrix:function(t){var e=t.getRect();return{coordSys:{type:"matrix",x:e.x,y:e.y,width:e.width,height:e.height},api:{coord:function(e,n){return t.dataToPoint(e,n)},layout:function(e,n){return t.dataToLayout(e,n)}}}}};function $G(t){return t instanceof Kl}function JG(t){return t instanceof Zs}var QG=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n,i){this._progressiveEls=null;var r=this._data,o=t.getData(),a=this.group,s=rF(t,o,e,n);r||a.removeAll(),o.diff(r).add(function(e){aF(n,null,e,s(e,i),t,a,o)}).remove(function(e){var n=r.getItemGraphicEl(e);n&&AG(n,vG(n).option,t)}).update(function(e,l){var u=r.getItemGraphicEl(l);aF(n,u,e,s(e,i),t,a,o)}).execute();var l=t.get("clip",!0)?xC(t.coordinateSystem,!1,t):null;l?a.setClipPath(l):a.removeClipPath(),this._data=o},e.prototype.incrementalPrepareRender=function(t,e,n){this.group.removeAll(),this._data=null},e.prototype.incrementalRender=function(t,e,n,i,r){var o=e.getData(),a=rF(e,o,n,i),s=this._progressiveEls=[];function l(t){t.isGroup||(t.incremental=is(e),t.ensureState("emphasis").hoverLayer=2)}for(var u=t.start;u=0?e.getStore().get(r,n):void 0}var o=e.get(i.name,n),a=i&&i.ordinalMeta;return a?a.categories[o]:o},styleEmphasis:function(n,i){0;null==i&&(i=l);var r=x(i,FG).getItemStyle(),o=_(i,FG),a=sp(o,null,null,!0,!0);a.text=o.getShallow("show")?st(t.getFormattedLabel(i,FG),t.getFormattedLabel(i,HG),jT(e,i)):null;var s=lp(o,null,!0);return w(n,r),r=CC(r,a,s),n&&b(r,n),r.legacy=!0,r},visual:function(t,n){if(null==n&&(n=l),wt(fG,t)){var i=e.getItemVisual(n,"style");return i?i[fG[t]]:null}if(wt(gG,t))return e.getItemVisual(n,t)},barLayout:function(t){if("cartesian2d"===a.type){return function(t){if(vw(t.axis.scale)){for(var e=bM(t.axis),n=[],i=0;i=h;f--){var g=e.childAt(f);dF(e,g,r)}}(t,h,n,i,r),a>=0?o.replaceAt(h,a):o.add(h),h}function lF(t,e,n){var i,r=vG(t),o=e.type,a=e.shape,s=e.style;return n.isUniversalTransitionEnabled()||null!=o&&o!==r.customGraphicType||"path"===o&&((i=a)&&(wt(i,"pathData")||wt(i,"d")))&&vF(a)!==r.customPathData||"image"===o&&wt(s,"image")&&s.image!==r.customImagePath}function uF(t,e,n){var i=e?cF(t,e):t,r=e?hF(t,i,FG):t.style,o=t.type,a=i?i.textConfig:null,s=t.textContent,l=s?e?cF(s,e):s:null;if(r&&(n.isLegacy||MC(r,o,!!a,!!l))){n.isLegacy=!0;var u=IC(r,o,!e);!a&&u.textConfig&&(a=u.textConfig),!l&&u.textContent&&(l=u.textContent)}if(!e&&l){var c=l;!c.type&&(c.type="text")}var h=e?n[e]:n.normal;h.cfg=a,h.conOpt=l}function cF(t,e){return e?t?t[e]:null:t}function hF(t,e,n){var i=e&&e.style;return null==i&&n===FG&&t&&(i=t.styleEmphasis),i}function dF(t,e,n){e&&AG(e,vG(t).option,n)}function pF(t,e){var n=t&&t.name;return null!=n?n:"e\0\0"+e}function fF(t,e){var n=this.context,i=null!=t?n.newChildren[t]:null,r=null!=e?n.oldChildren[e]:null;sF(n.api,r,n.dataIndex,i,n.seriesModel,n.group)}function gF(t){var e=this.context,n=e.oldChildren[t];n&&AG(n,vG(n).option,e.seriesModel)}function vF(t){return t&&(t.pathData||t.d)}var yF=Pa(),mF=C,xF=U,_F=function(){function t(){this._dragging=!1,this.animationThreshold=15}return t.prototype.render=function(t,e,n,i){var r=e.get("value"),o=e.get("status");if(this._axisModel=t,this._axisPointerModel=e,this._api=n,i||this._lastValue!==r||this._lastStatus!==o){this._lastValue=r,this._lastStatus=o;var a=this._group,s=this._handle;if(!o||"hide"===o)return a&&a.hide(),void(s&&s.hide());a&&a.show(),s&&s.show();var l={};this.makeElOption(l,r,t,e,n);var u=l.graphicKey;u!==this._lastGraphicKey&&this.clear(n),this._lastGraphicKey=u;var c=this._moveAnimation=this.determineAnimation(t,e);if(a){var h=Y(bF,e,c);this.updatePointerEl(a,l,h),this.updateLabelEl(a,l,h,e)}else a=this._group=new vo,this.createPointerEl(a,l,t,e),this.createLabelEl(a,l,t,e),n.getZr().add(a);IF(a,e,!0),this._renderHandle(r)}},t.prototype.remove=function(t){this.clear(t)},t.prototype.dispose=function(t){this.clear(t)},t.prototype.determineAnimation=function(t,e){var n=e.get("animation"),i=t.axis,r="category"===i.type,o=e.get("snap");if(!o&&!r)return!1;if("auto"===n||null==n){var a=this.animationThreshold;if(r&&bM(i).w>a)return!0;if(o){var s=mk(t).seriesDataCount,l=i.getExtent();return Math.abs(l[0]-l[1])/s>a}return!1}return!0===n},t.prototype.makeElOption=function(t,e,n,i,r){},t.prototype.createPointerEl=function(t,e,n,i){var r=e.pointer;if(r){var o=yF(t).pointerEl=new ep[r.type](mF(e.pointer));t.add(o)}},t.prototype.createLabelEl=function(t,e,n,i){if(e.label){var r=yF(t).labelEl=new du(mF(e.label));t.add(r),SF(r,i)}},t.prototype.updatePointerEl=function(t,e,n){var i=yF(t).pointerEl;i&&e.pointer&&(i.setStyle(e.pointer.style),n(i,{shape:e.pointer.shape}))},t.prototype.updateLabelEl=function(t,e,n,i){var r=yF(t).labelEl;r&&(r.setStyle(e.label.style),n(r,{x:e.label.x,y:e.label.y}),SF(r,i))},t.prototype._renderHandle=function(t){if(!this._dragging&&this.updateHandleTransform){var e,n=this._axisPointerModel,i=this._api.getZr(),r=this._handle,o=n.getModel("handle"),a=n.get("status");if(!o.get("show")||!a||"hide"===a)return r&&i.remove(r),void(this._handle=null);this._handle||(e=!0,r=this._handle=Od(o.get("icon"),{cursor:"move",draggable:!0,onmousemove:function(t){ye(t.event)},onmousedown:xF(this._onHandleDragMove,this,0,0),drift:xF(this._onHandleDragMove,this),ondragend:xF(this._onHandleDragEnd,this)}),i.add(r)),IF(r,n,!1),r.setStyle(o.getItemStyle(null,["color","borderColor","borderWidth","opacity","shadowColor","shadowBlur","shadowOffsetX","shadowOffsetY"]));var s=o.get("size");X(s)||(s=[s,s]),r.scaleX=s[0]/2,r.scaleY=s[1]/2,cm(this,"_doDispatchAxisPointer",o.get("throttle")||0,"fixRate"),this._moveHandleToValue(t,e)}},t.prototype._moveHandleToValue=function(t,e){bF(this._axisPointerModel,!e&&this._moveAnimation,this._handle,MF(this.getHandleTransform(t,this._axisModel,this._axisPointerModel)))},t.prototype._onHandleDragMove=function(t,e){var n=this._handle;if(n){this._dragging=!0;var i=this.updateHandleTransform(MF(n),[t,e],this._axisModel,this._axisPointerModel);this._payloadInfo=i,n.stopAnimation(),n.attr(MF(i)),yF(n).lastProp=null,this._doDispatchAxisPointer()}},t.prototype._doDispatchAxisPointer=function(){if(this._handle){var t=this._payloadInfo,e=this._axisModel;this._api.dispatchAction({type:"updateAxisPointer",x:t.cursorPoint[0],y:t.cursorPoint[1],tooltipOption:t.tooltipOption,axesInfo:[{axisDim:e.axis.dim,axisIndex:e.componentIndex}]})}},t.prototype._onHandleDragEnd=function(){if(this._dragging=!1,this._handle){var t=this._axisPointerModel.get("value");this._moveHandleToValue(t),this._api.dispatchAction({type:"hideTip"})}},t.prototype.clear=function(t){this._lastValue=null,this._lastStatus=null;var e=t.getZr(),n=this._group,i=this._handle;e&&n&&(this._lastGraphicKey=null,n&&e.remove(n),i&&e.remove(i),this._group=null,this._handle=null,this._payloadInfo=null),hm(this,"_doDispatchAxisPointer")},t.prototype.doClear=function(){},t.prototype.buildLabel=function(t,e,n){return{x:t[n=n||0],y:t[1-n],width:e[n],height:e[1-n]}},t}();function bF(t,e,n,i){wF(yF(n).lastProp,i)||(yF(n).lastProp=i,e?rd(n,i,t):(n.stopAnimation(),n.attr(i)))}function wF(t,e){if($(t)&&$(e)){var n=!0;return B(e,function(e,i){n=n&&wF(t[i],e)}),!!n}return t===e}function SF(t,e){t[e.get(["label","show"])?"show":"hide"]()}function MF(t){return{x:t.x||0,y:t.y||0,rotation:t.rotation||0}}function IF(t,e,n){var i=e.get("z"),r=e.get("zlevel");t&&t.traverse(function(t){"group"!==t.type&&(null!=i&&(t.z=i),null!=r&&(t.zlevel=r),t.silent=n)})}function TF(t){var e,n=t.get("type"),i=t.getModel(n+"Style");return"line"===n?(e=i.getLineStyle()).fill=null:"shadow"===n&&((e=i.getAreaStyle()).stroke=null),e}function CF(t,e,n,i,r){var o=DF(n.get("value"),e.axis,e.ecModel,n.get("seriesDataIndices"),{precision:n.get(["label","precision"]),formatter:n.get(["label","formatter"])}),a=n.getModel("label"),s=Df(a.get("padding")||0),l=a.getFont(),u=qr(o,l),c=r.position,h=u.width+s[1]+s[3],d=u.height+s[0]+s[2],p=r.align;"right"===p&&(c[0]-=h),"center"===p&&(c[0]-=h/2);var f=r.verticalAlign;"bottom"===f&&(c[1]-=d),"middle"===f&&(c[1]-=d/2),function(t,e,n,i){var r=i.getWidth(),o=i.getHeight();t[0]=Math.min(t[0]+e,r)-e,t[1]=Math.min(t[1]+n,o)-n,t[0]=Math.max(t[0],0),t[1]=Math.max(t[1],0)}(c,h,d,i);var g=a.get("backgroundColor");g&&"auto"!==g||(g=e.get(["axisLine","lineStyle","color"])),t.label={x:c[0],y:c[1],style:sp(a,{text:o,font:l,fill:a.getTextColor(),padding:s,backgroundColor:g}),z2:10}}function DF(t,e,n,i,r){t=e.scale.parse(t);var o=e.scale.getLabel({value:t},{precision:r.precision}),a=r.formatter;if(a){var s={value:jw(e,{value:t}),axisDimension:e.dim,axisIndex:e.index,seriesData:[]};B(i,function(t){var e=n.getSeriesByIndex(t.seriesIndex),i=t.dataIndexInside,r=e&&e.getDataParams(i);r&&s.seriesData.push(r)}),j(a)?o=a.replace("{value}",o):Z(a)&&(o=a(s))}return o}function AF(t,e,n){var i=[1,0,0,1,0,0];return Ce(i,i,n.rotation),Te(i,i,n.position),Cd([t.dataToCoord(e),(n.labelOffset||0)+(n.labelDirection||1)*(n.labelMargin||0)],i)}function kF(t,e,n,i,r,o){var a=dD.innerTextLayout(n.rotation,0,n.labelDirection);n.labelMargin=r.get(["label","margin"]),CF(e,i,r,o,{position:AF(i.axis,t,n),align:a.textAlign,verticalAlign:a.textVerticalAlign})}function LF(t,e,n){return{x1:t[n=n||0],y1:t[1-n],x2:e[n],y2:e[1-n]}}function PF(t,e,n){return{x:t[n=n||0],y:t[1-n],width:e[n],height:e[1-n]}}function OF(t,e,n,i,r,o){return{cx:t,cy:e,r0:n,r:i,startAngle:r,endAngle:o,clockwise:!0}}function RF(t,e,n){return bM(t,{fromStat:{sers:V(e,function(t){return n.getSeriesByIndex(t.seriesIndex)})},min:1}).w}function NF(t,e,n){return[Do(Co(e[0],e[1]),t-n/2),Co(t+n/2,Do(e[0],e[1]))]}var EF=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.makeElOption=function(t,e,n,i,r){var o=n.axis,a=o.grid,s=i.get("type"),l=o.getGlobalExtent(),u=zF(a,o).getOtherAxis(o).getGlobalExtent(),c=o.toGlobalCoord(o.dataToCoord(e,!0));if(s&&"none"!==s){var h=TF(i),d=BF[s](o,c,l,u,i.get("seriesDataIndices"),i.ecModel);d.style=h,t.graphicKey=d.type,t.pointer=d}kF(e,t,MD(a.getRect(),n),n,i,r)},e.prototype.getHandleTransform=function(t,e,n){var i=MD(e.axis.grid.getRect(),e,{labelInside:!1});i.labelMargin=n.get(["handle","margin"]);var r=AF(e.axis,t,i);return{x:r[0],y:r[1],rotation:i.rotation+(i.labelDirection<0?Math.PI:0)}},e.prototype.updateHandleTransform=function(t,e,n,i){var r=n.axis,o=r.grid,a=r.getGlobalExtent(!0),s=zF(o,r).getOtherAxis(r).getGlobalExtent(),l="x"===r.dim?0:1,u=[t.x,t.y];u[l]+=e[l],u[l]=Co(a[1],u[l]),u[l]=Do(a[0],u[l]);var c=(s[1]+s[0])/2,h=[c,c];h[l]=u[l];return{x:u[0],y:u[1],rotation:t.rotation,cursorPoint:h,tooltipOption:[{verticalAlign:"middle"},{align:"center"}][l]}},e}(_F);function zF(t,e){var n={};return n[e.dim+"AxisIndex"]=e.index,t.getCartesian(n)}var BF={line:function(t,e,n,i){return{type:"Line",subPixelOptimize:!0,shape:LF([e,i[0]],[e,i[1]],VF(t))}},shadow:function(t,e,n,i,r,o){var a=RF(t,r,o),s=i[1]-i[0],l=NF(e,n,a),u=l[0],c=l[1];return{type:"Rect",shape:PF([u,i[0]],[c-u,s],VF(t))}}};function VF(t){return"x"===t.dim?0:1}var GF=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="axisPointer",e.defaultOption={show:"auto",z:50,type:"line",snap:!1,triggerTooltip:!0,triggerEmphasis:!0,value:null,status:null,link:[],animation:null,animationDurationUpdate:200,lineStyle:{color:ug.color.border,width:1,type:"dashed"},shadowStyle:{color:ug.color.shadowTint},label:{show:!0,formatter:null,precision:"auto",margin:3,color:ug.color.neutral00,padding:[5,7,5,7],backgroundColor:ug.color.accent60,borderColor:null,borderWidth:0,borderRadius:3},handle:{show:!1,icon:"M10.7,11.9v-1.3H9.3v1.3c-4.9,0.3-8.8,4.4-8.8,9.4c0,5,3.9,9.1,8.8,9.4h1.3c4.9-0.3,8.8-4.4,8.8-9.4C19.5,16.3,15.6,12.2,10.7,11.9z M13.3,24.4H6.7v-1.2h6.6z M13.3,22H6.7v-1.2h6.6z M13.3,19.6H6.7v-1.2h6.6z",size:45,margin:50,color:ug.color.accent40,throttle:40}},e}(lg),FF=Pa(),HF=B;function WF(t,e,n){if(!r.node){var i=e.getZr();FF(i).records||(FF(i).records={}),function(t,e){if(FF(t).initialized)return;function n(n,i){t.on(n,function(n){var r=function(t){var e={showTip:[],hideTip:[]},n=function(i){var r=e[i.type];r?r.push(i):(i.dispatchAction=n,t.dispatchAction(i))};return{dispatchAction:n,pendings:e}}(e);HF(FF(t).records,function(t){t&&i(t,n,r.dispatchAction)}),function(t,e){var n,i=t.showTip.length,r=t.hideTip.length;i?n=t.showTip[i-1]:r&&(n=t.hideTip[r-1]);n&&(n.dispatchAction=null,e.dispatchAction(n))}(r.pendings,e)})}FF(t).initialized=!0,n("click",Y(YF,"click")),n("mousemove",Y(YF,"mousemove")),n("mousewheel",Y(YF,"mousewheel")),n("globalout",UF)}(i,e),(FF(i).records[t]||(FF(i).records[t]={})).handler=n}}function UF(t,e,n){t.handler("leave",null,n)}function YF(t,e,n,i){e.handler(t,n,i)}function XF(t,e){if(!r.node){var n=e.getZr();(FF(n).records||{})[t]&&(FF(n).records[t]=null)}}var ZF=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){var i=e.getComponent("tooltip"),r=t.get("triggerOn")||i&&i.get("triggerOn")||"mousemove|click|mousewheel";WF("axisPointer",n,function(t,e,n){"none"!==r&&("leave"===t||r.indexOf(t)>=0)&&n({type:"updateAxisPointer",currTrigger:t,x:e&&e.offsetX,y:e&&e.offsetY})})},e.prototype.remove=function(t,e){XF("axisPointer",e)},e.prototype.dispose=function(t,e){XF("axisPointer",e)},e.type="axisPointer",e}(Ky);function jF(t,e){var n,i=[],r=t.seriesIndex;if(null==r||!(n=e.getSeriesByIndex(r)))return{point:[]};var o=n.getData(),a=La(o,t);if(null==a||a<0||X(a))return{point:[]};var s=o.getItemGraphicEl(a),l=n.coordinateSystem;if(n.getTooltipPosition)i=n.getTooltipPosition(a)||[];else if(l&&l.dataToPoint)if(t.isStacked){var u=l.getBaseAxis(),c=l.getOtherAxis(u).dim,h=u.dim,d="x"===c||"radius"===c?1:0,p=o.mapDimension(h),f=[];f[d]=o.get(p,a),f[1-d]=o.get(o.getCalculationInfo("stackResultDimension"),a),i=l.dataToPoint(f)||[]}else i=l.dataToPoint(o.getValues(V(l.dimensions,function(t){return o.mapDimension(t)}),a))||[];else if(s){var g=s.getBoundingRect().clone();g.applyTransform(s.transform),i=[g.x+g.width/2,g.y+g.height/2]}return{point:i,el:s}}var qF=Pa();function KF(t,e,n){var i=t.currTrigger,r=[t.x,t.y],o=t,a=t.dispatchAction||U(n.dispatchAction,n),s=e.getComponent("axisPointer").coordSysAxesInfo;if(s){eH(r)&&(r=jF({seriesIndex:o.seriesIndex,dataIndex:o.dataIndex},e).point);var l=eH(r),u=o.axesInfo,c=s.axesInfo,h="leave"===i||eH(r),d={},p={},f={list:[],map:{}},g={showPointer:Y(JF,p),showTooltip:Y(QF,f)};B(s.coordSysMap,function(t,e){var n=l||t.containPoint(r);B(s.coordSysAxesInfo[e],function(t,e){var i=t.axis,o=function(t,e){for(var n=0;n<(t||[]).length;n++){var i=t[n];if(e.axis.dim===i.axisDim&&e.axis.model.componentIndex===i.axisIndex)return i}}(u,t);if(!h&&n&&(!u||o)){var a=o&&o.value;null!=a||l||(a=i.pointToData(r)),null!=a&&$F(t,a,g,!1,d)}})});var v={};return B(c,function(t,e){var n=t.linkGroup;n&&!p[e]&&B(n.axesInfo,function(e,i){var r=p[i];if(e!==t&&r){var o=r.value;n.mapper&&(o=t.axis.scale.parse(n.mapper(o,tH(e),tH(t)))),v[t.key]=o}})}),B(v,function(t,e){$F(c[e],t,g,!0,d)}),function(t,e,n){var i=n.axesInfo=[];B(e,function(e,n){var r=e.axisPointerModel.option,o=t[n];o?(!e.useHandle&&(r.status="show"),r.value=o.value,r.seriesDataIndices=(o.payloadBatch||[]).slice()):!e.useHandle&&(r.status="hide"),"show"===r.status&&i.push({axisDim:e.axis.dim,axisIndex:e.axis.model.componentIndex,value:r.value})})}(p,c,d),function(t,e,n,i){if(eH(e)||!t.list.length)return void i({type:"hideTip"});var r=((t.list[0].dataByAxis[0]||{}).seriesDataIndices||[])[0]||{};i({type:"showTip",escapeConnect:!0,x:e[0],y:e[1],tooltipOption:n.tooltipOption,position:n.position,dataIndexInside:r.dataIndexInside,dataIndex:r.dataIndex,seriesIndex:r.seriesIndex,dataByCoordSys:t.list})}(f,r,t,a),function(t,e,n){var i=n.getZr(),r="axisPointerLastHighlights",o=qF(i)[r]||{},a=qF(i)[r]={};B(t,function(t,e){var n=t.axisPointerModel.option;"show"===n.status&&t.triggerEmphasis&&B(n.seriesDataIndices,function(t){a[t.seriesIndex+"|"+t.dataIndex]=t})});var s=[],l=[];function u(t){return{seriesIndex:t.seriesIndex,dataIndex:t.dataIndex}}B(o,function(t,e){!a[e]&&l.push(u(t))}),B(a,function(t,e){!o[e]&&s.push(u(t))}),l.length&&n.dispatchAction({type:"downplay",escapeConnect:!0,notBlur:!0,batch:l}),s.length&&n.dispatchAction({type:"highlight",escapeConnect:!0,notBlur:!0,batch:s})}(c,0,n),d}}function $F(t,e,n,i,r){var o=t.axis;if(!o.scale.isBlank()&&o.containData(e))if(t.involveSeries){var a=function(t,e){var n=e.axis,i=n.dim,r=t,o=[],a=Number.MAX_VALUE,s=-1;return B(e.seriesModels,function(e,l){var u,c,h=e.getData().mapDimensionsAll(i);if(e.getAxisTooltipData){var d=e.getAxisTooltipData(h,t,n);c=d.dataIndices,u=d.nestestValue}else{if(!(c=e.indicesOfNearest(i,h[0],t,"category"===n.type?.5:null)).length)return;u=e.getData().get(h[0],c[0])}if(ca(u)){var p=t-u,f=Math.abs(p);f<=a&&((f=0&&s<0)&&(a=f,s=p,r=u,o.length=0),B(c,function(t){o.push({seriesIndex:e.seriesIndex,dataIndexInside:t,dataIndex:e.getData().getRawIndex(t)})}))}}),{payloadBatch:o,snapToValue:r}}(e,t),s=a.payloadBatch,l=a.snapToValue;s[0]&&null==r.seriesIndex&&k(r,s[0]),!i&&t.snap&&o.containData(l)&&null!=l&&(e=l),n.showPointer(t,e,s),n.showTooltip(t,a,l)}else n.showPointer(t,e)}function JF(t,e,n,i){t[e.key]={value:n,payloadBatch:i}}function QF(t,e,n,i){var r=n.payloadBatch,o=e.axis,a=o.model,s=e.axisPointerModel;if(e.triggerTooltip&&r.length){var l=e.coordSys.model,u=_k(l),c=t.map[u];c||(c=t.map[u]={coordSysId:l.id,coordSysIndex:l.componentIndex,coordSysType:l.type,coordSysMainType:l.mainType,dataByAxis:[]},t.list.push(c)),c.dataByAxis.push({axisDim:o.dim,axisIndex:a.componentIndex,axisType:a.type,axisId:a.id,value:i,valueLabelOpt:{precision:s.get(["label","precision"]),formatter:s.get(["label","formatter"])},seriesDataIndices:r.slice()})}}function tH(t){var e=t.axis.model,n={},i=n.axisDim=t.axis.dim;return n.axisIndex=n[i+"AxisIndex"]=e.componentIndex,n.axisName=n[i+"AxisName"]=e.name,n.axisId=n[i+"AxisId"]=e.id,n}function eH(t){return!t||null==t[0]||isNaN(t[0])||null==t[1]||isNaN(t[1])}function nH(t){wk.registerAxisPointerClass("CartesianAxisPointer",EF),t.registerComponentModel(GF),t.registerComponentView(ZF),t.registerPreprocessor(function(t){if(t){(!t.axisPointer||0===t.axisPointer.length)&&(t.axisPointer={});var e=t.axisPointer.link;e&&!X(e)&&(t.axisPointer.link=[e])}}),t.registerProcessor(t.PRIORITY.PROCESSOR.STATISTIC,{overallReset:function(t,e){t.getComponent("axisPointer").coordSysAxesInfo=vk(t,e)}}),t.registerAction({type:"updateAxisPointer",event:"updateAxisPointer",update:":updateAxisPointer"},KF)}var iH=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.makeElOption=function(t,e,n,i,r){var o=n.axis;"angle"===o.dim&&(this.animationThreshold=Math.PI/18);var a=o.polar,s=o.getExtent(),l=a.getOtherAxis(o).getExtent(),u=o.dataToCoord(e),c=i.get("type");if(c&&"none"!==c){var h=TF(i),d=rH[c](o,a,u,s,l,i.get("seriesDataIndices"),i.ecModel);d.style=h,t.graphicKey=d.type,t.pointer=d}var p=function(t,e,n,i,r){var o=e.axis,a=o.dataToCoord(t),s=i.getAngleAxis().getExtent()[0];s=s/180*Math.PI;var l,u,c,h=i.getRadiusAxis().getExtent();if("radius"===o.dim){var d=[1,0,0,1,0,0];Ce(d,d,s),Te(d,d,[i.cx,i.cy]),l=Cd([a,-r],d);var p=e.getModel("axisLabel").get("rotate")||0,f=dD.innerTextLayout(s,p*Math.PI/180,-1);u=f.textAlign,c=f.textVerticalAlign}else{var g=h[1];l=i.coordToPoint([g+r,a]);var v=i.cx,y=i.cy;u=Math.abs(l[0]-v)/g<.3?"center":l[0]>v?"left":"right",c=Math.abs(l[1]-y)/g<.3?"middle":l[1]>y?"top":"bottom"}return{position:l,align:u,verticalAlign:c}}(e,n,0,a,i.get(["label","margin"]));CF(t,n,i,r,p)},e}(_F);var rH={line:function(t,e,n,i,r){return"angle"===t.dim?{type:"Line",shape:LF(e.coordToPoint([r[0],n]),e.coordToPoint([r[1],n]))}:{type:"Circle",shape:{cx:e.cx,cy:e.cy,r:n}}},shadow:function(t,e,n,i,r,o,a){var s,l=Math.PI/180,u=RF(t,o,a);if("angle"===t.dim)s=OF(e.cx,e.cy,r[0],r[1],(-n-u/2)*l,(u/2-n)*l);else{var c=NF(n,i,u),h=c[0],d=c[1];s=OF(e.cx,e.cy,h,d,0,2*Math.PI)}return{type:"Sector",shape:s}}},oH="polar",aH=oH,sH=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.findAxisModel=function(t){var e;return this.ecModel.eachComponent(t,function(t){t.getCoordSysModel()===this&&(e=t)},this),e},e.type=oH,e.dependencies=["radiusAxis","angleAxis"],e.defaultOption={z:0,center:["50%","50%"],radius:"80%"},e}(lg),lH=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.getCoordSysModel=function(){return this.getReferringComponents("polar",Ea).models[0]},e.type="polarAxis",e}(lg);E(lH,nS);var uH=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="angleAxis",e}(lH),cH=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="radiusAxis",e}(lH),hH=function(t){function e(e,n){return t.call(this,"radius",e,n)||this}return n(e,t),e.prototype.pointToData=function(t,e){return this.polar.pointToData(t,e)["radius"===this.dim?0:1]},e}(SM);hH.prototype.dataToRadius=SM.prototype.dataToCoord,hH.prototype.radiusToData=SM.prototype.coordToData;var dH=Pa(),pH=function(t){function e(e,n){return t.call(this,"angle",e,n||[0,360])||this}return n(e,t),e.prototype.pointToData=function(t,e){return this.polar.pointToData(t,e)["radius"===this.dim?0:1]},e.prototype.calculateCategoryInterval=function(){var t=this,e=t.getLabelModel(),n=t.scale,i=n.getExtent(),r=n.count();if(i[1]-i[0]<1)return 0;var o=i[0],a=t.dataToCoord(o+1)-t.dataToCoord(o),s=Math.abs(a),l=qr(null==o?"":o+"",e.getFont(),"center","top"),u=Math.max(l.height,7)/s;isNaN(u)&&(u=1/0);var c=Math.max(0,Math.floor(u)),h=dH(t.model),d=h.lastAutoInterval,p=h.lastTickCount;return null!=d&&null!=p&&Math.abs(d-c)<=1&&Math.abs(p-r)<=1&&d>c?c=d:(h.lastTickCount=r,h.lastAutoInterval=c),c},e}(SM);pH.prototype.dataToAngle=SM.prototype.dataToCoord,pH.prototype.angleToData=SM.prototype.coordToData;var fH=["radius","angle"],gH=function(){function t(t){this.dimensions=fH,this.type=oH,this.cx=0,this.cy=0,this._radiusAxis=new hH,this._angleAxis=new pH,this.axisPointerEnabled=!0,this.name=t||"",this._radiusAxis.polar=this._angleAxis.polar=this}return t.prototype.containPoint=function(t){var e=this.pointToCoord(t);return this._radiusAxis.contain(e[0])&&this._angleAxis.contain(e[1])},t.prototype.containData=function(t){return this._radiusAxis.containData(t[0])&&this._angleAxis.containData(t[1])},t.prototype.getAxis=function(t){return this["_"+t+"Axis"]},t.prototype.getAxes=function(){return[this._radiusAxis,this._angleAxis]},t.prototype.getAxesByScale=function(t){var e=[],n=this._angleAxis,i=this._radiusAxis;return n.scale.type===t&&e.push(n),i.scale.type===t&&e.push(i),e},t.prototype.getAngleAxis=function(){return this._angleAxis},t.prototype.getRadiusAxis=function(){return this._radiusAxis},t.prototype.getOtherAxis=function(t){var e=this._angleAxis;return t===e?this._radiusAxis:e},t.prototype.getBaseAxis=function(){return this.getAxesByScale("ordinal")[0]||this.getAxesByScale("time")[0]||this.getAngleAxis()},t.prototype.getTooltipAxes=function(t){var e=null!=t&&"auto"!==t?this.getAxis(t):this.getBaseAxis();return{baseAxes:[e],otherAxes:[this.getOtherAxis(e)]}},t.prototype.dataToPoint=function(t,e,n){return this.coordToPoint([this._radiusAxis.dataToRadius(t[0],e),this._angleAxis.dataToAngle(t[1],e)],n)},t.prototype.pointToData=function(t,e,n){n=n||[];var i=this.pointToCoord(t);return n[0]=this._radiusAxis.radiusToData(i[0],e),n[1]=this._angleAxis.angleToData(i[1],e),n},t.prototype.pointToCoord=function(t){var e=t[0]-this.cx,n=t[1]-this.cy,i=this.getAngleAxis(),r=i.getExtent(),o=Math.min(r[0],r[1]),a=Math.max(r[0],r[1]);i.inverse?o=a-360:a=o+360;var s=Math.sqrt(e*e+n*n);e/=s,n/=s;for(var l=Math.atan2(-n,e)/Math.PI*180,u=la;)l+=360*u;return[s,l]},t.prototype.coordToPoint=function(t,e){e=e||[];var n=t[0],i=t[1]/180*Math.PI;return e[0]=Math.cos(i)*n+this.cx,e[1]=-Math.sin(i)*n+this.cy,e},t.prototype.getArea=function(){var t=this.getAngleAxis(),e=this.getRadiusAxis().getExtent().slice();e[0]>e[1]&&e.reverse();var n=t.getExtent(),i=Math.PI/180,r=1e-4;return{cx:this.cx,cy:this.cy,r0:e[0],r:e[1],startAngle:-n[0]*i,endAngle:-n[1]*i,clockwise:t.inverse,contain:function(t,e){var n=t-this.cx,i=e-this.cy,o=n*n+i*i,a=this.r,s=this.r0;return a!==s&&o-r<=a*a&&o+r>=s*s},x:this.cx-e[1],y:this.cy-e[1],width:2*e[1],height:2*e[1]}},t.prototype.convertToPixel=function(t,e,n){return vH(e)===this?this.dataToPoint(n):null},t.prototype.convertFromPixel=function(t,e,n){return vH(e)===this?this.pointToData(n):null},t}();function vH(t){var e=t.seriesModel,n=t.polarModel;return n&&n.coordinateSystem||e&&e.coordinateSystem}function yH(t,e){var n=this.getAngleAxis(),i=this.getRadiusAxis();if(CS(n,1),CS(i,1),RS(n),RS(i),"category"===n.type&&!n.onBand){var r=n.getExtent(),o=360/n.scale.count();n.inverse?r[1]+=o:r[1]-=o,n.setExtent(r[0],r[1])}}function mH(t,e){var n;if(t.type=Hw(e),t.scale=Ww(e,t.type,!1),t.onBand=eS(t.scale,e),t.inverse=e.get("inverse"),function(t){return"angleAxis"===t.mainType}(e)){t.inverse=t.inverse!==e.get("clockwise");var i=e.get("startAngle"),r=null!==(n=e.get("endAngle"))&&void 0!==n?n:i+(t.inverse?-360:360);t.setExtent(i,r)}e.axis=t,t.model=e}var xH={dimensions:fH,create:function(t,e){var n=[];return t.eachComponent(aH,function(t,i){var r=new gH(i+"");r.update=yH;var o=r.getRadiusAxis(),a=r.getAngleAxis(),s=t.findAxisModel("radiusAxis"),l=t.findAxisModel("angleAxis");mH(o,s),mH(a,l),function(t,e,n){var i=e.get("center"),r=eg(e,n).refContainer;t.cx=Vo(i[0],r.width)+r.x,t.cy=Vo(i[1],r.height)+r.y;var o=t.getRadiusAxis(),a=Math.min(r.width,r.height)/2,s=e.get("radius");null==s?s=[0,"100%"]:X(s)||(s=[0,s]);var l=[Vo(s[0],a),Vo(s[1],a)];o.inverse?o.setExtent(l[1],l[0]):o.setExtent(l[0],l[1])}(r,t,e),n.push(r),t.coordinateSystem=r,r.model=t}),t.eachSeries(function(t){if(t.get("coordinateSystem")===oH){var e=t.getReferringComponents(aH,Ea).models[0];0;var n=t.coordinateSystem=e.coordinateSystem;n&&(yS(n.getRadiusAxis(),t,oH),yS(n.getAngleAxis(),t,oH))}}),n}},_H=["axisLine","axisLabel","axisTick","minorTick","splitLine","minorSplitLine","splitArea"];function bH(t,e,n){e[1]>e[0]&&(e=e.slice().reverse());var i=t.coordToPoint([e[0],n]),r=t.coordToPoint([e[1],n]);return{x1:i[0],y1:i[1],x2:r[0],y2:r[1]}}function wH(t){return t.getRadiusAxis().inverse?0:1}function SH(t){var e=t[0],n=t[t.length-1];e&&n&&Math.abs(Math.abs(e.coord-n.coord)-360)<1e-4&&t.pop()}var MH=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.axisPointerClass="PolarAxisPointer",n}return n(e,t),e.prototype.render=function(t,e){if(this.group.removeAll(),t.get("show")){var n=t.axis,i=n.polar,r=i.getRadiusAxis().getExtent(),o=n.getTicksCoords({breakTicks:"none"}),a=n.getMinorTicksCoords(),s=[];B(n.getViewLabels(),function(t){if(!t.tick.offInterval){t=C(t);var e=n.scale;t.coord=n.dataToCoord(tS(e,t.tick)),s.push(t)}}),SH(s),SH(o),B(_H,function(e){!t.get([e,"show"])||n.scale.isBlank()&&"axisLine"!==e||IH[e](this.group,t,i,o,a,r,s)},this)}},e.type="angleAxis",e}(wk),IH={axisLine:function(t,e,n,i,r,o){var a,s=e.getModel(["axisLine","lineStyle"]),l=n.getAngleAxis(),u=Math.PI/180,c=l.getExtent(),h=wH(n),d=h?0:1,p=360===Math.abs(c[1]-c[0])?"Circle":"Arc";(a=0===o[d]?new ep[p]({shape:{cx:n.cx,cy:n.cy,r:o[h],startAngle:-c[0]*u,endAngle:-c[1]*u,clockwise:l.inverse},style:s.getLineStyle(),z2:1,silent:!0}):new Ih({shape:{cx:n.cx,cy:n.cy,r:o[h],r0:o[d]},style:s.getLineStyle(),z2:1,silent:!0})).style.fill=null,t.add(a)},axisTick:function(t,e,n,i,r,o){var a=e.getModel("axisTick"),s=(a.get("inside")?-1:1)*a.get("length"),l=o[wH(n)],u=V(i,function(t){return new Oh({shape:bH(n,[l,l+s],t.coord)})});t.add(wd(u,{style:P(a.getModel("lineStyle").getLineStyle(),{stroke:e.get(["axisLine","lineStyle","color"])})}))},minorTick:function(t,e,n,i,r,o){if(r.length){for(var a=e.getModel("axisTick"),s=e.getModel("minorTick"),l=(a.get("inside")?-1:1)*s.get("length"),u=o[wH(n)],c=[],h=0;hf?"left":"right",y=Math.abs(p[1]-g)/d<.3?"middle":p[1]>g?"top":"bottom";if(s&&s[h]){var m=s[h];$(m)&&m.textStyle&&(a=new kp(m.textStyle,l,l.ecModel))}var x=new du({silent:dD.isLabelSilent(e),style:sp(a,{x:p[0],y:p[1],fill:a.getTextColor()||e.get(["axisLine","lineStyle","color"]),text:i.formattedLabel,align:v,verticalAlign:y})});if(t.add(x),Gd({el:x,componentModel:e,itemName:i.formattedLabel,formatterParamsExtra:{isTruncated:function(){return x.isTruncated},value:i.rawLabel,tickIndex:r}}),c){var _=dD.makeAxisEventDataBase(e);_.targetType="axisLabel",_.value=i.rawLabel,Iu(x).eventData=_}},this)},splitLine:function(t,e,n,i,r,o){var a=e.getModel("splitLine").getModel("lineStyle"),s=a.get("color"),l=0;s=s instanceof Array?s:[s];for(var u=[],c=0;c=0?"p":"n",I=x;y&&(i[o][S]||(i[o][S]={p:x,n:x}),I=i[o][S][M]);var T=void 0,C=void 0,D=void 0,A=void 0;if("radius"===c.dim){var k=c.dataToCoord(w)-x,L=t.dataToCoord(S);Ao(k)=A})}}(t,e,n,i)})})});var PH={startAngle:90,clockwise:!0,splitNumber:12,containShape:!1,axisLabel:{rotate:0}},OH={splitNumber:5},RH=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="polar",e}(Ky);function NH(t,e){e=e||{};var n=t.coordinateSystem,i=t.axis,r={},o=i.position,a=i.orient,s=n.getRect(),l=[s.x,s.x+s.width,s.y,s.y+s.height],u={horizontal:{top:l[2],bottom:l[3]},vertical:{left:l[0],right:l[1]}};r.position=["vertical"===a?u.vertical[o]:l[0],"horizontal"===a?u.horizontal[o]:l[3]];r.rotation=Math.PI/2*{horizontal:0,vertical:1}[a];r.labelDirection=r.tickDirection=r.nameDirection={top:-1,bottom:1,right:1,left:-1}[o],t.get(["axisTick","inside"])&&(r.tickDirection=-r.tickDirection),ot(e.labelInside,t.get(["axisLabel","inside"]))&&(r.labelDirection=-r.labelDirection);var c=t.get(["axisLabel","rotate"]);return r.labelRotate="top"===o?-c:c,r.z2=1,r}var EH=["splitArea","splitLine","breakArea"],zH=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.axisPointerClass="SingleAxisPointer",n}return n(e,t),e.prototype.render=function(e,n,i,r){var o=this.group;o.removeAll();var a=this._axisGroup;this._axisGroup=new vo;var s=NH(e),l=new dD(e,i,s);l.build(),o.add(this._axisGroup),o.add(l.group),B(EH,function(t){e.get([t,"show"])&&BH[t](this,this.group,this._axisGroup,e,i)},this),kd(a,this._axisGroup,e),t.prototype.render.call(this,e,n,i,r)},e.prototype.remove=function(){Ik(this)},e.type="singleAxis",e}(wk),BH={splitLine:function(t,e,n,i,r){var o=i.axis;if(!o.scale.isBlank()){var a=i.getModel("splitLine"),s=a.getModel("lineStyle"),l=s.get("color");l=l instanceof Array?l:[l];for(var u=s.get("width"),c=i.coordinateSystem.getRect(),h=o.isHorizontal(),d=[],p=0,f=o.getTicksCoords({tickModel:a,breakTicks:"none",pruneByBreak:"preserve_extent_bound"}),g=[],v=[],y=0;y=e.y&&t[1]<=e.y+e.height:n.contain(n.toLocalCoord(t[1]))&&t[0]>=e.y&&t[0]<=e.y+e.height},t.prototype.pointToData=function(t,e,n){n=n||[];var i=this.getAxis();return n[0]=i.coordToData(i.toLocalCoord(t["horizontal"===i.orient?0:1])),n},t.prototype.dataToPoint=function(t,e,n){var i=this.getAxis(),r=this.getRect();n=n||[];var o="horizontal"===i.orient?0:1;return t instanceof Array&&(t=t[0]),n[o]=i.toGlobalCoord(i.dataToCoord(+t)),n[1-o]=0===o?r.y+r.height/2:r.x+r.width/2,n},t.prototype.convertToPixel=function(t,e,n){return HH(e)===this?this.dataToPoint(n):null},t.prototype.convertFromPixel=function(t,e,n){return HH(e)===this?this.pointToData(n):null},t}();function HH(t){var e=t.seriesModel,n=t.singleAxisModel;return n&&n.coordinateSystem||e&&e.coordinateSystem}var WH={create:function(t,e){var n=[];return t.eachComponent(Nk,function(i,r){var o=new FH(i,t,e);o.name="single_"+r,o.resize(i,e),i.coordinateSystem=o,n.push(o)}),t.eachSeries(function(t){if("singleAxis"===t.get("coordinateSystem")){var e=t.getReferringComponents(Nk,Ea).models[0],n=t.coordinateSystem=e&&e.coordinateSystem;n&&yS(n.getAxis(),t,Rk)}}),n},dimensions:GH},UH=["x","y"],YH=["width","height"],XH=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.makeElOption=function(t,e,n,i,r){var o=n.axis,a=o.coordinateSystem,s=jH(o),l=qH(a,s),u=qH(a,1-s),c=a.dataToPoint(e)[0],h=i.get("type");if(h&&"none"!==h){var d=TF(i),p=ZH[h](o,c,l,u,i.get("seriesDataIndices"),i.ecModel);p.style=d,t.graphicKey=p.type,t.pointer=p}kF(e,t,NH(n),n,i,r)},e.prototype.getHandleTransform=function(t,e,n){var i=NH(e,{labelInside:!1});i.labelMargin=n.get(["handle","margin"]);var r=AF(e.axis,t,i);return{x:r[0],y:r[1],rotation:i.rotation+(i.labelDirection<0?Math.PI:0)}},e.prototype.updateHandleTransform=function(t,e,n,i){var r=n.axis,o=r.coordinateSystem,a=jH(r),s=qH(o,a),l=[t.x,t.y];l[a]+=e[a],l[a]=Math.min(s[1],l[a]),l[a]=Math.max(s[0],l[a]);var u=qH(o,1-a),c=(u[1]+u[0])/2,h=[c,c];return h[a]=l[a],{x:l[0],y:l[1],rotation:t.rotation,cursorPoint:h,tooltipOption:{verticalAlign:"middle"}}},e}(_F),ZH={line:function(t,e,n,i){return{type:"Line",subPixelOptimize:!0,shape:LF([e,i[0]],[e,i[1]],jH(t))}},shadow:function(t,e,n,i,r,o){var a=RF(t,r,o),s=i[1]-i[0],l=NF(e,n,a),u=l[0],c=l[1];return{type:"Rect",shape:PF([u,i[0]],[c-u,s],jH(t))}}};function jH(t){return t.isHorizontal()?0:1}function qH(t,e){var n=t.getRect();return[n[UH[e]],n[UH[e]]+n[YH[e]]]}var KH=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="single",e}(Ky);var $H=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(e,n,i){var r=og(e);t.prototype.init.apply(this,arguments),JH(e,r)},e.prototype.mergeOption=function(e){t.prototype.mergeOption.apply(this,arguments),JH(this.option,e)},e.prototype.getCellSize=function(){return this.option.cellSize},e.type="calendar",e.layoutMode="box",e.defaultOption={z:2,left:80,top:60,cellSize:20,orient:"horizontal",splitLine:{show:!0,lineStyle:{color:ug.color.axisLine,width:1,type:"solid"}},itemStyle:{color:ug.color.neutral00,borderWidth:1,borderColor:ug.color.neutral10},dayLabel:{show:!0,firstDay:0,position:"start",margin:ug.size.s,color:ug.color.secondary},monthLabel:{show:!0,position:"start",margin:ug.size.s,align:"center",formatter:null,color:ug.color.secondary},yearLabel:{show:!0,position:null,margin:ug.size.xl,formatter:null,color:ug.color.quaternary,fontFamily:"sans-serif",fontWeight:"bolder",fontSize:20}},e}(lg);function JH(t,e){var n,i=t.cellSize;1===(n=X(i)?i:t.cellSize=[i,i]).length&&(n[1]=n[0]);var r=V([0,1],function(t){return function(t,e){return null!=t[Zf[e][0]]||null!=t[Zf[e][1]]&&null!=t[Zf[e][2]]}(e,t)&&(n[t]="auto"),null!=n[t]&&"auto"!==n[t]});rg(t,e,{type:"box",ignoreSize:r})}var QH=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){var i=this.group;i.removeAll();var r=t.coordinateSystem,o=r.getRangeInfo(),a=r.getOrient(),s=e.getLocaleModel();this._renderDayRect(t,o,i),this._renderLines(t,o,a,i),this._renderYearText(t,o,a,i),this._renderMonthText(t,s,a,i),this._renderWeekText(t,s,o,a,i)},e.prototype._renderDayRect=function(t,e,n){for(var i=t.coordinateSystem,r=t.getModel("itemStyle").getItemStyle(),o=i.getCellWidth(),a=i.getCellHeight(),s=e.start.time;s<=e.end.time;s=i.getNextNDay(s,1).time){var l=i.dataToCalendarLayout([s],!1).tl,u=new lu({shape:{x:l[0],y:l[1],width:o,height:a},cursor:"default",style:r});n.add(u)}},e.prototype._renderLines=function(t,e,n,i){var r=this,o=t.coordinateSystem,a=t.getModel(["splitLine","lineStyle"]).getLineStyle(),s=t.get(["splitLine","show"]),l=a.lineWidth;this._tlpoints=[],this._blpoints=[],this._firstDayOfMonth=[],this._firstDayPoints=[];for(var u=e.start,c=0;u.time<=e.end.time;c++){d(u.formatedDate),0===c&&(u=o.getDateInfo(e.start.y+"-"+e.start.m));var h=u.date;h.setMonth(h.getMonth()+1),u=o.getDateInfo(h)}function d(e){r._firstDayOfMonth.push(o.getDateInfo(e)),r._firstDayPoints.push(o.dataToCalendarLayout([e],!1).tl);var l=r._getLinePointsOfOneWeek(t,e,n);r._tlpoints.push(l[0]),r._blpoints.push(l[l.length-1]),s&&r._drawSplitline(l,a,i)}d(o.getNextNDay(e.end.time,1).formatedDate),s&&this._drawSplitline(r._getEdgesPoints(r._tlpoints,l,n),a,i),s&&this._drawSplitline(r._getEdgesPoints(r._blpoints,l,n),a,i)},e.prototype._getEdgesPoints=function(t,e,n){var i=[t[0].slice(),t[t.length-1].slice()],r="horizontal"===n?0:1;return i[0][r]=i[0][r]-e/2,i[1][r]=i[1][r]+e/2,i},e.prototype._drawSplitline=function(t,e,n){var i=new kh({z2:20,shape:{points:t},style:e});n.add(i)},e.prototype._getLinePointsOfOneWeek=function(t,e,n){for(var i=t.coordinateSystem,r=i.getDateInfo(e),o=[],a=0;a<7;a++){var s=i.getNextNDay(r.time,a),l=i.dataToCalendarLayout([s.time],!1);o[2*s.day]=l.tl,o[2*s.day+1]=l["horizontal"===n?"bl":"tr"]}return o},e.prototype._formatterLabel=function(t,e){return j(t)&&t?Of(t,e):Z(t)?t(e):e.nameMap},e.prototype._yearTextPositionControl=function(t,e,n,i,r){var o=e[0],a=e[1],s=["center","bottom"];"bottom"===i?(a+=r,s=["center","top"]):"left"===i?o-=r:"right"===i?(o+=r,s=["center","top"]):a-=r;var l=0;return"left"!==i&&"right"!==i||(l=Math.PI/2),{rotation:l,x:o,y:a,style:{align:s[0],verticalAlign:s[1]}}},e.prototype._renderYearText=function(t,e,n,i){var r=t.getModel("yearLabel");if(r.get("show")){var o=r.get("margin"),a=r.get("position");a||(a="horizontal"!==n?"top":"left");var s=[this._tlpoints[this._tlpoints.length-1],this._blpoints[0]],l=(s[0][0]+s[1][0])/2,u=(s[0][1]+s[1][1])/2,c="horizontal"===n?0:1,h={top:[l,s[c][1]],bottom:[l,s[1-c][1]],left:[s[1-c][0],u],right:[s[c][0],u]},d=e.start.y;+e.end.y>+e.start.y&&(d=d+"-"+e.end.y);var p=r.get("formatter"),f={start:e.start.y,end:e.end.y,nameMap:d},g=this._formatterLabel(p,f),v=new du({z2:30,style:sp(r,{text:g}),silent:r.get("silent")});v.attr(this._yearTextPositionControl(v,h[a],n,a,o)),i.add(v)}},e.prototype._monthTextPositionControl=function(t,e,n,i,r){var o="left",a="top",s=t[0],l=t[1];return"horizontal"===n?(l+=r,e&&(o="center"),"start"===i&&(a="bottom")):(s+=r,e&&(a="middle"),"start"===i&&(o="right")),{x:s,y:l,align:o,verticalAlign:a}},e.prototype._renderMonthText=function(t,e,n,i){var r=t.getModel("monthLabel");if(r.get("show")){var o=r.get("nameMap"),a=r.get("margin"),s=r.get("position"),l=r.get("align"),u=[this._tlpoints,this._blpoints];o&&!j(o)||(o&&(e=Fp(o)||e),o=e.get(["time","monthAbbr"])||[]);var c="start"===s?0:1,h="horizontal"===n?0:1;a="start"===s?-a:a;for(var d="center"===l,p=r.get("silent"),f=0;f=r.start.time&&i.timea.end.time&&t.reverse(),t},t.prototype._getRangeInfo=function(t){var e,n=[this.getDateInfo(t[0]),this.getDateInfo(t[1])];n[0].time>n[1].time&&(e=!0,n.reverse());var i=Math.floor(n[1].time/tW)-Math.floor(n[0].time/tW)+1,r=new Date(n[0].time),o=r.getDate(),a=n[1].date.getDate();r.setDate(o+i-1);var s=r.getDate();if(s!==a)for(var l=r.getTime()-n[1].time>0?1:-1;(s=r.getDate())!==a&&(r.getTime()-n[1].time)*l>0;)i-=l,r.setDate(s-l);var u=Math.floor((i+n[0].day+6)/7),c=e?1-u:u-1;return e&&n.reverse(),{range:[n[0].formatedDate,n[1].formatedDate],start:n[0],end:n[1],allDay:i,weeks:u,nthWeek:c,fweek:n[0].day,lweek:n[1].day}},t.prototype._getDateByWeeksAndDay=function(t,e,n){var i=this._getRangeInfo(n);if(t>i.weeks||0===t&&ei.lweek)return null;var r=7*(t-1)-i.fweek+e,o=new Date(i.start.time);return o.setDate(+i.start.d+r),this.getDateInfo(o)},t.create=function(e,n){var i=[];return e.eachComponent("calendar",function(r){var o=new t(r,e,n);i.push(o),r.coordinateSystem=o}),e.eachComponent(function(t,e){Wf({targetModel:e,coordSysType:"calendar",coordSysProvider:Uf})}),i},t.dimensions=["time","value"],t}();function nW(t){var e=t.calendarModel,n=t.seriesModel;return e?e.coordinateSystem:n?n.coordinateSystem:null}var iW=1,rW=2,oW=3,aW={none:0,all:1,body:2,corner:3};function sW(t,e,n){var i=e[dd[n]].getCell(t);return!i&&K(t)&&t<0&&(i=e[dd[1-n]].getUnitLayoutInfo(n,Math.round(t))),i}function lW(t){var e=t||[];return e[0]=e[0]||[],e[1]=e[1]||[],e[0][0]=e[0][1]=e[1][0]=e[1][1]=NaN,e}function uW(t,e,n,i,r){cW(t[0],e,r,n,i,0),cW(t[1],e,r,n,i,1)}function cW(t,e,n,i,r,o){t[0]=1/0,t[1]=-1/0;var a=i[o],s=X(a)?a:[a],l=s.length,u=!!n;if(l>=1?(hW(t,e,s,u,r,o,0),l>1&&hW(t,e,s,u,r,o,l-1)):t[0]=t[1]=NaN,u){var c=-r[dd[1-o]].getLocatorCount(o),h=r[dd[o]].getLocatorCount(o)-1;n===aW.body?c=Do(0,c):n===aW.corner&&(h=Co(-1,h)),h=e[0]&&t[0]<=e[1]}function yW(t,e){t.id.set(e[0][0],e[1][0]),t.span.set(e[0][1]-t.id.x+1,e[1][1]-t.id.y+1)}function mW(t,e,n,i){var r=sW(e[i][0],n,i),o=sW(e[i][1],n,i);t[dd[i]]=t[pd[i]]=NaN,r&&o&&(t[dd[i]]=r.xy,t[pd[i]]=o.xy+o.wh-r.xy)}function xW(t,e,n,i){return t[dd[e]]=n,t[dd[1-e]]=i,t}var _W=function(){function t(t,e){this._cells=[],this._levels=[],this.dim=t,this.dimIdx="x"===t?0:1,this._model=e,this._uniqueValueGen=function(t){var e=t.toUpperCase(),n=new RegExp("^"+e+"([0-9]+)$"),i=0;function r(t){var e;null!=t&&(e=t.match(n))&&(i=Do(i,+e[1]+1))}function o(){return""+e+i++}function a(t,e){for(var n=mt(),i=0;i=1,x=n[dd[i]],_=o.getLocatorCount(i)-1,b=new Wa;for(a.resetLayoutIterator(b,i);b.next();)w(b.item);for(o.resetLayoutIterator(b,i);b.next();)w(b.item);function w(t){rt(t.wh)&&(t.wh=y),t.xy=x,t.id[dd[i]]!==_||m||(t.wh=n[dd[i]]+n[pd[i]]-t.xy),x+=t.wh}}function XW(t,e){for(var n=e[dd[t]].resetCellIterator();n.next();){var i=n.item;jW(i.rect,t,i.id,i.span,e),jW(i.rect,1-t,i.id,i.span,e),i.type===oW&&(i.xy=i.rect[dd[t]],i.wh=i.rect[pd[t]])}}function ZW(t,e){t.travelExistingCells(function(t){var n=t.span;if(n){var i=t.spanRect,r=t.id;jW(i,0,r,n,e),jW(i,1,r,n,e)}})}function jW(t,e,n,i,r){t[pd[e]]=0;var o=n[dd[e]]<0?r[dd[1-e]]:r[dd[e]],a=o.getUnitLayoutInfo(e,n[dd[e]]);if(t[dd[e]]=a.xy,t[pd[e]]=a.wh,i[dd[e]]>1){var s=o.getUnitLayoutInfo(e,n[dd[e]]+i[dd[e]]-1);t[pd[e]]=s.xy+s.wh-a.xy}}function qW(t,e){return Math.max(Math.min(t,at(e,1/0)),0)}function KW(t){var e=t.matrixModel,n=t.seriesModel;return e?e.coordinateSystem:n?n.coordinateSystem:null}var $W={inBody:1,inCorner:2,outside:3},JW={x:null,y:null,point:[]};function QW(t,e,n,i,r){var o=n[dd[e]],a=n[dd[1-e]],s=o.getUnitLayoutInfo(e,o.getLocatorCount(e)-1),l=o.getUnitLayoutInfo(e,0),u=a.getUnitLayoutInfo(e,-a.getLocatorCount(e)),c=a.shouldShow()?a.getUnitLayoutInfo(e,-1):null,h=t.point[e]=i[e];if(l||c)if(r!==aW.body)if(r!==aW.corner){var d=l?l.xy:c?c.xy+c.wh:NaN,p=u?u.xy:d,f=s?s.xy+s.wh:d;if(hf){if(!r)return void(t[dd[e]]=$W.outside);h=f}t.point[e]=h,t[dd[e]]=d<=h&&h<=f?$W.inBody:p<=h&&h<=d?$W.inCorner:$W.outside}else c?(t[dd[e]]=$W.inCorner,h=Co(c.xy+c.wh,Do(u.xy,h)),t.point[e]=h):t[dd[e]]=$W.outside;else l?(t[dd[e]]=$W.inBody,h=Co(s.xy+s.wh,Do(l.xy,h)),t.point[e]=h):t[dd[e]]=$W.outside;else t[dd[e]]=$W.outside}function tU(t,e,n,i){var r=1-n;if(t[dd[n]]!==$W.outside)for(i[dd[n]].resetCellIterator(UW);UW.next();){var o=UW.item;if(iU(t.point[n],o.rect,n)&&iU(t.point[r],o.rect,r))return e[n]=o.ordinal,void(e[r]=o.id[dd[r]])}}function eU(t,e,n,i){if(t[dd[n]]!==$W.outside)for((t[dd[n]]===$W.inCorner?i[dd[1-n]]:i[dd[n]]).resetLayoutIterator(WW,n);WW.next();)if(nU(t.point[n],WW.item))return void(e[n]=WW.item.id[dd[n]])}function nU(t,e){return e.xy<=t&&t<=e.xy+e.wh}function iU(t,e,n){return e[dd[n]]<=t&&t<=e[dd[n]]+e[pd[n]]}function rU(t,e){var n;return B(e,function(e){null!=t[e]&&"auto"!==t[e]&&(n=!0)}),n}var oU=["transition","enterFrom","leaveTo"],aU=oU.concat(["enterAnimation","updateAnimation","leaveAnimation"]);function sU(t,e,n){if(n&&(!t[n]&&e[n]&&(t[n]={}),t=t[n],e=e[n]),t&&e)for(var i=n?oU:aU,r=0;r=0;l--){var d,p,f;if(f=null!=(p=Da((d=n[l]).id,null))?r.get(p):null){var g=f.parent,v=(h=cU(g),{}),y=ng(f,d,g===i?{width:o,height:a}:{width:h.width,height:h.height},null,{hv:d.hv,boundingMode:d.bounding},v);if(!cU(f).isNew&&y){for(var m=d.transition,x={},_=0;_=0)?x[b]=w:f[b]=w}rd(f,x,t,0)}else f.attr(v)}}},e.prototype._clear=function(){var t=this,e=this._elMap;e.each(function(n){fU(n,cU(n).option,e,t._lastGraphicModel)}),this._elMap=mt()},e.prototype.dispose=function(){this._clear()},e.type="graphic",e}(Ky);function dU(t){var e=wt(uU,t)?uU[t]:md(t);var n=new e({});return cU(n).type=t,n}function pU(t,e,n,i){var r=dU(n);return e.add(r),i.set(t,r),cU(r).id=t,cU(r).isNew=!0,r}function fU(t,e,n,i){t&&t.parent&&("group"===t.type&&t.traverse(function(t){fU(t,e,n,i)}),AG(t,e,i),n.removeKey(cU(t).id))}function gU(t,e,n,i){t.isGroup||B([["cursor",Zs.prototype.cursor],["zlevel",i||0],["z",n||0],["z2",0]],function(n){var i=n[0];wt(e,i)?t[i]=at(e[i],n[1]):null==t[i]&&(t[i]=n[1])}),B(W(e),function(n){if(0===n.indexOf("on")){var i=e[n];t[n]=Z(i)?i:null}}),wt(e,"draggable")&&(t.draggable=e.draggable),null!=e.name&&(t.name=e.name),null!=e.id&&(t.id=e.id)}var vU=["x","y","radius","angle","single"],yU=Pa(),mU=["cartesian2d","polar","singleAxis"];function xU(t){return t+"Axis"}function _U(t,e){var n,i=mt(),r=[],o=mt();t.eachComponent({mainType:"dataZoom",query:e},function(t){o.get(t.uid)||s(t)});do{n=!1,t.eachComponent("dataZoom",a)}while(n);function a(t){!o.get(t.uid)&&function(t){var e=!1;return t.eachTargetAxis(function(t,n){var r=i.get(t);r&&r[n]&&(e=!0)}),e}(t)&&(s(t),n=!0)}function s(t){o.set(t.uid,!0),r.push(t),t.eachTargetAxis(function(t,e){(i.get(t)||i.set(t,[]))[e]=!0})}return r}function bU(t){var e=t.ecModel,n={infoList:[],infoMap:mt()};return t.eachTargetAxis(function(t,i){var r=e.getComponent(xU(t),i);if(r){var o=r.getCoordSysModel();if(o){var a=o.uid,s=n.infoMap.get(a);s||(s={model:o,axisModels:[]},n.infoList.push(s),n.infoMap.set(a,s)),s.axisModels.push(r)}}}),n}function wU(t){var e=yU(ax(t));return e.axisProxyMap||(e.axisProxyMap=mt())}function SU(t){if(t)return wU(t.ecModel).get(t.uid)}function MU(t,e){var n=e.getAxisModel().axis.__alignTo;return n&&t.getAxisProxy(n.dim,n.model.componentIndex)?SU(n.model):null}var IU=function(){function t(){this.indexList=[],this.indexMap=[]}return t.prototype.add=function(t){this.indexMap[t]||(this.indexList.push(t),this.indexMap[t]=!0)},t}(),TU=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n._autoThrottle=!0,n._noTarget=!0,n._rangePropMode=["percent","percent"],n}return n(e,t),e.prototype.init=function(t,e,n){var i=CU(t);this.settledOption=i,this.mergeDefaultAndTheme(t,n),this._doInit(i)},e.prototype.mergeOption=function(t){var e=CU(t);D(this.option,t,!0),D(this.settledOption,e,!0),this._doInit(e)},e.prototype._doInit=function(t){var e=this.option;this._setDefaultThrottle(t),this._updateRangeUse(t);var n=this.settledOption;B([["start","startValue"],["end","endValue"]],function(t,i){"value"===this._rangePropMode[i]&&(e[t[0]]=n[t[0]]=null)},this),this._resetTarget()},e.prototype._resetTarget=function(){var t=this.get("orient",!0),e=this._targetAxisInfoMap=mt();this._fillSpecifiedTargetAxis(e)?this._orient=t||this._makeAutoOrientByTargetAxis():(this._orient=t||"horizontal",this._fillAutoTargetAxisByOrient(e,this._orient)),this._noTarget=!0,e.each(function(t){t.indexList.length&&(this._noTarget=!1)},this)},e.prototype._fillSpecifiedTargetAxis=function(t){var e=!1;return B(vU,function(n){var i=this.getReferringComponents(xU(n),za);if(i.specified){e=!0;var r=new IU;B(i.models,function(t){r.add(t.componentIndex)}),t.set(n,r)}},this),e},e.prototype._fillAutoTargetAxisByOrient=function(t,e){var n=this.ecModel,i=!0;if(i){var r="vertical"===e?"y":"x";o(n.findComponents({mainType:r+"Axis"}),r)}i&&o(n.findComponents({mainType:"singleAxis",filter:function(t){return t.get("orient",!0)===e}}),"single");function o(e,n){var r=e[0];if(r){var o=new IU;if(o.add(r.componentIndex),t.set(n,o),i=!1,"x"===n||"y"===n){var a=r.getReferringComponents("grid",Ea).models[0];a&&B(e,function(t){r.componentIndex!==t.componentIndex&&a===t.getReferringComponents("grid",Ea).models[0]&&o.add(t.componentIndex)})}}}i&&B(vU,function(e){if(i){var r=n.findComponents({mainType:xU(e),filter:function(t){return"category"===t.get("type",!0)}});if(r[0]){var o=new IU;o.add(r[0].componentIndex),t.set(e,o),i=!1}}},this)},e.prototype._makeAutoOrientByTargetAxis=function(){var t;return this.eachTargetAxis(function(e){!t&&(t=e)},this),"y"===t?"vertical":"horizontal"},e.prototype._setDefaultThrottle=function(t){if(t.hasOwnProperty("throttle")&&(this._autoThrottle=!1),this._autoThrottle){var e=this.ecModel.option;this.option.throttle=e.animation&&e.animationDurationUpdate>0?100:20}},e.prototype._updateRangeUse=function(t){var e=this._rangePropMode,n=this.get("rangeMode");B([["start","startValue"],["end","endValue"]],function(i,r){var o=null!=t[i[0]],a=null!=t[i[1]];o&&!a?e[r]="percent":!o&&a?e[r]="value":n?e[r]=n[r]:o&&(e[r]="percent")})},e.prototype.noTarget=function(){return this._noTarget},e.prototype.getFirstTargetAxisModel=function(){var t;return this.eachTargetAxis(function(e,n){null==t&&(t=this.ecModel.getComponent(xU(e),n))},this),t},e.prototype.eachTargetAxis=function(t,e){this._targetAxisInfoMap.each(function(n,i){B(n.indexList,function(n){t.call(e,i,n)})})},e.prototype.getAxisProxy=function(t,e){return SU(this.getAxisModel(t,e))},e.prototype.getAxisModel=function(t,e){var n=this._targetAxisInfoMap.get(t);if(n&&n.indexMap[e])return this.ecModel.getComponent(xU(t),e)},e.prototype.setRawRange=function(t){var e=this.option,n=this.settledOption;B([["start","startValue"],["end","endValue"]],function(i){null==t[i[0]]&&null==t[i[1]]||(e[i[0]]=n[i[0]]=t[i[0]],e[i[1]]=n[i[1]]=t[i[1]])},this),this._updateRangeUse(t)},e.prototype.setCalculatedRange=function(t){var e=this.option;B(["start","startValue","end","endValue"],function(n){e[n]=t[n]})},e.prototype.getPercentRange=function(){var t=this.findRepresentativeAxisProxy();if(t)return t.getWindow().percent},e.prototype.getValueRange=function(t,e){if(null!=t||null!=e)return this.getAxisProxy(t,e).getWindow().value;var n=this.findRepresentativeAxisProxy();return n?n.getWindow().value:void 0},e.prototype.findRepresentativeAxisProxy=function(t){if(t)return SU(t);for(var e,n=this._targetAxisInfoMap.keys(),i=0;i=0}(e)){var n=xU(this._dimName),i=e.getReferringComponents(n,Ea).models[0];i&&this._axisIndex===i.componentIndex&&t.push(e)}},this),t},t.prototype.getAxisModel=function(){return this.ecModel.getComponent(this._dimName+"Axis",this._axisIndex)},t.prototype.getMinMaxSpan=function(){return C(this._minMaxSpan)},t.prototype.calculateDataWindow=function(t){var e,n=this._extent,i=this.getAxisModel().axis,r=i.scale,o=this._dataZoomModel.getRangePropMode(),a=[0,100],s=[],l=[],u=[!1,!1];B(["start","end"],function(i,c){var h=t[i],d=t[i+"Value"];"percent"===o[c]?(null==h&&(h=a[c]),d=Bo(h,a,n),u[c]=!0):(e=!0,null==d?d=n[c]:(d=r.parse(d),r.sanitize&&(d=r.sanitize(d,n))),h=Bo(d,n,a)),l[c]=null==d||isNaN(d)?n[c]:d,s[c]=null==h||isNaN(h)?a[c]:h}),Wo(l),Wo(s);var c=this._minMaxSpan;function h(t,e,n,i,r){var o=r?"Span":"ValueSpan";ez(0,t,n,"all",c["min"+o],c["max"+o]);for(var a=0;a<2;a++)e[a]=Bo(t[a],n,i,!0),r&&(e[a]=e[a],u[a]=!0);$a(e)}e?h(l,s,n,a,!1):h(s,l,a,n,!0);var d=vw(r)||fw(r),p=i.getExtent(),f=Ao(p[1]-p[0]),g=d?0:Xo(l,f,.5);B([[0,Po],[1,Lo]],function(t){var e=t[0],i=t[1];u[e]&&isFinite(g)&&(l[e]=Ho(l[e],g),l[e]=Co(n[1],Do(n[0],l[e])),s[e]===a[e]&&(l[e]=n[e],d&&(l[e]=i(l[e]))))}),$a(l);var v=[Bo(l[0],n,a,!0),Bo(l[1],n,a,!0)];return $a(v),{value:l,percent:s,percentInverted:v,valuePrecision:g}},t.prototype.reset=function(t,e){if(this.hostedBy(t)){var n=this.getAxisModel().axis;CS(n,2);var i=n.scale.rawExtentInfo;this._extent=i.makeNoZoom(),this._updateMinMaxSpan();var r=t.settledOption;e&&(r=P({start:e[0],end:e[1]},r));var o=this._window=this.calculateDataWindow(r),a=o.percent,s=o.value;0!==a[0]&&i.setZoomMM(0,s[0]),100!==a[1]&&i.setZoomMM(1,s[1])}},t.prototype.filterData=function(t,e){if(this.hostedBy(t)){var n=this._dimName,i=this.getTargetSeriesModels(),r=t.get("filterMode"),o=this._window.value;"none"!==r&&B(i,function(t){var e=t.getData(),i=e.mapDimensionsAll(n);if(i.length){if("weakFilter"===r){var a=e.getStore(),s=V(i,function(t){return e.getDimensionIndex(t)},e);e.filterSelf(function(t){for(var e,n,r,l=0;lo[1];if(c&&!h&&!d)return!0;c&&(r=!0),h&&(e=!0),d&&(n=!0)}return r&&e&&n})}else B(i,function(n){if("empty"===r)t.setData(e=e.map(n,function(t){return function(t){return t>=o[0]&&t<=o[1]}(t)?t:NaN}));else{var i={};i[n]=o,e.selectRange(i)}});B(i,function(t){e.setApproximateExtent(o,t)})}})}},t.prototype._updateMinMaxSpan=function(){var t=this._minMaxSpan={},e=this._dataZoomModel,n=this._extent;B(["min","max"],function(i){var r=e.get(i+"Span"),o=e.get(i+"ValueSpan");null!=o&&(o=this.getAxisModel().axis.scale.parse(o)),null!=o?r=Bo(n[0]+o,n,[0,100],!0):null!=r&&(o=Bo(r,[0,100],n,!0)-n[0]),t[i+"Span"]=r,t[i+"ValueSpan"]=o},this)},t}(),PU={dirtyOnOverallProgress:!0,getTargetSeries:function(t){var e,n=[];e=function(e,i,r,o){if(!SU(r)){var a=new LU(e,i,o,t);n.push(a),function(t,e){wU(t.ecModel).set(t.uid,e)}(r,a)}},t.eachComponent("dataZoom",function(n){n.eachTargetAxis(function(i,r){var o=t.getComponent(xU(i),r);e(i,r,o,n)})});var i=mt();return B(n,function(t){B(t.getTargetSeriesModels(),function(t){i.set(t.uid,t)})}),i},overallReset:function(t,e){t.eachComponent("dataZoom",function(t){var n=[];t.eachTargetAxis(function(e,i){var r=t.getAxisProxy(e,i),o=MU(t,r);o?n.push([r,o]):r.reset(t,null)}),B(n,function(e){e[0].reset(t,e[1].getWindow().percentInverted)}),t.eachTargetAxis(function(n,i){t.getAxisProxy(n,i).filterData(t,e)})}),t.eachComponent("dataZoom",function(t){var e=t.findRepresentativeAxisProxy();if(e){var n=e.getWindow(),i=n.percent,r=n.value;t.setCalculatedRange({start:i[0],end:i[1],startValue:r[0],endValue:r[1]})}})}};var OU=Ja();function RU(t){OU(t,function(){t.registerProcessor(t.PRIORITY.PROCESSOR.FILTER,PU),function(t){t.registerAction("dataZoom",function(t,e){B(_U(e,t),function(e){e.setRawRange({start:t.start,end:t.end,startValue:t.startValue,endValue:t.endValue})})})}(t),t.registerSubTypeDefaulter("dataZoom",function(){return"slider"})})}function NU(t){t.registerComponentModel(DU),t.registerComponentView(kU),RU(t)}var EU=function(){},zU={};function BU(t,e){zU[t]=e}function VU(t){return zU[t]}var GU=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(e,n,i){var r=i.getTheme().get("toolbox"),o=r?r.feature:null;o&&(this._themeFeatureOption=k({},o),r.feature={}),t.prototype.init.call(this,e,n,i),o&&(r.feature=o)},e.prototype.optionUpdated=function(){B(this.option.feature,function(t,e){var n=this._themeFeatureOption,i=VU(e);i&&(i.getDefaultOption&&(i.defaultOption=i.getDefaultOption(this.ecModel)),n&&n[e]&&(D(t,n[e]),n[e]=null),D(t,i.defaultOption))},this)},e.type="toolbox",e.layoutMode={type:"box",ignoreSize:!0},e.defaultOption={show:!0,z:6,orient:"horizontal",left:"right",top:"top",backgroundColor:"transparent",borderColor:ug.color.border,borderRadius:0,borderWidth:0,padding:ug.size.m,itemSize:15,itemGap:ug.size.s,showTitle:!0,iconStyle:{borderColor:ug.color.accent50,color:"none"},emphasis:{iconStyle:{borderColor:ug.color.accent70}},tooltip:{show:!1,position:"bottom"}},e}(lg);function FU(t,e){var n=Df(e.get("padding")),i=e.getItemStyle(["color","opacity"]);return i.fill=e.get("backgroundColor"),new lu({shape:{x:t.x-n[3],y:t.y-n[0],width:t.width+n[1]+n[3],height:t.height+n[0]+n[2],r:e.get("borderRadius")},style:i,silent:!0,z2:-1})}var HU=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.render=function(t,e,n,i){var r=this.group;if(r.removeAll(),t.get("show")){var o=+t.get("itemSize"),a="vertical"===t.get("orient"),s=t.get("feature")||{},l=this._features||(this._features=mt()),u=[];B(s,function(t,e){u.push(e)}),new mb(this._featureNames||[],u).add(f).update(f).remove(Y(f,null)).execute(),this._featureNames=F(u,function(t){return l.hasKey(t)});var c=eg(t,n).refContainer,h=t.getBoxLayoutParams(),d=t.get("padding"),p=Jf(h,c,d);qf(t.get("orient"),r,t.get("itemGap"),p.width,p.height),ng(r,h,c,d),r.add(FU(r.getBoundingRect(),t)),a||r.eachChild(function(t){var e=t.__title,i=t.ensureState("emphasis"),a=i.textConfig||(i.textConfig={}),s=t.getTextContent(),l=s&&s.ensureState("emphasis");if(l&&!Z(l)&&e){var u=l.style||(l.style={}),c=qr(e,du.makeFont(u)),h=t.x+r.x,d=!1;t.y+r.y+o+c.height>n.getHeight()&&(a.position="top",d=!0);var p=d?-5-c.height:o+10;h+c.width/2>n.getWidth()?(a.position=["100%",p],u.align="right"):h-c.width/2<0&&(a.position=[0,p],u.align="left")}})}function f(c,h){var d,p=null!=c&&null==h,f=null!=c&&null!=h,g=null==c,v=p||f?u[c]:u[h],y=s[v],m=p||f?new kp(y,t,e):null,x=m&&m.get("show");if(p){if(!x)return;if(function(t){return 0===t.indexOf("my")}(v))d={onclick:m.option.onclick,featureName:v};else{var _=VU(v);if(!_)return;d=new _}l.set(v,d)}else d=l.get(v);if(g||!x)return WU(d)&&d.dispose&&d.dispose(e,n),void l.removeKey(v);i&&null!=i.newTitle&&i.featureName===v&&(y.title=i.newTitle),p&&(d.uid=Pp("toolbox-feature")),d.model=m,d.ecModel=e,d.api=n,function(i,s,l){var u,c,h=i.getModel("iconStyle"),d=i.getModel(["emphasis","iconStyle"]),p=s instanceof EU&&s.getIcons?s.getIcons():i.get("icon"),f=i.get("title")||{};j(p)?(u={})[l]=p:u=p;j(f)?(c={})[l]=f:c=f;var g=i.iconPaths={};B(u,function(l,u){var p=Od(l,{},{x:-o/2,y:-o/2,width:o,height:o});p.setStyle(h.getItemStyle()),p.ensureState("emphasis").style=d.getItemStyle();var f=new du({style:{text:c[u],align:d.get("textAlign"),borderRadius:d.get("textBorderRadius"),padding:d.get("textPadding"),fill:null,font:pp({fontStyle:d.get("textFontStyle"),fontFamily:d.get("textFontFamily"),fontSize:d.get("textFontSize"),fontWeight:d.get("textFontWeight")},e)},ignore:!0});p.setTextContent(f),Gd({el:p,componentModel:t,itemName:u,formatterParamsExtra:{title:c[u]}}),p.__title=c[u],p.on("mouseover",function(){var e=d.getItemStyle(),i=a?null==t.get("right")&&"right"!==t.get("left")?"right":"left":null==t.get("bottom")&&"bottom"!==t.get("top")?"bottom":"top";f.setStyle({fill:d.get("textFill")||e.fill||e.stroke||ug.color.neutral99,backgroundColor:d.get("textBackgroundColor")}),p.setTextConfig({position:d.get("textPosition")||i}),f.ignore=!t.get("showTitle"),n.enterEmphasis(this)}).on("mouseout",function(){"emphasis"!==i.get(["iconStatus",u])&&n.leaveEmphasis(this),f.hide()}),("emphasis"===i.get(["iconStatus",u])?vc:yc)(p),r.add(p),p.on("click",U(s.onclick,s,e,n,u)),g[u]=p})}(m,d,v),m.setIconStatus=function(t,e){var n=this.option,i=this.iconPaths;n.iconStatus=n.iconStatus||{},n.iconStatus[t]=e,i[t]&&("emphasis"===e?vc:yc)(i[t])},WU(d)&&d.render&&d.render(m,e,n,i)}},e.prototype.updateView=function(t,e,n,i){B(this._features,function(t){t&&t instanceof EU&&t.updateView&&t.updateView(t.model,e,n,i)})},e.prototype.dispose=function(t,e){B(this._features,function(n){n&&n instanceof EU&&n.dispose&&n.dispose(t,e)})},e.type="toolbox",e}(Ky);function WU(t){return t instanceof EU}var UU=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.onclick=function(t,e){var n=this.model,i=n.get("name")||t.get("title.0.text")||"echarts",o="svg"===e.getZr().painter.getType(),a=o?"svg":n.get("type",!0)||"png",s=e.getConnectedDataURL({type:a,backgroundColor:n.get("backgroundColor",!0)||t.get("backgroundColor")||ug.color.neutral00,connectedBackgroundColor:n.get("connectedBackgroundColor"),excludeComponents:n.get("excludeComponents"),pixelRatio:n.get("pixelRatio")}),l=r.browser;if("function"!=typeof MouseEvent||!l.newEdge&&(l.ie||l.edge))if(window.navigator.msSaveOrOpenBlob||o){var u=s.split(","),c=u[0].indexOf("base64")>-1,h=o?decodeURIComponent(u[1]):u[1];c&&(h=window.atob(h));var d=i+"."+a;if(window.navigator.msSaveOrOpenBlob){for(var p=h.length,f=new Uint8Array(p);p--;)f[p]=h.charCodeAt(p);var g=new Blob([f]);window.navigator.msSaveOrOpenBlob(g,d)}else{var v=document.createElement("iframe");document.body.appendChild(v);var y=v.contentWindow,m=y.document;m.open("image/svg+xml","replace"),m.write(h),m.close(),y.focus(),m.execCommand("SaveAs",!0,d),document.body.removeChild(v)}}else{var x=n.get("lang"),_='',b=window.open();b.document.write(_),b.document.title=i}else{var w=document.createElement("a");w.download=i+"."+a,w.target="_blank",w.href=s;var S=new MouseEvent("click",{view:document.defaultView,bubbles:!0,cancelable:!1});w.dispatchEvent(S)}},e.getDefaultOption=function(t){return{show:!0,icon:"M4.7,22.9L29.3,45.5L54.7,23.4M4.6,43.6L4.6,58L53.8,58L53.8,43.6M29.2,45.1L29.2,0",title:t.getLocaleModel().get(["toolbox","saveAsImage","title"]),type:"png",connectedBackgroundColor:ug.color.neutral00,name:"",excludeComponents:["toolbox"],lang:t.getLocaleModel().get(["toolbox","saveAsImage","lang"])}},e}(EU),YU="__ec_magicType_stack__",XU=[["line","bar"],["stack"]],ZU=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.getIcons=function(){var t=this.model,e=t.get("icon"),n={};return B(t.get("type"),function(t){e[t]&&(n[t]=e[t])}),n},e.getDefaultOption=function(t){return{show:!0,type:[],icon:{line:"M4.1,28.9h7.1l9.3-22l7.4,38l9.7-19.7l3,12.8h14.9M4.1,58h51.4",bar:"M6.7,22.9h10V48h-10V22.9zM24.9,13h10v35h-10V13zM43.2,2h10v46h-10V2zM3.1,58h53.7",stack:"M8.2,38.4l-8.4,4.1l30.6,15.3L60,42.5l-8.1-4.1l-21.5,11L8.2,38.4z M51.9,30l-8.1,4.2l-13.4,6.9l-13.9-6.9L8.2,30l-8.4,4.2l8.4,4.2l22.2,11l21.5-11l8.1-4.2L51.9,30z M51.9,21.7l-8.1,4.2L35.7,30l-5.3,2.8L24.9,30l-8.4-4.1l-8.3-4.2l-8.4,4.2L8.2,30l8.3,4.2l13.9,6.9l13.4-6.9l8.1-4.2l8.1-4.1L51.9,21.7zM30.4,2.2L-0.2,17.5l8.4,4.1l8.3,4.2l8.4,4.2l5.5,2.7l5.3-2.7l8.1-4.2l8.1-4.2l8.1-4.1L30.4,2.2z"},title:t.getLocaleModel().get(["toolbox","magicType","title"]),option:{},seriesIndex:{}}},e.prototype.onclick=function(t,e,n){var i=this.model,r=i.get(["seriesIndex",n]);if(jU[n]){var o,a={series:[]};B(XU,function(t){R(t,n)>=0&&B(t,function(t){i.setIconStatus(t,"normal")})}),i.setIconStatus(n,"emphasis"),t.eachComponent({mainType:"series",query:null==r?null:{seriesIndex:r}},function(t){var e=t.subType,r=t.id,o=jU[n](e,r,t,i);o&&(P(o,t.option),a.series.push(o));var s=t.coordinateSystem;if(s&&"cartesian2d"===s.type&&("line"===n||"bar"===n)){var l=s.getAxesByScale("ordinal")[0];if(l){var u=l.dim+"Axis",c=t.getReferringComponents(u,Ea).models[0].componentIndex;a[u]=a[u]||[];for(var h=0;h<=c;h++)a[u][c]=a[u][c]||{};a[u][c].boundaryGap="bar"===n}}});var s=n;"stack"===n&&(o=D({stack:i.option.title.tiled,tiled:i.option.title.stack},i.option.title),"emphasis"!==i.get(["iconStatus",n])&&(s="tiled")),e.dispatchAction({type:"changeMagicType",currentType:s,newOption:a,newTitle:o,featureName:"magicType"})}},e}(EU),jU={line:function(t,e,n,i){if("bar"===t)return D({id:e,type:"line",data:n.get("data"),stack:n.get("stack"),markPoint:n.get("markPoint"),markLine:n.get("markLine")},i.get(["option","line"])||{},!0)},bar:function(t,e,n,i){if("line"===t)return D({id:e,type:"bar",data:n.get("data"),stack:n.get("stack"),markPoint:n.get("markPoint"),markLine:n.get("markLine")},i.get(["option","bar"])||{},!0)},stack:function(t,e,n,i){var r=n.get("stack")===YU;if("line"===t||"bar"===t)return i.setIconStatus("stack",r?"normal":"emphasis"),D({id:e,stack:r?"":YU},i.get(["option","stack"])||{},!0)}};ib({type:"changeMagicType",event:"magicTypeChanged",update:"prepareAndUpdate"},function(t,e){e.mergeOption(t.newOption)});var qU=new Array(60).join("-"),KU="\t";function $U(t){return t.replace(/^\s\s*/,"").replace(/\s\s*$/,"")}var JU=new RegExp("[\t]+","g");function QU(t,e){var n=t.split(new RegExp("\n*"+qU+"\n*","g")),i={series:[]};return B(n,function(t,n){if(function(t){if(t.slice(0,t.indexOf("\n")).indexOf(KU)>=0)return!0}(t)){var r=function(t){for(var e=t.split(/\n+/g),n=[],i=V($U(e.shift()).split(JU),function(t){return{name:t,data:[]}}),r=0;r=0)&&t(r,i._targetInfoList)})}return t.prototype.setOutputRanges=function(t,e){return this.matchOutputRanges(t,e,function(t,e,n){if((t.coordRanges||(t.coordRanges=[])).push(e),!t.coordRange){t.coordRange=e;var i=pY[t.brushType](0,n,e);t.__rangeOffset={offset:gY[t.brushType](i.values,t.range,[1,1]),xyMinMax:i.xyMinMax}}}),t},t.prototype.matchOutputRanges=function(t,e,n){B(t,function(t){var i=this.findTargetInfo(t,e);i&&!0!==i&&B(i.coordSyses,function(i){var r=pY[t.brushType](1,i,t.range,!0);n(t,r.values,i,e)})},this)},t.prototype.setInputRanges=function(t,e){B(t,function(t){var n,i,r,o,a,s=this.findTargetInfo(t,e);if(t.range=t.range||[],s&&!0!==s){t.panelId=s.panelId;var l=pY[t.brushType](0,s.coordSys,t.coordRange),u=t.__rangeOffset;t.range=u?gY[t.brushType](l.values,u.offset,(n=l.xyMinMax,i=u.xyMinMax,r=yY(n),o=yY(i),a=[r[0]/o[0],r[1]/o[1]],isNaN(a[0])&&(a[0]=1),isNaN(a[1])&&(a[1]=1),a)):l.values}},this)},t.prototype.makePanelOpts=function(t,e){return V(this._targetInfoList,function(n){var i=n.getPanelRect();return{panelId:n.panelId,defaultBrushType:e?e(n):null,clipPath:Kz(i),isTargetByCursor:Jz(i,t,n.coordSysModel),getLinearBrushOtherExtent:$z(i)}})},t.prototype.controlSeries=function(t,e,n){var i=this.findTargetInfo(t,n);return!0===i||i&&R(i.coordSyses,e.coordinateSystem)>=0},t.prototype.findTargetInfo=function(t,e){for(var n=this._targetInfoList,i=uY(e,t),r=0;rt[1]&&t.reverse(),t}function uY(t,e){return Ra(t,e,{includeMainTypes:aY})}var cY={grid:function(t,e){var n=t.xAxisModels,i=t.yAxisModels,r=t.gridModels,o=mt(),a={},s={};(n||i||r)&&(B(n,function(t){var e=t.axis.grid.model;o.set(e.id,e),a[e.id]=!0}),B(i,function(t){var e=t.axis.grid.model;o.set(e.id,e),s[e.id]=!0}),B(r,function(t){o.set(t.id,t),a[t.id]=!0,s[t.id]=!0}),o.each(function(t){var r=t.coordinateSystem,o=[];B(r.getCartesians(),function(t,e){(R(n,t.getAxis("x").model)>=0||R(i,t.getAxis("y").model)>=0)&&o.push(t)}),e.push({panelId:"grid--"+t.id,gridModel:t,coordSysModel:t,coordSys:o[0],coordSyses:o,getPanelRect:dY.grid,xAxisDeclared:a[t.id],yAxisDeclared:s[t.id]})}))},geo:function(t,e){B(t.geoModels,function(t){var n=t.coordinateSystem;e.push({panelId:"geo--"+t.id,geoModel:t,coordSysModel:t,coordSys:n,coordSyses:[n],getPanelRect:dY.geo})})}},hY=[function(t,e){var n=t.xAxisModel,i=t.yAxisModel,r=t.gridModel;return!r&&n&&(r=n.axis.grid.model),!r&&i&&(r=i.axis.grid.model),r&&r===e.gridModel},function(t,e){var n=t.geoModel;return n&&n===e.geoModel}],dY={grid:function(){return this.coordSys.master.getRect().clone()},geo:function(){var t=this.coordSys.view,e=aP(null,t);return $e(e,e,rP(null,t)),e}},pY={lineX:Y(fY,0),lineY:Y(fY,1),rect:function(t,e,n,i){var r=t?e.pointToData([n[0][0],n[1][0]],i):e.dataToPoint([n[0][0],n[1][0]],i),o=t?e.pointToData([n[0][1],n[1][1]],i):e.dataToPoint([n[0][1],n[1][1]],i),a=[lY([r[0],o[0]]),lY([r[1],o[1]])];return{values:a,xyMinMax:a}},polygon:function(t,e,n,i){var r=[[1/0,-1/0],[1/0,-1/0]];return{values:V(n,function(n){var o=t?e.pointToData(n,i):e.dataToPoint(n,i);return r[0][0]=Math.min(r[0][0],o[0]),r[1][0]=Math.min(r[1][0],o[1]),r[0][1]=Math.max(r[0][1],o[0]),r[1][1]=Math.max(r[1][1],o[1]),o}),xyMinMax:r}}};function fY(t,e,n,i){var r=n.getAxis(["x","y"][t]),o=lY(V([0,1],function(t){return e?r.coordToData(r.toLocalCoord(i[t]),!0):r.toGlobalCoord(r.dataToCoord(i[t]))})),a=[];return a[t]=o,a[1-t]=[NaN,NaN],{values:o,xyMinMax:a}}var gY={lineX:Y(vY,0),lineY:Y(vY,1),rect:function(t,e,n){return[[t[0][0]-n[0]*e[0][0],t[0][1]-n[0]*e[0][1]],[t[1][0]-n[1]*e[1][0],t[1][1]-n[1]*e[1][1]]]},polygon:function(t,e,n){return V(t,function(t,i){return[t[0]-n[0]*e[i][0],t[1]-n[1]*e[i][1]]})}};function vY(t,e,n,i){return[e[0]-i[t]*n[0],e[1]-i[t]*n[1]]}function yY(t){return t?[t[0][1]-t[0][0],t[1][1]-t[1][0]]:[NaN,NaN]}var mY,xY,_Y=B,bY=xa+"toolbox-dataZoom_",wY={x:"width",y:"height"},SY=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.render=function(t,e,n,i){this._brushController||(this._brushController=new xz(n.getZr()),this._brushController.on("brush",U(this._onBrush,this)).mount()),function(t,e,n,i,r){var o=n._isZoomActive;i&&"takeGlobalCursor"===i.type&&(o="dataZoomSelect"===i.key&&i.dataZoomSelectActive);n._isZoomActive=o,t.setIconStatus("zoom",o?"emphasis":"normal");var a=new sY(IY(t),e,{include:["grid"]}),s=a.makePanelOpts(r,function(t){return t.xAxisDeclared&&!t.yAxisDeclared?"lineX":!t.xAxisDeclared&&t.yAxisDeclared?"lineY":"rect"});n._brushController.setPanels(s).enableBrush(!(!o||!s.length)&&{brushType:"auto",brushStyle:t.getModel("brushStyle").getItemStyle()})}(t,e,this,i,n),function(t,e){t.setIconStatus("back",function(t){return rY(t).length}(e)>1?"emphasis":"normal")}(t,e)},e.prototype.onclick=function(t,e,n){MY[n].call(this)},e.prototype.dispose=function(t,e){this._brushController&&this._brushController.dispose()},e.prototype._onBrush=function(t){var e=t.areas;if(t.isEnd&&e.length){var n={},i=this.ecModel;this._brushController.updateCovers([]),new sY(IY(this.model),i,{include:["grid"]}).matchOutputRanges(e,i,function(t,e,n){if("cartesian2d"===n.type){var i=n.master.getRect().clone(),o=t.brushType;"rect"===o?(r("x",n,i,e[0]),r("y",n,i,e[1])):r({lineX:"x",lineY:"y"}[o],n,i,e)}}),function(t,e){var n=rY(t);nY(e,function(e,i){for(var r=n.length-1;r>=0&&!n[r][i];r--);if(r<0){var o=t.queryComponents({mainType:"dataZoom",subType:"select",id:i})[0];if(o){var a=o.getPercentRange();n[0][i]={dataZoomId:i,start:a[0],end:a[1]}}}}),n.push(e)}(i,n),this._dispatchZoomAction(n)}function r(t,e,r,o){var a=e.getAxis(t),s=a.model,l=function(t,e,n){var i;return n.eachComponent({mainType:"dataZoom",subType:"select"},function(n){n.getAxisModel(t,e.componentIndex)&&(i=n)}),i}(t,s,i),u=l.findRepresentativeAxisProxy(s).getMinMaxSpan(),c=a.scale.getExtent();null==u.minValueSpan&&null==u.maxValueSpan||(o=ez(0,o.slice(),c,0,u.minValueSpan,u.maxValueSpan));var h=Xo(c,r[wY[t]],.5);l&&(n[l.id]={dataZoomId:l.id,startValue:isFinite(h)?Ho(o[0],h):o[0],endValue:isFinite(h)?Ho(o[1],h):o[1]})}},e.prototype._dispatchZoomAction=function(t){var e=[];_Y(t,function(t,n){e.push(C(t))}),e.length&&this.api.dispatchAction({type:"dataZoom",from:this.uid,batch:e})},e.getDefaultOption=function(t){return{show:!0,filterMode:"filter",icon:{zoom:"M0,13.5h26.9 M13.5,26.9V0 M32.1,13.5H58V58H13.5 V32.1",back:"M22,1.4L9.9,13.5l12.3,12.3 M10.3,13.5H54.9v44.6 H10.3v-26"},title:t.getLocaleModel().get(["toolbox","dataZoom","title"]),brushStyle:{borderWidth:0,color:ug.color.backgroundTint}}},e}(EU),MY={zoom:function(){var t=!this._isZoomActive;this.api.dispatchAction({type:"takeGlobalCursor",key:"dataZoomSelect",dataZoomSelectActive:t})},back:function(){this._dispatchZoomAction(function(t){var e=rY(t),n=e[e.length-1];e.length>1&&e.pop();var i={};return nY(n,function(t,n){for(var r=e.length-1;r>=0;r--)if(t=e[r][n]){i[n]=t;break}}),i}(this.ecModel))}};function IY(t){var e={xAxisIndex:t.get("xAxisIndex",!0),yAxisIndex:t.get("yAxisIndex",!0),xAxisId:t.get("xAxisId",!0),yAxisId:t.get("yAxisId",!0)};return null==e.xAxisIndex&&null==e.xAxisId&&(e.xAxisIndex="all"),null==e.yAxisIndex&&null==e.yAxisId&&(e.yAxisIndex="all"),e}mY="dataZoom",xY=function(t){var e=t.getComponent("toolbox",0),n=["feature","dataZoom"];if(e&&null!=e.get(n)){var i=e.getModel(n),r=[],o=Ra(t,IY(i));return _Y(o.xAxisModels,function(t){return a(t,"xAxis","xAxisIndex")}),_Y(o.yAxisModels,function(t){return a(t,"yAxis","yAxisIndex")}),r}function a(t,e,n){var o=t.componentIndex,a={type:"select",$fromToolbox:!0,filterMode:i.get("filterMode",!0)||"filter",id:bY+e+o};a[n]=o,r.push(a)}},ct(null==Cg.get(mY)&&xY),Cg.set(mY,xY);var TY=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="tooltip",e.dependencies=["axisPointer"],e.defaultOption={z:60,show:!0,showContent:!0,trigger:"item",triggerOn:"mousemove|click|mousewheel",alwaysShowContent:!1,renderMode:"auto",confine:null,showDelay:0,hideDelay:100,transitionDuration:.4,displayTransition:!0,enterable:!1,backgroundColor:ug.color.neutral00,shadowBlur:10,shadowColor:"rgba(0, 0, 0, .2)",shadowOffsetX:1,shadowOffsetY:2,borderRadius:4,borderWidth:1,defaultBorderColor:ug.color.border,padding:null,extraCssText:"",axisPointer:{type:"line",axis:"auto",animation:"auto",animationDurationUpdate:200,animationEasingUpdate:"exponentialOut",crossStyle:{color:ug.color.borderShade,width:1,type:"dashed",textStyle:{}}},textStyle:{color:ug.color.tertiary,fontSize:14}},e}(lg);function CY(t){var e=t.get("confine");return null!=e?!!e:"richText"===t.get("renderMode")}function DY(t){if(r.domSupported)for(var e=document.documentElement.style,n=0,i=t.length;n0&&o.push(function(t,e,n){var i="cubic-bezier(0.23,1,0.32,1)",o="",a="";return n&&(a="opacity"+(o=" "+t/2+"s "+i)+",visibility"+o),e||(o=" "+t+"s "+i,a+=(a.length?",":"")+(r.transformSupported?""+PY+o:",left"+o+",top"+o)),LY+":"+a}(a,n,i)),s&&o.push("background-color:"+s),B(["width","color","radius"],function(e){var n="border-"+e,i=Cf(n),r=t.get(i);null!=r&&o.push(n+":"+r+("color"===e?"":"px"))}),o.push(function(t){var e=[],n=t.get("fontSize"),i=t.getTextColor();i&&e.push("color:"+i),e.push("font:"+t.getFont());var r=at(t.get("lineHeight"),Math.round(3*n/2));n&&e.push("line-height:"+r+"px");var o=t.get("textShadowColor"),a=t.get("textShadowBlur")||0,s=t.get("textShadowOffsetX")||0,l=t.get("textShadowOffsetY")||0;return o&&a&&e.push("text-shadow:"+s+"px "+l+"px "+a+"px "+o),B(["decoration","align"],function(n){var i=t.get(n);i&&e.push("text-"+n+":"+i)}),e.join(";")}(d)),null!=p&&o.push("padding:"+Df(p).join("px ")+"px"),o.join(";")+";"}function EY(t,e,n,i,r){var o=e&&e.painter;if(n){var a=o&&o.getViewportRoot();a&&function(t,e,n,i,r){ie(ne,e,i,r,!0)&&ie(t,n,ne[0],ne[1])}(t,a,n,i,r)}else{t[0]=i,t[1]=r;var s=o&&o.getViewportRootOffset();s&&(t[0]+=s.offsetLeft,t[1]+=s.offsetTop)}t[2]=t[0]/e.getWidth(),t[3]=t[1]/e.getHeight()}var zY=function(){function t(t,e){if(this._show=!1,this._styleCoord=[0,0,0,0],this._enterable=!0,this._alwaysShowContent=!1,this._firstShow=!0,this._longHide=!0,r.wxa)return null;var n=document.createElement("div");n.domBelongToZr=!0,this.el=n;var i=this._zr=t.getZr(),o=e.appendTo,a=o&&(j(o)?document.querySelector(o):tt(o)?o:Z(o)&&o(t.getDom()));EY(this._styleCoord,i,a,t.getWidth()/2,t.getHeight()/2),(a||t.getDom()).appendChild(n),this._api=t,this._container=a;var s=this;n.onmouseenter=function(){s._enterable&&(clearTimeout(s._hideTimeout),s._show=!0),s._inContent=!0},n.onmousemove=function(t){if(t=t||window.event,!s._enterable){var e=i.handler;fe(i.painter.getViewportRoot(),t,!0),e.dispatch("mousemove",t)}},n.onmouseleave=function(){s._inContent=!1,s._enterable&&s._show&&s.hideLater(s._hideDelay)}}return t.prototype.update=function(t){if(!this._container){var e=this._api.getDom(),n=(o="position",(a=(r=e).currentStyle||document.defaultView&&document.defaultView.getComputedStyle(r))?o?a[o]:a:null),i=e.style;"absolute"!==i.position&&"absolute"!==n&&(i.position="relative")}var r,o,a,s=t.get("alwaysShowContent");s&&this._moveIfResized(),this._alwaysShowContent=s,this._enableDisplayTransition=t.get("displayTransition")&&t.get("transitionDuration")>0,this.el.className=t.get("className")||""},t.prototype.show=function(t,e){clearTimeout(this._hideTimeout),clearTimeout(this._longHideTimeout);var n=this.el,i=n.style,r=this._styleCoord;n.innerHTML?i.cssText=OY+NY(t,!this._firstShow,this._longHide,this._enableDisplayTransition)+RY(r[0],r[1],!0)+"border-color:"+Nf(e)+";"+(t.get("extraCssText")||"")+";pointer-events:"+(this._enterable?"auto":"none"):i.display="none",this._show=!0,this._firstShow=!1,this._longHide=!1},t.prototype.setContent=function(t,e,n,i,r){var o=this.el;if(null!=t){var a="";if(j(r)&&"item"===n.get("trigger")&&!CY(n)&&(a=function(t,e,n){if(!j(n)||"inside"===n)return"";var i=t.get("backgroundColor"),r=t.get("borderWidth");e=Nf(e);var o,a,s="left"===(o=n)?"right":"right"===o?"left":"top"===o?"bottom":"top",l=Math.max(1.5*Math.round(r),6),u="",c=PY+":";R(["left","right"],s)>-1?(u+="top:50%",c+="translateY(-50%) rotate("+(a="left"===s?-225:-45)+"deg)"):(u+="left:50%",c+="translateX(-50%) rotate("+(a="top"===s?225:45)+"deg)");var h=a*Math.PI/180,d=l+r,p=d*Math.abs(Math.cos(h))+d*Math.abs(Math.sin(h)),f=e+" solid "+r+"px;";return'
'}(n,i,r)),j(t))o.innerHTML=t+a;else if(t){o.innerHTML="",X(t)||(t=[t]);for(var s=0;s=0?this._tryShow(n,i):"leave"===e&&this._hide(i))},this))},e.prototype._keepShow=function(){var t=this._tooltipModel,e=this._ecModel,n=this._api,i=t.get("triggerOn");if("axis"!==t.get("trigger")&&(this._lastDataByCoordSys=null,this._cbParamsList=null),null!=this._lastX&&null!=this._lastY&&"none"!==i&&"click"!==i){var r=this;clearTimeout(this._refreshUpdateTimeout),this._refreshUpdateTimeout=setTimeout(function(){!n.isDisposed()&&r.manuallyShowTip(t,e,n,{x:r._lastX,y:r._lastY,dataByCoordSys:r._lastDataByCoordSys})})}},e.prototype.manuallyShowTip=function(t,e,n,i){if(i.from!==this.uid&&!r.node&&n.getDom()){var o=YY(i,n);this._ticket="";var a=i.dataByCoordSys,s=function(t,e,n){var i=Na(t).queryOptionMap,r=i.keys()[0];if(!r||"series"===r)return;var o=Ba(e,r,i.get(r),{useDefault:!1,enableAll:!1,enableNone:!1}),a=o.models[0];if(!a)return;var s,l=n.getViewOfComponentModel(a);if(l.group.traverse(function(e){var n=Iu(e).tooltipConfig;if(n&&n.name===t.name)return s=e,!0}),s)return{componentMainType:r,componentIndex:a.componentIndex,el:s}}(i,e,n);if(s){var l=s.el.getBoundingRect().clone();l.applyTransform(s.el.transform),this._tryShow({offsetX:l.x+l.width/2,offsetY:l.y+l.height/2,target:s.el,position:i.position,positionDefault:"bottom"},o)}else if(i.tooltip&&null!=i.x&&null!=i.y){var u=HY;u.x=i.x,u.y=i.y,u.update(),Iu(u).tooltipConfig={name:null,option:i.tooltip},this._tryShow({offsetX:i.x,offsetY:i.y,target:u},o)}else if(a)this._tryShow({offsetX:i.x,offsetY:i.y,position:i.position,dataByCoordSys:a,tooltipOption:i.tooltipOption},o);else if(null!=i.seriesIndex){if(this._manuallyAxisShowTip(t,e,n,i))return;var c=jF(i,e),h=c.point[0],d=c.point[1];null!=h&&null!=d&&this._tryShow({offsetX:h,offsetY:d,target:c.el,position:i.position,positionDefault:"bottom"},o)}else null!=i.x&&null!=i.y&&(n.dispatchAction({type:"updateAxisPointer",x:i.x,y:i.y}),this._tryShow({offsetX:i.x,offsetY:i.y,position:i.position,target:n.getZr().findHover(i.x,i.y).target},o))}},e.prototype.manuallyHideTip=function(t,e,n,i){var r=this._tooltipContent;this._tooltipModel&&r.hideLater(this._tooltipModel.get("hideDelay")),this._lastX=this._lastY=this._lastDataByCoordSys=null,this._cbParamsList=null,i.from!==this.uid&&this._hide(YY(i,n))},e.prototype._manuallyAxisShowTip=function(t,e,n,i){var r=i.seriesIndex,o=i.dataIndex,a=e.getComponent("axisPointer").coordSysAxesInfo;if(null!=r&&null!=o&&null!=a){var s=e.getSeriesByIndex(r);if(s)if("axis"===UY([s.getData().getItemModel(o),s,(s.coordinateSystem||{}).model],this._tooltipModel).get("trigger"))return n.dispatchAction({type:"updateAxisPointer",seriesIndex:r,dataIndex:o,position:i.position}),!0}},e.prototype._tryShow=function(t,e){var n=t.target;if(this._tooltipModel){this._lastX=t.offsetX,this._lastY=t.offsetY;var i=t.dataByCoordSys;if(i&&i.length)this._showAxisTooltip(i,t);else if(n){var r,o;if("legend"===Iu(n).ssrType)return;this._lastDataByCoordSys=null,this._cbParamsList=null,Qm(n,function(t){if(t.tooltipDisabled)return r=o=null,!0;r||o||(null!=Iu(t).dataIndex?r=t:null!=Iu(t).tooltipConfig&&(o=t))},!0),r?this._showSeriesItemTooltip(t,r,e):o?this._showComponentItemTooltip(t,o,e):this._hide(e)}else this._lastDataByCoordSys=null,this._cbParamsList=null,this._hide(e)}},e.prototype._showOrMove=function(t,e){var n=t.get("showDelay");e=U(e,this),clearTimeout(this._showTimout),n>0?this._showTimout=setTimeout(e,n):e()},e.prototype._showAxisTooltip=function(t,e){var n=this._ecModel,i=this._tooltipModel,r=[e.offsetX,e.offsetY],o=UY([e.tooltipOption],i),a=this._renderMode,s=[],l=Ty("section",{blocks:[],noHeader:!0}),u=[],c=new zy;B(t,function(t){B(t.dataByAxis,function(t){var e=n.getComponent(t.axisDim+"Axis",t.axisIndex),r=t.value,o=e.axis,h=o.scale.parse(r);if(e&&null!=r){var d=DF(r,o,n,t.seriesDataIndices,t.valueLabelOpt),p=Ty("section",{header:d,noHeader:!ht(d),sortBlocks:!0,blocks:[]});l.blocks.push(p),B(t.seriesDataIndices,function(r){var o=n.getSeriesByIndex(r.seriesIndex),l=r.dataIndexInside,f=o.getDataParams(l);if(!(f.dataIndex<0)){f.axisDim=t.axisDim,f.axisIndex=t.axisIndex,f.axisType=t.axisType,f.axisId=t.axisId,f.axisValue=jw(e.axis,{value:h}),f.axisValueLabel=d,f.marker=c.makeTooltipMarker("item",Nf(f.color),a);var g=Gv(o.formatTooltip(l,!0,null)),v=g.frag;if(v){var y=UY([o],i).get("valueFormatter");p.blocks.push(y?k({valueFormatter:y},v):v)}g.text&&u.push(g.text),s.push(f)}})}})}),l.blocks.reverse(),u.reverse();var h=e.position,d=o.get("order"),p=Py(l,c,a,d,n.get("useUTC"),o.get("textStyle"));p&&u.unshift(p);var f="richText"===a?"\n\n":"
",g=u.join(f);this._showOrMove(o,function(){this._updateContentNotChangedOnAxis(t,s)?this._updatePosition(o,h,r[0],r[1],this._tooltipContent,s):this._showTooltipContent(o,g,s,Math.random()+"",r[0],r[1],h,null,c)})},e.prototype._showSeriesItemTooltip=function(t,e,n){var i=this._ecModel,r=Iu(e),o=r.seriesIndex,a=i.getSeriesByIndex(o),s=r.dataModel||a,l=r.dataIndex,u=r.dataType,c=s.getData(u),h=this._renderMode,d=t.positionDefault,p=UY([c.getItemModel(l),s,a&&(a.coordinateSystem||{}).model],this._tooltipModel,d?{position:d}:null),f=p.get("trigger");if(null==f||"item"===f){var g=s.getDataParams(l,u),v=new zy;g.marker=v.makeTooltipMarker("item",Nf(g.color),h);var y=Gv(s.formatTooltip(l,!1,u)),m=p.get("order"),x=p.get("valueFormatter"),_=y.frag,b=_?Py(x?k({valueFormatter:x},_):_,v,h,m,i.get("useUTC"),p.get("textStyle")):y.text,w="item_"+s.name+"_"+l;this._showOrMove(p,function(){this._showTooltipContent(p,b,g,w,t.offsetX,t.offsetY,t.position,t.target,v)}),n({type:"showTip",dataIndexInside:l,dataIndex:c.getRawIndex(l),seriesIndex:o,from:this.uid})}},e.prototype._showComponentItemTooltip=function(t,e,n){var i="html"===this._renderMode,r=Iu(e),o=r.tooltipConfig.option||{},a=o.encodeHTMLContent;if(j(o)){o={content:o,formatter:o},a=!0}a&&i&&o.content&&((o=C(o)).content=se(o.content));var s=[o],l=this._ecModel.getComponent(r.componentMainType,r.componentIndex);l&&s.push(l),s.push({formatter:o.content});var u=t.positionDefault,c=UY(s,this._tooltipModel,u?{position:u}:null),h=c.get("content"),d=Math.random()+"",p=new zy;this._showOrMove(c,function(){var n=C(c.get("formatterParams")||{});this._showTooltipContent(c,h,n,d,t.offsetX,t.offsetY,t.position,e,p)}),n({type:"showTip",from:this.uid})},e.prototype._showTooltipContent=function(t,e,n,i,r,o,a,s,l){if(this._ticket="",t.get("showContent")&&t.get("show")){var u=this._tooltipContent;u.setEnterable(t.get("enterable"));var c=t.get("formatter");a=a||t.get("position");var h=e,d=this._getNearestPoint([r,o],n,t.get("trigger"),t.get("borderColor"),t.get("defaultBorderColor",!0)).color;if(c)if(j(c)){var p=t.ecModel.get("useUTC"),f=X(n)?n[0]:n;h=c,f&&f.axisType&&f.axisType.indexOf("time")>=0&&(h=uf(f.axisValue,h,p)),h=Pf(h,n,!0)}else if(Z(c)){var g=U(function(e,i){e===this._ticket&&(u.setContent(i,l,t,d,a),this._updatePosition(t,a,r,o,u,n,s))},this);this._ticket=i,h=c(n,i,g)}else h=c;u.setContent(h,l,t,d,a),u.show(t,d),this._updatePosition(t,a,r,o,u,n,s)}},e.prototype._getNearestPoint=function(t,e,n,i,r){return"axis"===n||X(e)?{color:i||r}:X(e)?void 0:{color:i||e.color||e.borderColor}},e.prototype._updatePosition=function(t,e,n,i,r,o,a){var s=this._api.getWidth(),l=this._api.getHeight();e=e||t.get("position");var u=r.getSize(),c=t.get("align"),h=t.get("verticalAlign"),d=a&&a.getBoundingRect().clone();if(a&&d.applyTransform(a.transform),Z(e)&&(e=e([n,i],o,r.el,d,{viewSize:[s,l],contentSize:u.slice()})),X(e))n=Vo(e[0],s),i=Vo(e[1],l);else if($(e)){var p=e;p.width=u[0],p.height=u[1];var f=Jf(p,{width:s,height:l});n=f.x,i=f.y,c=null,h=null}else if(j(e)&&a){var g=function(t,e,n,i){var r=n[0],o=n[1],a=Math.ceil(Math.SQRT2*i)+8,s=0,l=0,u=e.width,c=e.height;switch(t){case"inside":s=e.x+u/2-r/2,l=e.y+c/2-o/2;break;case"top":s=e.x+u/2-r/2,l=e.y-o-a;break;case"bottom":s=e.x+u/2-r/2,l=e.y+c+a;break;case"left":s=e.x-r-a,l=e.y+c/2-o/2;break;case"right":s=e.x+u+a,l=e.y+c/2-o/2}return[s,l]}(e,d,u,t.get("borderWidth"));n=g[0],i=g[1]}else{g=function(t,e,n,i,r,o,a){var s=n.getSize(),l=s[0],u=s[1];null!=o&&(t+l+o+2>i?t-=l+o:t+=o);null!=a&&(e+u+a>r?e-=u+a:e+=a);return[t,e]}(n,i,r,s,l,c?null:20,h?null:20);n=g[0],i=g[1]}if(c&&(n-=XY(c)?u[0]/2:"right"===c?u[0]:0),h&&(i-=XY(h)?u[1]/2:"bottom"===h?u[1]:0),CY(t)){g=function(t,e,n,i,r){var o=n.getSize(),a=o[0],s=o[1];return t=Math.min(t+a,i)-a,e=Math.min(e+s,r)-s,t=Math.max(t,0),e=Math.max(e,0),[t,e]}(n,i,r,s,l);n=g[0],i=g[1]}r.moveTo(n,i)},e.prototype._updateContentNotChangedOnAxis=function(t,e){var n=this._lastDataByCoordSys,i=this._cbParamsList,r=!!n&&n.length===t.length;return r&&B(n,function(n,o){var a=n.dataByAxis||[],s=(t[o]||{}).dataByAxis||[];(r=r&&a.length===s.length)&&B(a,function(t,n){var o=s[n]||{},a=t.seriesDataIndices||[],l=o.seriesDataIndices||[];(r=r&&t.value===o.value&&t.axisType===o.axisType&&t.axisId===o.axisId&&a.length===l.length)&&B(a,function(t,e){var n=l[e];r=r&&t.seriesIndex===n.seriesIndex&&t.dataIndex===n.dataIndex}),i&&B(t.seriesDataIndices,function(t){var n=t.seriesIndex,o=e[n],a=i[n];o&&a&&a.data!==o.data&&(r=!1)})})}),this._lastDataByCoordSys=t,this._cbParamsList=e,!!r},e.prototype._hide=function(t){this._lastDataByCoordSys=null,this._cbParamsList=null,t({type:"hideTip",from:this.uid})},e.prototype.dispose=function(t,e){!r.node&&e.getDom()&&(hm(this,"_updatePosition"),this._tooltipContent.dispose(),XF("itemTooltip",e),this._tooltipContent=null,this._tooltipModel=null,this._lastDataByCoordSys=null,this._cbParamsList=null)},e.type="tooltip",e}(Ky);function UY(t,e,n){var i,r=e.ecModel;n?(i=new kp(n,r,r),i=new kp(e.option,i,r)):i=e;for(var o=t.length-1;o>=0;o--){var a=t[o];a&&(a instanceof kp&&(a=a.get("tooltip",!0)),j(a)&&(a={formatter:a}),a&&(i=new kp(a,i,r)))}return i}function YY(t,e){return t.dispatchAction||U(e.dispatchAction,e)}function XY(t){return"center"===t||"middle"===t}var ZY=["rect","polygon","keep","clear"];function jY(t,e){var n=_a(t?t.brush:[]);if(n.length){var i=[];B(n,function(t){var e=t.hasOwnProperty("toolbox")?t.toolbox:[];e instanceof Array&&(i=i.concat(e))});var r=t&&t.toolbox;X(r)&&(r=r[0]),r||(r={feature:{}},t.toolbox=[r]);var o=r.feature||(r.feature={}),a=o.brush||(o.brush={}),s=a.type||(a.type=[]);s.push.apply(s,i),ts(s,function(t){return t+""},null),e&&!s.length&&s.push.apply(s,ZY)}}var qY=B;function KY(t){if(t)for(var e in t)if(t.hasOwnProperty(e))return!0}function $Y(t,e,n){var i={};return qY(e,function(e){var r,o=i[e]=((r=function(){}).prototype.__hidden=r.prototype,new r);qY(t[e],function(t,i){if(ZR.isValidType(i)){var r={type:i,visual:t};n&&n(r,e),o[i]=new ZR(r),"opacity"===i&&((r=C(r)).type="colorAlpha",o.__hidden.__alphaForOpacity=new ZR(r))}})}),i}function JY(t,e,n){var i;B(n,function(t){e.hasOwnProperty(t)&&KY(e[t])&&(i=!0)}),i&&B(n,function(n){e.hasOwnProperty(n)&&KY(e[n])?t[n]=C(e[n]):delete t[n]})}var QY={lineX:tX(0),lineY:tX(1),rect:{point:function(t,e,n){return t&&n.boundingRect.contain(t[0],t[1])},rect:function(t,e,n){return t&&n.boundingRect.intersect(t)}},polygon:{point:function(t,e,n){return t&&n.boundingRect.contain(t[0],t[1])&&FS(n.range,t[0],t[1])},rect:function(t,e,n){var i=n.range;if(!t||i.length<=1)return!1;var r=t.x,o=t.y,a=t.width,s=t.height,l=i[0];return!!(FS(i,r,o)||FS(i,r+a,o)||FS(i,r,o+s)||FS(i,r+a,o+s)||Xe.create(t).contain(l[0],l[1])||Rd(r,o,r+a,o,i)||Rd(r,o,r,o+s,i)||Rd(r+a,o,r+a,o+s,i)||Rd(r,o+s,r+a,o+s,i))||void 0}}};function tX(t){var e=["x","y"],n=["width","height"];return{point:function(e,n,i){if(e){var r=i.range;return eX(e[t],r)}},rect:function(i,r,o){if(i){var a=o.range,s=[i[e[t]],i[e[t]]+i[n[t]]];return s[1]e[0][1]&&(e[0][1]=o[0]),o[1]e[1][1]&&(e[1][1]=o[1])}return e&&cX(e)}};function cX(t){return new Xe(t[0][0],t[1][0],t[0][1]-t[0][0],t[1][1]-t[1][0])}var hX=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(t,e){this.ecModel=t,this.api=e,this.model,(this._brushController=new xz(e.getZr())).on("brush",U(this._onBrush,this)).mount()},e.prototype.render=function(t,e,n,i){this.model=t,this._updateController(t,e,n,i)},e.prototype.updateTransform=function(t,e,n,i){oX(e),this._updateController(t,e,n,i)},e.prototype.updateVisual=function(t,e,n,i){this.updateTransform(t,e,n,i)},e.prototype.updateView=function(t,e,n,i){this._updateController(t,e,n,i)},e.prototype._updateController=function(t,e,n,i){(!i||i.$from!==t.id)&&this._brushController.setPanels(t.brushTargetManager.makePanelOpts(n)).enableBrush(t.brushOption).updateCovers(t.areas.slice())},e.prototype.dispose=function(){this._brushController.dispose()},e.prototype._onBrush=function(t){var e=this.model.id,n=this.model.brushTargetManager.setOutputRanges(t.areas,this.ecModel);(!t.isEnd||t.removeOnClick)&&this.api.dispatchAction({type:"brush",brushId:e,areas:C(n),$from:e}),t.isEnd&&this.api.dispatchAction({type:"brushEnd",brushId:e,areas:C(n),$from:e})},e.type="brush",e}(Ky),dX=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.areas=[],n.brushOption={},n}return n(e,t),e.prototype.optionUpdated=function(t,e){var n=this.option;!e&&JY(n,t,["inBrush","outOfBrush"]);var i=n.inBrush=n.inBrush||{};n.outOfBrush=n.outOfBrush||{color:this.option.defaultOutOfBrushColor},i.hasOwnProperty("liftZ")||(i.liftZ=5)},e.prototype.setAreas=function(t){t&&(this.areas=V(t,function(t){return pX(this.option,t)},this))},e.prototype.setBrushOption=function(t){this.brushOption=pX(this.option,t),this.brushType=this.brushOption.brushType},e.type="brush",e.dependencies=["geo","grid","xAxis","yAxis","parallel","series"],e.defaultOption={seriesIndex:"all",brushType:"rect",brushMode:"single",transformable:!0,brushStyle:{borderWidth:1,color:ug.color.backgroundTint,borderColor:ug.color.borderTint},throttleType:"fixRate",throttleDelay:0,removeOnClick:!0,z:1e4,defaultOutOfBrushColor:ug.color.disabled},e}(lg);function pX(t,e){return D({brushType:t.brushType,brushMode:t.brushMode,transformable:t.transformable,brushStyle:new kp(t.brushStyle).getItemStyle(),removeOnClick:t.removeOnClick,z:t.z},e,!0)}var fX=["rect","polygon","lineX","lineY","keep","clear"],gX=function(t){function e(){return null!==t&&t.apply(this,arguments)||this}return n(e,t),e.prototype.render=function(t,e,n){var i,r,o;e.eachComponent({mainType:"brush"},function(t){i=t.brushType,r=t.brushOption.brushMode||"single",o=o||!!t.areas.length}),this._brushType=i,this._brushMode=r,B(t.get("type",!0),function(e){t.setIconStatus(e,("keep"===e?"multiple"===r:"clear"===e?o:e===i)?"emphasis":"normal")})},e.prototype.updateView=function(t,e,n){this.render(t,e,n)},e.prototype.getIcons=function(){var t=this.model,e=t.get("icon",!0),n={};return B(t.get("type",!0),function(t){e[t]&&(n[t]=e[t])}),n},e.prototype.onclick=function(t,e,n){var i=this._brushType,r=this._brushMode;"clear"===n?(e.dispatchAction({type:"axisAreaSelect",intervals:[]}),e.dispatchAction({type:"brush",command:"clear",areas:[]})):e.dispatchAction({type:"takeGlobalCursor",key:"brush",brushOption:{brushType:"keep"===n?i:i!==n&&n,brushMode:"keep"===n?"multiple"===r?"single":"multiple":r}})},e.getDefaultOption=function(t){return{show:!0,type:fX.slice(),icon:{rect:"M7.3,34.7 M0.4,10V-0.2h9.8 M89.6,10V-0.2h-9.8 M0.4,60v10.2h9.8 M89.6,60v10.2h-9.8 M12.3,22.4V10.5h13.1 M33.6,10.5h7.8 M49.1,10.5h7.8 M77.5,22.4V10.5h-13 M12.3,31.1v8.2 M77.7,31.1v8.2 M12.3,47.6v11.9h13.1 M33.6,59.5h7.6 M49.1,59.5 h7.7 M77.5,47.6v11.9h-13",polygon:"M55.2,34.9c1.7,0,3.1,1.4,3.1,3.1s-1.4,3.1-3.1,3.1 s-3.1-1.4-3.1-3.1S53.5,34.9,55.2,34.9z M50.4,51c1.7,0,3.1,1.4,3.1,3.1c0,1.7-1.4,3.1-3.1,3.1c-1.7,0-3.1-1.4-3.1-3.1 C47.3,52.4,48.7,51,50.4,51z M55.6,37.1l1.5-7.8 M60.1,13.5l1.6-8.7l-7.8,4 M59,19l-1,5.3 M24,16.1l6.4,4.9l6.4-3.3 M48.5,11.6 l-5.9,3.1 M19.1,12.8L9.7,5.1l1.1,7.7 M13.4,29.8l1,7.3l6.6,1.6 M11.6,18.4l1,6.1 M32.8,41.9 M26.6,40.4 M27.3,40.2l6.1,1.6 M49.9,52.1l-5.6-7.6l-4.9-1.2",lineX:"M15.2,30 M19.7,15.6V1.9H29 M34.8,1.9H40.4 M55.3,15.6V1.9H45.9 M19.7,44.4V58.1H29 M34.8,58.1H40.4 M55.3,44.4 V58.1H45.9 M12.5,20.3l-9.4,9.6l9.6,9.8 M3.1,29.9h16.5 M62.5,20.3l9.4,9.6L62.3,39.7 M71.9,29.9H55.4",lineY:"M38.8,7.7 M52.7,12h13.2v9 M65.9,26.6V32 M52.7,46.3h13.2v-9 M24.9,12H11.8v9 M11.8,26.6V32 M24.9,46.3H11.8v-9 M48.2,5.1l-9.3-9l-9.4,9.2 M38.9-3.9V12 M48.2,53.3l-9.3,9l-9.4-9.2 M38.9,62.3V46.4",keep:"M4,10.5V1h10.3 M20.7,1h6.1 M33,1h6.1 M55.4,10.5V1H45.2 M4,17.3v6.6 M55.6,17.3v6.6 M4,30.5V40h10.3 M20.7,40 h6.1 M33,40h6.1 M55.4,30.5V40H45.2 M21,18.9h62.9v48.6H21V18.9z",clear:"M22,14.7l30.9,31 M52.9,14.7L22,45.7 M4.7,16.8V4.2h13.1 M26,4.2h7.8 M41.6,4.2h7.8 M70.3,16.8V4.2H57.2 M4.7,25.9v8.6 M70.3,25.9v8.6 M4.7,43.2v12.6h13.1 M26,55.8h7.8 M41.6,55.8h7.8 M70.3,43.2v12.6H57.2"},title:t.getLocaleModel().get(["toolbox","brush","title"])}},e}(EU);var vX=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.layoutMode={type:"box",ignoreSize:!0},n}return n(e,t),e.type="title",e.defaultOption={z:6,show:!0,text:"",target:"blank",subtext:"",subtarget:"blank",left:"center",top:ug.size.m,backgroundColor:ug.color.transparent,borderColor:ug.color.primary,borderWidth:0,padding:5,itemGap:10,textStyle:{fontSize:18,fontWeight:"bold",color:ug.color.primary},subtextStyle:{fontSize:12,color:ug.color.quaternary}},e}(lg),yX=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){if(this.group.removeAll(),t.get("show")){var i=this.group,r=t.getModel("textStyle"),o=t.getModel("subtextStyle"),a=t.get("textAlign"),s=at(t.get("textBaseline"),t.get("textVerticalAlign")),l=new du({style:sp(r,{text:t.get("text"),fill:r.getTextColor()},{disableBox:!0}),z2:10}),u=l.getBoundingRect(),c=t.get("subtext"),h=new du({style:sp(o,{text:c,fill:o.getTextColor(),y:u.height+t.get("itemGap"),verticalAlign:"top"},{disableBox:!0}),z2:10}),d=t.get("link"),p=t.get("sublink"),f=t.get("triggerEvent",!0);l.silent=!d&&!f,h.silent=!p&&!f,d&&l.on("click",function(){Ef(d,"_"+t.get("target"))}),p&&h.on("click",function(){Ef(p,"_"+t.get("subtarget"))}),Iu(l).eventData=Iu(h).eventData=f?{componentType:"title",componentIndex:t.componentIndex}:null,i.add(l),c&&i.add(h);var g=i.getBoundingRect(),v=t.getBoxLayoutParams();v.width=g.width,v.height=g.height;var y=Jf(v,eg(t,n).refContainer,t.get("padding"));a||("middle"===(a=t.get("left")||t.get("right"))&&(a="center"),"right"===a?y.x+=y.width:"center"===a&&(y.x+=y.width/2)),s||("center"===(s=t.get("top")||t.get("bottom"))&&(s="middle"),"bottom"===s?y.y+=y.height:"middle"===s&&(y.y+=y.height/2),s=s||"top"),i.x=y.x,i.y=y.y,i.markRedraw();var m={align:a,verticalAlign:s};l.setStyle(m),h.setStyle(m),g=i.getBoundingRect();var x=y.margin,_=t.getItemStyle(["color","opacity"]);_.fill=t.get("backgroundColor");var b=new lu({shape:{x:g.x-x[3],y:g.y-x[0],width:g.width+x[1]+x[3],height:g.height+x[0]+x[2],r:t.get("borderRadius")},style:_,subPixelOptimize:!0,silent:!0});i.add(b)}},e.type="title",e}(Ky);var mX=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.layoutMode="box",n}return n(e,t),e.prototype.init=function(t,e,n){this.mergeDefaultAndTheme(t,n),this._initData()},e.prototype.mergeOption=function(e){t.prototype.mergeOption.apply(this,arguments),this._initData()},e.prototype.setCurrentIndex=function(t){null==t&&(t=this.option.currentIndex);var e=this._data.count();this.option.loop?t=(t%e+e)%e:(t>=e&&(t=e-1),t<0&&(t=0)),this.option.currentIndex=t},e.prototype.getCurrentIndex=function(){return this.option.currentIndex},e.prototype.isIndexMax=function(){return this.getCurrentIndex()>=this._data.count()-1},e.prototype.setPlayState=function(t){this.option.autoPlay=!!t},e.prototype.getPlayState=function(){return!!this.option.autoPlay},e.prototype._initData=function(){var t,e=this.option,n=e.data||[],i=e.axisType,r=this._names=[];"category"===i?(t=[],B(n,function(e,n){var i,o=Da(Sa(e),"");$(e)?(i=C(e)).value=n:i=n,t.push(i),r.push(o)})):t=n;var o={category:"ordinal",time:"time",value:"number"}[i]||"number";(this._data=new Hb([{name:"value",type:o}],this)).initData(t,r)},e.prototype.getData=function(){return this._data},e.prototype.getCategories=function(){if("category"===this.get("axisType"))return this._names.slice()},e.type="timeline",e.defaultOption={z:4,show:!0,axisType:"time",realtime:!0,left:"20%",top:null,right:"20%",bottom:0,width:null,height:40,padding:ug.size.m,controlPosition:"left",autoPlay:!1,rewind:!1,loop:!0,playInterval:2e3,currentIndex:0,itemStyle:{},label:{color:ug.color.secondary},data:[]},e}(lg),xX=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="timeline.slider",e.defaultOption=Op(mX.defaultOption,{backgroundColor:"rgba(0,0,0,0)",borderColor:ug.color.border,borderWidth:0,orient:"horizontal",inverse:!1,tooltip:{trigger:"item"},symbol:"circle",symbolSize:12,lineStyle:{show:!0,width:2,color:ug.color.accent10},label:{position:"auto",show:!0,interval:"auto",rotate:0,color:ug.color.tertiary},itemStyle:{color:ug.color.accent20,borderWidth:0},checkpointStyle:{symbol:"circle",symbolSize:15,color:ug.color.accent50,borderColor:ug.color.accent50,borderWidth:0,shadowBlur:0,shadowOffsetX:0,shadowOffsetY:0,shadowColor:"rgba(0, 0, 0, 0)",animation:!0,animationDuration:300,animationEasing:"quinticInOut"},controlStyle:{show:!0,showPlayBtn:!0,showPrevBtn:!0,showNextBtn:!0,itemSize:24,itemGap:12,position:"left",playIcon:"path://M15 0C23.2843 0 30 6.71573 30 15C30 23.2843 23.2843 30 15 30C6.71573 30 0 23.2843 0 15C0 6.71573 6.71573 0 15 0ZM15 3C8.37258 3 3 8.37258 3 15C3 21.6274 8.37258 27 15 27C21.6274 27 27 21.6274 27 15C27 8.37258 21.6274 3 15 3ZM11.5 10.6699C11.5 9.90014 12.3333 9.41887 13 9.80371L20.5 14.1338C21.1667 14.5187 21.1667 15.4813 20.5 15.8662L13 20.1963C12.3333 20.5811 11.5 20.0999 11.5 19.3301V10.6699Z",stopIcon:"path://M15 0C23.2843 0 30 6.71573 30 15C30 23.2843 23.2843 30 15 30C6.71573 30 0 23.2843 0 15C0 6.71573 6.71573 0 15 0ZM15 3C8.37258 3 3 8.37258 3 15C3 21.6274 8.37258 27 15 27C21.6274 27 27 21.6274 27 15C27 8.37258 21.6274 3 15 3ZM11.5 10C12.3284 10 13 10.6716 13 11.5V18.5C13 19.3284 12.3284 20 11.5 20C10.6716 20 10 19.3284 10 18.5V11.5C10 10.6716 10.6716 10 11.5 10ZM18.5 10C19.3284 10 20 10.6716 20 11.5V18.5C20 19.3284 19.3284 20 18.5 20C17.6716 20 17 19.3284 17 18.5V11.5C17 10.6716 17.6716 10 18.5 10Z",nextIcon:"path://M0.838834 18.7383C0.253048 18.1525 0.253048 17.2028 0.838834 16.617L7.55635 9.89949L0.838834 3.18198C0.253048 2.59619 0.253048 1.64645 0.838834 1.06066C1.42462 0.474874 2.37437 0.474874 2.96015 1.06066L10.7383 8.83883L10.8412 8.95277C11.2897 9.50267 11.2897 10.2963 10.8412 10.8462L10.7383 10.9602L2.96015 18.7383C2.37437 19.3241 1.42462 19.3241 0.838834 18.7383Z",prevIcon:"path://M10.9602 1.06066C11.5459 1.64645 11.5459 2.59619 10.9602 3.18198L4.24264 9.89949L10.9602 16.617C11.5459 17.2028 11.5459 18.1525 10.9602 18.7383C10.3744 19.3241 9.42462 19.3241 8.83883 18.7383L1.06066 10.9602L0.957771 10.8462C0.509245 10.2963 0.509245 9.50267 0.957771 8.95277L1.06066 8.83883L8.83883 1.06066C9.42462 0.474874 10.3744 0.474874 10.9602 1.06066Z",prevBtnSize:18,nextBtnSize:18,color:ug.color.accent50,borderColor:ug.color.accent50,borderWidth:0},emphasis:{label:{show:!0,color:ug.color.accent60},itemStyle:{color:ug.color.accent60,borderColor:ug.color.accent60},controlStyle:{color:ug.color.accent70,borderColor:ug.color.accent70}},progress:{lineStyle:{color:ug.color.accent30},itemStyle:{color:ug.color.accent40}},data:[]}),e}(mX);E(xX,Vv.prototype);var _X=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="timeline",e}(Ky),bX=function(t){function e(e,n,i,r){var o=t.call(this,e,n,i)||this;return o.type=r||"value",o}return n(e,t),e.prototype.getLabelModel=function(){return this.model.getModel("label")},e.prototype.isHorizontal=function(){return"horizontal"===this.model.get("orient")},e}(SM),wX=Math.PI,SX=Pa(),MX=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(t,e){this.api=e},e.prototype.render=function(t,e,n){if(this.model=t,this.api=n,this.ecModel=e,this.group.removeAll(),t.get("show",!0)){var i=this._layout(t,n),r=this._createGroup("_mainGroup"),o=this._createGroup("_labelGroup"),a=this._axis=this._createAxis(i,t);t.formatTooltip=function(t){return Ty("nameValue",{noName:!0,value:a.scale.getLabel({value:t})})},B(["AxisLine","AxisTick","Control","CurrentPointer"],function(e){this["_render"+e](i,r,a,t)},this),this._renderAxisLabel(i,o,a,t),this._position(i,t)}this._doPlayStop(),this._updateTicksStatus()},e.prototype.remove=function(){this._clearTimer(),this.group.removeAll()},e.prototype.dispose=function(){this._clearTimer()},e.prototype._layout=function(t,e){var n,i,r,o,a=t.get(["label","position"]),s=t.get("orient"),l=function(t,e){return Jf(t.getBoxLayoutParams(),eg(t,e).refContainer,t.get("padding"))}(t,e),u={horizontal:"center",vertical:(n=null==a||"auto"===a?"horizontal"===s?l.y+l.height/2=0||"+"===n?"left":"right"},c={horizontal:n>=0||"+"===n?"top":"bottom",vertical:"middle"},h={horizontal:0,vertical:wX/2},d="vertical"===s?l.height:l.width,p=t.getModel("controlStyle"),f=p.get("show",!0),g=f?p.get("itemSize"):0,v=f?p.get("itemGap"):0,y=g+v,m=t.get(["label","rotate"])||0;m=m*wX/180;var x=p.get("position",!0),_=f&&p.get("showPlayBtn",!0),b=f&&p.get("showPrevBtn",!0),w=f&&p.get("showNextBtn",!0),S=0,M=d;"left"===x||"bottom"===x?(_&&(i=[0,0],S+=y),b&&(r=[S,0],S+=y),w&&(o=[M-g,0],M-=y)):(_&&(i=[M-g,0],M-=y),b&&(r=[0,0],S+=y),w&&(o=[M-g,0],M-=y));var I=[S,M];return t.get("inverse")&&I.reverse(),{viewRect:l,mainLength:d,orient:s,rotation:h[s],labelRotation:m,labelPosOpt:n,labelAlign:t.get(["label","align"])||u[s],labelBaseline:t.get(["label","verticalAlign"])||t.get(["label","baseline"])||c[s],playPosition:i,prevBtnPosition:r,nextBtnPosition:o,axisExtent:I,controlSize:g,controlGap:v}},e.prototype._position=function(t,e){var n=this._mainGroup,i=this._labelGroup,r=t.viewRect;if("vertical"===t.orient){var o=[1,0,0,1,0,0],a=r.x,s=r.y+r.height;Te(o,o,[-a,-s]),Ce(o,o,-wX/2),Te(o,o,[a,s]),(r=r.clone()).applyTransform(o)}var l=v(r),u=v(n.getBoundingRect()),c=v(i.getBoundingRect()),h=[n.x,n.y],d=[i.x,i.y];d[0]=h[0]=l[0][0];var p,f=t.labelPosOpt;null==f||j(f)?(y(h,u,l,1,p="+"===f?0:1),y(d,c,l,1,1-p)):(y(h,u,l,1,p=f>=0?0:1),d[1]=h[1]+f);function g(t){t.originX=l[0][0]-t.x,t.originY=l[1][0]-t.y}function v(t){return[[t.x,t.x+t.width],[t.y,t.y+t.height]]}function y(t,e,n,i,r){t[i]+=n[i][r]-e[i][r]}n.setPosition(h),i.setPosition(d),n.rotation=i.rotation=t.rotation,g(n),g(i)},e.prototype._createAxis=function(t,e){var n=e.getData(),i=e.get("axisType")||e.get("type");"category"!==i&&"time"!==i&&(i="value");var r=Ww(e,i,!1);r.getTicks=function(){return n.mapArray(["value"],function(t){return{value:t}})};var o=n.getDataExtent("value");r.setExtent(o[0],o[1]),ES(r,{fixMinMax:[!0,!0]});var a=new bX("value",r,t.axisExtent,i);return a.model=e,a},e.prototype._createGroup=function(t){var e=this[t]=new vo;return this.group.add(e),e},e.prototype._renderAxisLine=function(t,e,n,i){var r=n.getExtent();if(i.get(["lineStyle","show"])){var o=new Oh({shape:{x1:r[0],y1:0,x2:r[1],y2:0},style:k({lineCap:"round"},i.getModel("lineStyle").getLineStyle()),silent:!0,z2:1});e.add(o);var a=this._progressLine=new Oh({shape:{x1:r[0],x2:this._currentPointer?this._currentPointer.x:r[0],y1:0,y2:0},style:P({lineCap:"round",lineWidth:o.style.lineWidth},i.getModel(["progress","lineStyle"]).getLineStyle()),silent:!0,z2:1});e.add(a)}},e.prototype._renderAxisTick=function(t,e,n,i){var r=this,o=i.getData(),a=n.scale.getTicks();this._tickSymbols=[],B(a,function(t){var a=n.dataToCoord(t.value),s=o.getItemModel(t.value),l=s.getModel("itemStyle"),u=s.getModel(["emphasis","itemStyle"]),c=s.getModel(["progress","itemStyle"]),h={x:a,y:0,onclick:U(r._changeTimeline,r,t.value)},d=IX(s,l,e,h);d.ensureState("emphasis").style=u.getItemStyle(),d.ensureState("progress").style=c.getItemStyle(),Ac(d);var p=Iu(d);s.get("tooltip")?(p.dataIndex=t.value,p.dataModel=i):p.dataIndex=p.dataModel=null,r._tickSymbols.push(d)})},e.prototype._renderAxisLabel=function(t,e,n,i){var r=this;if(n.getLabelModel().get("show")){var o=i.getData(),a=n.getViewLabels();this._tickLabels=[],B(a,function(i){if(!i.tick.offInterval){var a=i.tick.value,s=o.getItemModel(a),l=s.getModel("label"),u=s.getModel(["emphasis","label"]),c=s.getModel(["progress","label"]),h=n.dataToCoord(a),d=new du({x:h,y:0,rotation:t.labelRotation-t.rotation,onclick:U(r._changeTimeline,r,a),silent:!1,style:sp(l,{text:i.formattedLabel,align:t.labelAlign,verticalAlign:t.labelBaseline})});d.ensureState("emphasis").style=sp(u),d.ensureState("progress").style=sp(c),e.add(d),Ac(d),SX(d).dataIndex=a,r._tickLabels.push(d)}})}},e.prototype._renderControl=function(t,e,n,i){var r=t.controlSize,o=t.rotation,a=i.getModel("controlStyle").getItemStyle(),s=i.getModel(["emphasis","controlStyle"]).getItemStyle(),l=i.getPlayState(),u=i.get("inverse",!0);function c(t,n,l,u){if(t){var c=Qr(at(i.get(["controlStyle",n+"BtnSize"]),r),r),h=function(t,e,n,i){var r=i.style,o=Od(t.get(["controlStyle",e]),i||{},new Xe(n[0],n[1],n[2],n[3]));r&&o.setStyle(r);return o}(i,n+"Icon",[0,-c/2,c,c],{x:t[0],y:t[1],originX:r/2,originY:0,rotation:u?-o:0,rectHover:!0,style:a,onclick:l});h.ensureState("emphasis").style=s,e.add(h),Ac(h)}}c(t.nextBtnPosition,"next",U(this._changeTimeline,this,u?"-":"+")),c(t.prevBtnPosition,"prev",U(this._changeTimeline,this,u?"+":"-")),c(t.playPosition,l?"stop":"play",U(this._handlePlayClick,this,!l),!0)},e.prototype._renderCurrentPointer=function(t,e,n,i){var r=i.getData(),o=i.getCurrentIndex(),a=r.getItemModel(o).getModel("checkpointStyle"),s=this,l={onCreate:function(t){t.draggable=!0,t.drift=U(s._handlePointerDrag,s),t.ondragend=U(s._handlePointerDragend,s),TX(t,s._progressLine,o,n,i,!0)},onUpdate:function(t){TX(t,s._progressLine,o,n,i)}};this._currentPointer=IX(a,a,this._mainGroup,{},this._currentPointer,l)},e.prototype._handlePlayClick=function(t){this._clearTimer(),this.api.dispatchAction({type:"timelinePlayChange",playState:t,from:this.uid})},e.prototype._handlePointerDrag=function(t,e,n){this._clearTimer(),this._pointerChangeTimeline([n.offsetX,n.offsetY])},e.prototype._handlePointerDragend=function(t){this._pointerChangeTimeline([t.offsetX,t.offsetY],!0)},e.prototype._pointerChangeTimeline=function(t,e){var n=this._toAxisCoord(t)[0],i=Wo(this._axis.getExtent().slice());n>i[1]&&(n=i[1]),n=0&&(s[a]=+s[a].toFixed(d)),[s,h]}var zX={min:Y(EX,"min"),max:Y(EX,"max"),average:Y(EX,"average"),median:Y(EX,"median")};function BX(t,e){if(e){var n=t.getData(),i=t.coordinateSystem,r=i&&i.dimensions;if(!function(t){return!isNaN(parseFloat(t.x))&&!isNaN(parseFloat(t.y))}(e)&&!X(e.coord)&&X(r)){var o=VX(e,n,i,t);if((e=C(e)).type&&zX[e.type]&&o.baseAxis&&o.valueAxis){var a=R(r,o.baseAxis.dim),s=R(r,o.valueAxis.dim),l=zX[e.type](n,o.valueAxis.dim,o.baseDataDim,o.valueDataDim,a,s);e.coord=l[0],e.value=l[1]}else e.coord=[null!=e.xAxis?e.xAxis:e.radiusAxis,null!=e.yAxis?e.yAxis:e.angleAxis]}if(null!=e.coord&&X(r))for(var u=e.coord,c=0;c<2;c++)zX[u[c]]&&(u[c]=HX(n,n.mapDimension(r[c]),u[c]));else{e.coord=[];var h=t.getBaseAxis();if(h&&e.type&&zX[e.type]){var d=i.getOtherAxis(h);d&&(e.value=HX(n,n.mapDimension(d.dim),e.type))}}return e}}function VX(t,e,n,i){var r={};return null!=t.valueIndex||null!=t.valueDim?(r.valueDataDim=null!=t.valueIndex?e.getDimension(t.valueIndex):t.valueDim,r.valueAxis=n.getAxis(function(t,e){var n=t.getData().getDimensionInfo(e);return n&&n.coordDim}(i,r.valueDataDim)),r.baseAxis=n.getOtherAxis(r.valueAxis),r.baseDataDim=e.mapDimension(r.baseAxis.dim)):(r.baseAxis=i.getBaseAxis(),r.valueAxis=n.getOtherAxis(r.baseAxis),r.baseDataDim=e.mapDimension(r.baseAxis.dim),r.valueDataDim=e.mapDimension(r.valueAxis.dim)),r}function GX(t,e){return!(t&&t.containData&&e.coord&&!NX(e))||t.containData(e.coord)}function FX(t,e){return t?function(t,n,i,r){return Uv(r<2?t.coord&&t.coord[r]:t.value,e[r])}:function(t,n,i,r){return Uv(t.value,e[r])}}function HX(t,e,n){if("average"===n){var i=0,r=0;return t.each(e,function(t,e){isNaN(t)||(i+=t,r++)}),i/r}return"median"===n?t.getMedian(e):t.getDataExtent(e)["max"===n?1:0]}var WX=Pa(),UX=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(){this.markerGroupMap=mt()},e.prototype.render=function(t,e,n){var i=this,r=this.markerGroupMap;r.each(function(t){WX(t).keep=!1}),e.eachSeries(function(t){var r=OX.getMarkerModelFromSeries(t,i.type);r&&i.renderSeries(t,r,e,n)}),r.each(function(t){!WX(t).keep&&i.group.remove(t.group)}),function(t,e,n){t.eachSeries(function(t){var i=OX.getMarkerModelFromSeries(t,n),r=e.get(t.id);if(i&&r&&r.group){var o=Zd(i),a=o.z,s=o.zlevel;qd(r.group,a,s)}})}(e,r,this.type)},e.prototype.markKeep=function(t){WX(t).keep=!0},e.prototype.toggleBlurSeries=function(t,e){var n=this;B(t,function(t){var i=OX.getMarkerModelFromSeries(t,n.type);i&&i.getData().eachItemGraphicEl(function(t){t&&(e?mc(t):xc(t))})})},e.type="marker",e}(Ky);function YX(t,e,n){var i=e.coordinateSystem,r=n.getWidth(),o=n.getHeight(),a=i&&i.getArea&&i.getArea();t.each(function(n){var s,l=t.getItemModel(n),u="coordinate"===l.get("relativeTo"),c=u?a?a.width:0:r,h=u?a?a.height:0:o,d=u&&a?a.x:0,p=u&&a?a.y:0,f=Vo(l.get("x"),c)+d,g=Vo(l.get("y"),h)+p;if(isNaN(f)||isNaN(g)){if(e.getMarkerPosition)s=e.getMarkerPosition(t.getValues(t.dimensions,n));else if(i){var v=t.get(i.dimensions[0],n),y=t.get(i.dimensions[1],n);s=i.dataToPoint([v,y])}}else s=[f,g];isNaN(f)||(s[0]=f),isNaN(g)||(s[1]=g),t.setItemLayout(n,s)})}var XX=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.updateTransform=function(t,e,n){e.eachSeries(function(t){var e=OX.getMarkerModelFromSeries(t,"markPoint");e&&(YX(e.getData(),t,n),this.markerGroupMap.get(t.id).updateLayout())},this)},e.prototype.renderSeries=function(t,e,n,i){var r=t.coordinateSystem,o=t.id,a=t.getData(),s=this.markerGroupMap,l=s.get(o)||s.set(o,new nC),u=function(t,e,n){var i;i=t?V(t&&t.dimensions,function(t){var n=e.getData();return k(k({},n.getDimensionInfo(n.mapDimension(t))||{}),{name:t,ordinalMeta:null})}):[{name:"value",type:"float"}];var r=new Hb(i,n),o=V(n.get("data"),Y(BX,e));t&&(o=F(o,Y(GX,t)));var a=FX(!!t,i);return r.initData(o,null,a),r}(r,t,e);e.setData(u),YX(e.getData(),t,i),u.each(function(t){var n=u.getItemModel(t),i=n.getShallow("symbol"),r=n.getShallow("symbolSize"),o=n.getShallow("symbolRotate"),s=n.getShallow("symbolOffset"),l=n.getShallow("symbolKeepAspect");if(Z(i)||Z(r)||Z(o)||Z(s)){var c=e.getRawValue(t),h=e.getDataParams(t);Z(i)&&(i=i(c,h)),Z(r)&&(r=r(c,h)),Z(o)&&(o=o(c,h)),Z(s)&&(s=s(c,h))}var d=n.getModel("itemStyle").getItemStyle(),p=n.get("z2"),f=qm(a,"color");d.fill||(d.fill=f),u.setItemVisual(t,{z2:at(p,0),symbol:i,symbolSize:r,symbolRotate:o,symbolOffset:s,symbolKeepAspect:l,style:d})}),l.updateData(u),this.group.add(l.group),u.eachItemGraphicEl(function(t){t.traverse(function(t){Iu(t).dataModel=e})}),this.markKeep(l),l.group.silent=e.get("silent")||t.get("silent")},e.type="markPoint",e}(UX);var ZX=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.createMarkerModelFromSeries=function(t,n,i){return new e(t,n,i)},e.type="markLine",e.defaultOption={z:5,symbol:["circle","arrow"],symbolSize:[8,16],symbolOffset:0,precision:2,tooltip:{trigger:"item"},label:{show:!0,position:"end",distance:5},lineStyle:{type:"dashed"},emphasis:{label:{show:!0},lineStyle:{width:3}},animationEasing:"linear"},e}(OX),jX=Pa(),qX=function(t,e,n,i){var r,o=t.getData();if(X(i))r=i;else{var a=i.type;if("min"===a||"max"===a||"average"===a||"median"===a||null!=i.xAxis||null!=i.yAxis){var s=void 0,l=void 0;if(null!=i.yAxis||null!=i.xAxis)s=e.getAxis(null!=i.yAxis?"y":"x"),l=ot(i.yAxis,i.xAxis);else{var u=VX(i,o,e,t);s=u.valueAxis,l=HX(o,Kb(o,u.valueDataDim),a)}var c="x"===s.dim?0:1,h=1-c,d=C(i),p={coord:[]};d.type=null,d.coord=[],d.coord[h]=-1/0,p.coord[h]=1/0;var f=n.get("precision");f>=0&&K(l)&&(l=+l.toFixed(Math.min(f,20))),d.coord[c]=p.coord[c]=l,r=[d,p,{type:a,valueIndex:i.valueIndex,value:l}]}else r=[]}var g=[BX(t,r[0]),BX(t,r[1]),k({},r[2])];return g[2].type=g[2].type||null,D(g[2],g[0]),D(g[2],g[1]),g};function KX(t){return!isNaN(t)&&!isFinite(t)}function $X(t,e,n,i){var r=1-t,o=i.dimensions[t];return KX(e[r])&&KX(n[r])&&e[t]===n[t]&&i.getAxis(o).containData(e[t])}function JX(t,e){if("cartesian2d"===t.type){var n=e[0].coord,i=e[1].coord;if(n&&i&&($X(1,n,i,t)||$X(0,n,i,t)))return!0}return GX(t,e[0])&&GX(t,e[1])}function QX(t,e,n,i,r){var o,a=i.coordinateSystem,s=t.getItemModel(e),l=Vo(s.get("x"),r.getWidth()),u=Vo(s.get("y"),r.getHeight());if(isNaN(l)||isNaN(u)){if(i.getMarkerPosition)o=i.getMarkerPosition(t.getValues(t.dimensions,e));else{var c=a.dimensions,h=t.get(c[0],e),d=t.get(c[1],e);o=a.dataToPoint([h,d])}if(wC(a,"cartesian2d")){var p=a.getAxis("x"),f=a.getAxis("y");c=a.dimensions;KX(t.get(c[0],e))?o[0]=p.toGlobalCoord(p.getExtent()[n?0:1]):KX(t.get(c[1],e))&&(o[1]=f.toGlobalCoord(f.getExtent()[n?0:1]))}isNaN(l)||(o[0]=l),isNaN(u)||(o[1]=u)}else o=[l,u];t.setItemLayout(e,o)}var tZ=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.updateTransform=function(t,e,n){e.eachSeries(function(t){var e=OX.getMarkerModelFromSeries(t,"markLine");if(e){var i=e.getData(),r=jX(e).from,o=jX(e).to;r.each(function(e){QX(r,e,!0,t,n),QX(o,e,!1,t,n)}),i.each(function(t){i.setItemLayout(t,[r.getItemLayout(t),o.getItemLayout(t)])}),this.markerGroupMap.get(t.id).updateLayout()}},this)},e.prototype.renderSeries=function(t,e,n,i){var r=t.coordinateSystem,o=t.id,a=t.getData(),s=this.markerGroupMap,l=s.get(o)||s.set(o,new QN);this.group.add(l.group);var u=function(t,e,n){var i;i=t?V(t&&t.dimensions,function(t){var n=e.getData();return k(k({},n.getDimensionInfo(n.mapDimension(t))||{}),{name:t,ordinalMeta:null})}):[{name:"value",type:"float"}];var r=new Hb(i,n),o=new Hb(i,n),a=new Hb([],n),s=V(n.get("data"),Y(qX,e,t,n));t&&(s=F(s,Y(JX,t)));var l=FX(!!t,i);return r.initData(V(s,function(t){return t[0]}),null,l),o.initData(V(s,function(t){return t[1]}),null,l),a.initData(V(s,function(t){return t[2]})),a.hasItemOption=!0,{from:r,to:o,line:a}}(r,t,e),c=u.from,h=u.to,d=u.line;jX(e).from=c,jX(e).to=h,e.setData(d);var p=e.get("symbol"),f=e.get("symbolSize"),g=e.get("symbolRotate"),v=e.get("symbolOffset");function y(e,n,r){var o=e.getItemModel(n);QX(e,n,r,t,i);var s=o.getModel("itemStyle").getItemStyle();null==s.fill&&(s.fill=qm(a,"color")),e.setItemVisual(n,{symbolKeepAspect:o.get("symbolKeepAspect"),symbolOffset:at(o.get("symbolOffset",!0),v[r?0:1]),symbolRotate:at(o.get("symbolRotate",!0),g[r?0:1]),symbolSize:at(o.get("symbolSize"),f[r?0:1]),symbol:at(o.get("symbol",!0),p[r?0:1]),style:s})}X(p)||(p=[p,p]),X(f)||(f=[f,f]),X(g)||(g=[g,g]),X(v)||(v=[v,v]),u.from.each(function(t){y(c,t,!0),y(h,t,!1)}),d.each(function(t){var e=d.getItemModel(t),n=e.getModel("lineStyle").getLineStyle();d.setItemLayout(t,[c.getItemLayout(t),h.getItemLayout(t)]);var i=e.get("z2");null==n.stroke&&(n.stroke=c.getItemVisual(t,"style").fill),d.setItemVisual(t,{z2:at(i,0),fromSymbolKeepAspect:c.getItemVisual(t,"symbolKeepAspect"),fromSymbolOffset:c.getItemVisual(t,"symbolOffset"),fromSymbolRotate:c.getItemVisual(t,"symbolRotate"),fromSymbolSize:c.getItemVisual(t,"symbolSize"),fromSymbol:c.getItemVisual(t,"symbol"),toSymbolKeepAspect:h.getItemVisual(t,"symbolKeepAspect"),toSymbolOffset:h.getItemVisual(t,"symbolOffset"),toSymbolRotate:h.getItemVisual(t,"symbolRotate"),toSymbolSize:h.getItemVisual(t,"symbolSize"),toSymbol:h.getItemVisual(t,"symbol"),style:n})}),l.updateData(d),u.line.eachItemGraphicEl(function(t){Iu(t).dataModel=e,t.traverse(function(t){Iu(t).dataModel=e})}),this.markKeep(l),l.group.silent=e.get("silent")||t.get("silent")},e.type="markLine",e}(UX);var eZ=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.createMarkerModelFromSeries=function(t,n,i){return new e(t,n,i)},e.type="markArea",e.defaultOption={z:1,tooltip:{trigger:"item"},animation:!1,label:{show:!0,position:"top"},itemStyle:{borderWidth:0},emphasis:{label:{show:!0,position:"top"}}},e}(OX),nZ=Pa(),iZ=function(t,e,n,i){var r=i[0],o=i[1];if(r&&o){var a=BX(t,r),s=BX(t,o),l=a.coord,u=s.coord;l[0]=ot(l[0],-1/0),l[1]=ot(l[1],-1/0),u[0]=ot(u[0],1/0),u[1]=ot(u[1],1/0);var c=A([{},a,s]);return c.coord=[a.coord,s.coord],c.x0=a.x,c.y0=a.y,c.x1=s.x,c.y1=s.y,c}};function rZ(t){return!isNaN(t)&&!isFinite(t)}function oZ(t,e,n,i){var r=1-t;return rZ(e[r])&&rZ(n[r])}function aZ(t,e){var n=e.coord[0],i=e.coord[1],r={coord:n,x:e.x0,y:e.y0},o={coord:i,x:e.x1,y:e.y1};return wC(t,"cartesian2d")?!(!n||!i||!oZ(1,n,i)&&!oZ(0,n,i))||function(t,e,n){return!(t&&t.containZone&&e.coord&&n.coord&&!NX(e)&&!NX(n))||t.containZone(e.coord,n.coord)}(t,r,o):GX(t,r)||GX(t,o)}function sZ(t,e,n,i,r){var o,a=i.coordinateSystem,s=t.getItemModel(e),l=Vo(s.get(n[0]),r.getWidth()),u=Vo(s.get(n[1]),r.getHeight());if(isNaN(l)||isNaN(u)){if(i.getMarkerPosition){var c=t.getValues(["x0","y0"],e),h=t.getValues(["x1","y1"],e),d=a.clampData(c),p=a.clampData(h),f=[];"x0"===n[0]?f[0]=d[0]>p[0]?h[0]:c[0]:f[0]=d[0]>p[0]?c[0]:h[0],"y0"===n[1]?f[1]=d[1]>p[1]?h[1]:c[1]:f[1]=d[1]>p[1]?c[1]:h[1],o=i.getMarkerPosition(f,n,!0)}else{var g=[m=t.get(n[0],e),x=t.get(n[1],e)];a.clampData&&a.clampData(g,g),o=a.dataToPoint(g,!0)}if(wC(a,"cartesian2d")){var v=a.getAxis("x"),y=a.getAxis("y"),m=t.get(n[0],e),x=t.get(n[1],e);rZ(m)?o[0]=v.toGlobalCoord(v.getExtent()["x0"===n[0]?0:1]):rZ(x)&&(o[1]=y.toGlobalCoord(y.getExtent()["y0"===n[1]?0:1]))}isNaN(l)||(o[0]=l),isNaN(u)||(o[1]=u)}else o=[l,u];return o}var lZ=[["x0","y0"],["x1","y0"],["x1","y1"],["x0","y1"]],uZ=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.updateTransform=function(t,e,n){e.eachSeries(function(t){var e=OX.getMarkerModelFromSeries(t,"markArea");if(e){var i=e.getData();i.each(function(e){var r=V(lZ,function(r){return sZ(i,e,r,t,n)});i.setItemLayout(e,r),i.getItemGraphicEl(e).setShape("points",r)})}},this)},e.prototype.renderSeries=function(t,e,n,i){var r=t.coordinateSystem,o=t.id,a=t.getData(),s=this.markerGroupMap,l=s.get(o)||s.set(o,{group:new vo});this.group.add(l.group),this.markKeep(l);var u=function(t,e,n){var i,r,o=["x0","y0","x1","y1"];if(t){var a=V(t&&t.dimensions,function(t){var n=e.getData();return k(k({},n.getDimensionInfo(n.mapDimension(t))||{}),{name:t,ordinalMeta:null})});r=V(o,function(t,e){return{name:t,type:a[e%2].type}}),i=new Hb(r,n)}else i=new Hb(r=[{name:"value",type:"float"}],n);var s=V(n.get("data"),Y(iZ,e,t,n));t&&(s=F(s,Y(aZ,t)));var l=t?function(t,e,n,i){return Uv(t.coord[Math.floor(i/2)][i%2],r[i])}:function(t,e,n,i){return Uv(t.value,r[i])};return i.initData(s,null,l),i.hasItemOption=!0,i}(r,t,e);e.setData(u),u.each(function(e){var n=V(lZ,function(n){return sZ(u,e,n,t,i)}),o=r.getAxis("x").scale,s=r.getAxis("y").scale,l=o.getExtent(),c=s.getExtent(),h=[o.parse(u.get("x0",e)),o.parse(u.get("x1",e))],d=[s.parse(u.get("y0",e)),s.parse(u.get("y1",e))];Wo(h),Wo(d);var p=!!(l[0]>h[1]||l[1]d[1]||c[1]=0},e.prototype.getOrient=function(){return"vertical"===this.get("orient")?{index:1,name:"vertical"}:{index:0,name:"horizontal"}},e.type="legend.plain",e.dependencies=["series"],e.defaultOption={z:4,show:!0,orient:"horizontal",left:"center",bottom:ug.size.m,align:"auto",backgroundColor:ug.color.transparent,borderColor:ug.color.border,borderRadius:0,borderWidth:0,padding:5,itemGap:8,itemWidth:25,itemHeight:14,symbolRotate:"inherit",symbolKeepAspect:!0,inactiveColor:ug.color.disabled,inactiveBorderColor:ug.color.disabled,inactiveBorderWidth:"auto",itemStyle:{color:"inherit",opacity:"inherit",borderColor:"inherit",borderWidth:"auto",borderCap:"inherit",borderJoin:"inherit",borderDashOffset:"inherit",borderMiterLimit:"inherit"},lineStyle:{width:"auto",color:"inherit",inactiveColor:ug.color.disabled,inactiveWidth:2,opacity:"inherit",type:"inherit",cap:"inherit",join:"inherit",dashOffset:"inherit",miterLimit:"inherit"},textStyle:{color:ug.color.secondary},selectedMode:!0,selector:!1,selectorLabel:{show:!0,borderRadius:10,padding:[3,5,3,5],fontSize:12,fontFamily:"sans-serif",color:ug.color.tertiary,borderWidth:1,borderColor:ug.color.border},emphasis:{selectorLabel:{show:!0,color:ug.color.quaternary}},selectorPosition:"auto",selectorItemGap:7,selectorButtonGap:10,tooltip:{show:!1},triggerEvent:!1},e}(lg),hZ=Y,dZ=B,pZ=vo,fZ=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.newlineDisabled=!1,n}return n(e,t),e.prototype.init=function(){this.group.add(this._contentGroup=new pZ),this.group.add(this._selectorGroup=new pZ),this._isFirstRender=!0},e.prototype.getContentGroup=function(){return this._contentGroup},e.prototype.getSelectorGroup=function(){return this._selectorGroup},e.prototype.render=function(t,e,n){var i=this._isFirstRender;if(this._isFirstRender=!1,this.resetInner(),t.get("show",!0)){var r=t.get("align"),o=t.get("orient");r&&"auto"!==r||(r="right"===t.get("left")&&"vertical"===o?"right":"left");var a=t.get("selector",!0),s=t.get("selectorPosition",!0);!a||s&&"auto"!==s||(s="horizontal"===o?"end":"start"),this.renderInner(r,t,e,n,a,o,s);var l=eg(t,n).refContainer,u=t.getBoxLayoutParams(),c=t.get("padding"),h=Jf(u,l,c),d=this.layoutInner(t,r,h,i,a,s),p=Jf(P({width:d.width,height:d.height},u),l,c);this.group.x=p.x-d.x,this.group.y=p.y-d.y,this.group.markRedraw(),this.group.add(this._backgroundEl=FU(d,t))}},e.prototype.resetInner=function(){this.getContentGroup().removeAll(),this._backgroundEl&&this.group.remove(this._backgroundEl),this.getSelectorGroup().removeAll()},e.prototype.renderInner=function(t,e,n,i,r,o,a){var s=this.getContentGroup(),l=mt(),u=e.get("selectedMode"),c=e.get("triggerEvent"),h=[];n.eachRawSeries(function(t){!t.get("legendHoverLink")&&h.push(t.id)}),dZ(e.getData(),function(r,o){var a=this,d=r.get("name");if(!this.newlineDisabled&&(""===d||"\n"===d)){var p=new pZ;return p.newline=!0,void s.add(p)}var f=n.getSeriesByName(d)[0];if(!l.get(d)){if(f){var g=f.getData(),v=g.getVisual("legendLineStyle")||{},y=g.getVisual("legendIcon"),m=g.getVisual("style"),x=this._createItem(f,d,o,r,e,t,v,m,y,u,i);x.on("click",hZ(gZ,d,null,i,h)).on("mouseover",hZ(vZ,f.name,null,i,h)).on("mouseout",hZ(yZ,f.name,null,i,h)),n.ssr&&x.eachChild(function(t){var e=Iu(t);e.seriesIndex=f.seriesIndex,e.dataIndex=o,e.ssrType="legend"}),c&&x.eachChild(function(t){a.packEventData(t,e,f,o,d)}),l.set(d,!0)}else n.eachRawSeries(function(a){var s=this;if(!l.get(d)&&a.legendVisualProvider){var p=a.legendVisualProvider;if(!p.containName(d))return;var f=p.indexOfName(d),g=p.getItemVisual(f,"style"),v=p.getItemVisual(f,"legendIcon"),y=gi(g.fill);y&&0===y[3]&&(y[3]=.2,g=k(k({},g),{fill:Mi(y,"rgba")}));var m=this._createItem(a,d,o,r,e,t,{},g,v,u,i);m.on("click",hZ(gZ,null,d,i,h)).on("mouseover",hZ(vZ,null,d,i,h)).on("mouseout",hZ(yZ,null,d,i,h)),n.ssr&&m.eachChild(function(t){var e=Iu(t);e.seriesIndex=a.seriesIndex,e.dataIndex=o,e.ssrType="legend"}),c&&m.eachChild(function(t){s.packEventData(t,e,a,o,d)}),l.set(d,!0)}},this);0}},this),r&&this._createSelector(r,e,i,o,a)},e.prototype.packEventData=function(t,e,n,i,r){var o={componentType:"legend",componentIndex:e.componentIndex,dataIndex:i,value:r,seriesIndex:n.seriesIndex};Iu(t).eventData=o},e.prototype._createSelector=function(t,e,n,i,r){var o=this.getSelectorGroup();dZ(t,function(t){var i=t.type,r=new du({style:{x:0,y:0,align:"center",verticalAlign:"middle"},onclick:function(){n.dispatchAction({type:"all"===i?"legendAllSelect":"legendInverseSelect",legendId:e.id})}});o.add(r),op(r,{normal:e.getModel("selectorLabel"),emphasis:e.getModel(["emphasis","selectorLabel"])},{defaultText:t.title}),Ac(r)})},e.prototype._createItem=function(t,e,n,i,r,o,a,s,l,u,c){var h=t.visualDrawType,d=r.get("itemWidth"),p=r.get("itemHeight"),f=r.isSelected(e),g=i.get("symbolRotate"),v=i.get("symbolKeepAspect"),y=i.get("icon"),m=function(t,e,n,i,r,o,a){function s(t,e){"auto"===t.lineWidth&&(t.lineWidth=e.lineWidth>0?2:0),dZ(t,function(n,i){"inherit"===t[i]&&(t[i]=e[i])})}var l=e.getModel("itemStyle"),u=l.getItemStyle(),c=0===t.lastIndexOf("empty",0)?"fill":"stroke",h=l.getShallow("decal");u.decal=h&&"inherit"!==h?Zx(h,a):i.decal,"inherit"===u.fill&&(u.fill=i[r]);"inherit"===u.stroke&&(u.stroke=i[c]);"inherit"===u.opacity&&(u.opacity=("fill"===r?i:n).opacity);s(u,i);var d=e.getModel("lineStyle"),p=d.getLineStyle();if(s(p,n),"auto"===u.fill&&(u.fill=i.fill),"auto"===u.stroke&&(u.stroke=i.fill),"auto"===p.stroke&&(p.stroke=i.fill),!o){var f=e.get("inactiveBorderWidth"),g=u[c];u.lineWidth="auto"===f?i.lineWidth>0&&g?2:0:u.lineWidth,u.fill=e.get("inactiveColor"),u.stroke=e.get("inactiveBorderColor"),p.stroke=d.get("inactiveColor"),p.lineWidth=d.get("inactiveWidth")}return{itemStyle:u,lineStyle:p}}(l=y||l||"roundRect",i,a,s,h,f,c),x=new pZ,_=i.getModel("textStyle");if(!Z(t.getLegendIcon)||y&&"inherit"!==y){var b="inherit"===y&&t.getData().getVisual("symbol")?"inherit"===g?t.getData().getVisual("symbolRotate"):g:0;x.add(function(t){var e=t.icon||"roundRect",n=xx(e,0,0,t.itemWidth,t.itemHeight,t.itemStyle.fill,t.symbolKeepAspect);n.setStyle(t.itemStyle),n.rotation=(t.iconRotate||0)*Math.PI/180,n.setOrigin([t.itemWidth/2,t.itemHeight/2]),e.indexOf("empty")>-1&&(n.style.stroke=n.style.fill,n.style.fill=ug.color.neutral00,n.style.lineWidth=2);return n}({itemWidth:d,itemHeight:p,icon:l,iconRotate:b,itemStyle:m.itemStyle,lineStyle:m.lineStyle,symbolKeepAspect:v}))}else x.add(t.getLegendIcon({itemWidth:d,itemHeight:p,icon:l,iconRotate:g,itemStyle:m.itemStyle,lineStyle:m.lineStyle,symbolKeepAspect:v}));var w="left"===o?d+5:-5,S=o,M=r.get("formatter"),I=e;j(M)&&M?I=M.replace("{name}",null!=e?e:""):Z(M)&&(I=M(e));var T=f?_.getTextColor():i.get("inactiveColor");x.add(new du({style:sp(_,{text:I,x:w,y:p/2,fill:T,align:S,verticalAlign:"middle"},{inheritColor:T})}));var C=new lu({shape:x.getBoundingRect(),style:{fill:"transparent"}}),D=i.getModel("tooltip");return D.get("show")&&Gd({el:C,componentModel:r,itemName:e,itemTooltipOption:D.option}),x.add(C),x.eachChild(function(t){t.silent=!0}),C.silent=!u,this.getContentGroup().add(x),Ac(x),x.__legendDataIndex=n,x},e.prototype.layoutInner=function(t,e,n,i,r,o){var a=this.getContentGroup(),s=this.getSelectorGroup();qf(t.get("orient"),a,t.get("itemGap"),n.width,n.height);var l=a.getBoundingRect(),u=[-l.x,-l.y];if(s.markRedraw(),a.markRedraw(),r){qf("horizontal",s,t.get("selectorItemGap",!0));var c=s.getBoundingRect(),h=[-c.x,-c.y],d=t.get("selectorButtonGap",!0),p=t.getOrient().index,f=0===p?"width":"height",g=0===p?"height":"width",v=0===p?"y":"x";"end"===o?h[p]+=l[f]+d:u[p]+=c[f]+d,h[1-p]+=l[g]/2-c[g]/2,s.x=h[0],s.y=h[1],a.x=u[0],a.y=u[1];var y={x:0,y:0};return y[f]=l[f]+d+c[f],y[g]=Math.max(l[g],c[g]),y[v]=Math.min(0,c[v]+h[1-p]),y}return a.x=u[0],a.y=u[1],this.group.getBoundingRect()},e.prototype.remove=function(){this.getContentGroup().removeAll(),this._isFirstRender=!0},e.type="legend.plain",e}(Ky);function gZ(t,e,n,i){yZ(t,e,n,i),n.dispatchAction({type:"legendToggleSelect",name:null!=t?t:e}),vZ(t,e,n,i)}function vZ(t,e,n,i){n.usingTHL()||n.dispatchAction({type:"highlight",seriesName:t,name:e,excludeSeriesId:i})}function yZ(t,e,n,i){n.usingTHL()||n.dispatchAction({type:"downplay",seriesName:t,name:e,excludeSeriesId:i})}function mZ(t,e,n){var i="allSelect"===t||"inverseSelect"===t,r={},o=[];n.eachComponent({mainType:"legend",query:e},function(n){i?n[t]():n[t](e.name),xZ(n,r),o.push(n.componentIndex)});var a={};return n.eachComponent("legend",function(t){B(r,function(e,n){t[e?"select":"unSelect"](n)}),xZ(t,a)}),i?{selected:a,legendIndex:o}:{name:e.name,selected:a}}function xZ(t,e){var n=e||{};return B(t.getData(),function(e){var i=e.get("name");if("\n"!==i&&""!==i){var r=t.isSelected(i);wt(n,i)?n[i]=n[i]&&r:n[i]=r}}),n}var _Z=as(function(t){var e=t.findComponents({mainType:"legend"});e&&e.length&&t.filterSeries(function(t){for(var n=0;nn[r],f=[-h.x,-h.y];e||(f[i]=l[s]);var g=[0,0],v=[-d.x,-d.y],y=at(t.get("pageButtonGap",!0),t.get("itemGap",!0));p&&("end"===t.get("pageButtonPosition",!0)?v[i]+=n[r]-d[r]:g[i]+=d[r]+y);v[1-i]+=h[o]/2-d[o]/2,l.setPosition(f),u.setPosition(g),c.setPosition(v);var m={x:0,y:0};if(m[r]=p?n[r]:h[r],m[o]=Math.max(h[o],d[o]),m[a]=Math.min(0,d[a]+v[1-i]),u.__rectSize=n[r],p){var x={x:0,y:0};x[r]=Math.max(n[r]-d[r]-y,0),x[o]=m[o],u.setClipPath(new lu({shape:x})),u.__rectSize=x[r]}else c.eachChild(function(t){t.attr({invisible:!0,silent:!0})});var _=this._getPageInfo(t);return null!=_.pageIndex&&rd(l,{x:_.contentPosition[0],y:_.contentPosition[1]},p?t:null),this._updatePageInfoView(t,_),m},e.prototype._pageGo=function(t,e,n){var i=this._getPageInfo(e)[t];null!=i&&n.dispatchAction({type:"legendScroll",scrollDataIndex:i,legendId:e.id})},e.prototype._updatePageInfoView=function(t,e){var n=this._controllerGroup;B(["pagePrev","pageNext"],function(i){var r=null!=e[i+"DataIndex"],o=n.childOfName(i);o&&(o.setStyle("fill",r?t.get("pageIconColor",!0):t.get("pageIconInactiveColor",!0)),o.cursor=r?"pointer":"default")});var i=n.childOfName("pageText"),r=t.get("pageFormatter"),o=e.pageIndex,a=null!=o?o+1:0,s=e.pageCount;i&&r&&i.setStyle("text",j(r)?r.replace("{current}",null==a?"":a+"").replace("{total}",null==s?"":s+""):r({current:a,total:s}))},e.prototype._getPageInfo=function(t){var e=t.get("scrollDataIndex",!0),n=this.getContentGroup(),i=this._containerGroup.__rectSize,r=t.getOrient().index,o=IZ[r],a=TZ[r],s=this._findTargetItemIndex(e),l=n.children(),u=l[s],c=l.length,h=c?1:0,d={contentPosition:[n.x,n.y],pageCount:h,pageIndex:h-1,pagePrevDataIndex:null,pageNextDataIndex:null};if(!u)return d;var p=m(u);d.contentPosition[r]=-p.s;for(var f=s+1,g=p,v=p,y=null;f<=c;++f)(!(y=m(l[f]))&&v.e>g.s+i||y&&!x(y,g.s))&&(g=v.i>g.i?v:y)&&(null==d.pageNextDataIndex&&(d.pageNextDataIndex=g.i),++d.pageCount),v=y;for(f=s-1,g=p,v=p,y=null;f>=-1;--f)(y=m(l[f]))&&x(v,y.s)||!(g.i=e&&t.s<=e+i}},e.prototype._findTargetItemIndex=function(t){return this._showController?(this.getContentGroup().eachChild(function(i,r){var o=i.__legendDataIndex;null==n&&null!=o&&(n=r),o===t&&(e=r)}),null!=e?e:n):0;var e,n},e.type="legend.scroll",e}(fZ);function DZ(t){gb(bZ),t.registerComponentModel(wZ),t.registerComponentView(CZ),function(t){t.registerAction("legendScroll","legendscroll",function(t,e){var n=t.scrollDataIndex;null!=n&&e.eachComponent({mainType:"legend",subType:"scroll",query:t},function(t){t.setScrollDataIndex(n)})})}(t)}var AZ=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="dataZoom.inside",e.defaultOption=Op(TU.defaultOption,{disabled:!1,zoomLock:!1,zoomOnMouseWheel:!0,moveOnMouseMove:!0,moveOnMouseWheel:!1,preventDefaultMouseMove:!0}),e}(TU),kZ=Pa();function LZ(t,e){if(e){t.removeKey(e.model.uid);var n=e.controller;n&&n.dispose()}}function PZ(t,e){t.isDisposed()||t.dispatchAction({type:"dataZoom",animation:{easing:"cubicOut",duration:100},batch:e})}function OZ(t,e,n,i){return t.coordinateSystem.containPoint([n,i])}function RZ(t){t.registerUpdateLifecycle("coordsys:aftercreate",function(t,e){var n=kZ(e),i=n.coordSysRecordMap||(n.coordSysRecordMap=mt());i.each(function(t){t.dataZoomInfoMap=null}),t.eachComponent({mainType:"dataZoom",subType:"inside"},function(t){B(bU(t).infoList,function(n){var r=n.model.uid,o=i.get(r)||i.set(r,function(t,e){var n={model:e,containsPoint:Y(OZ,e),dispatchAction:Y(PZ,t),dataZoomInfoMap:null,controller:null},i=n.controller=new uL(t.getZr());return B(["pan","zoom","scrollMove"],function(t){i.on(t,function(e){var i=[];n.dataZoomInfoMap.each(function(r){if(e.isAvailableBehavior(r.model.option)){var o=(r.getRange||{})[t],a=o&&o(r.dzReferCoordSysInfo,n.model.mainType,n.controller,e);!r.model.get("disabled",!0)&&a&&i.push({dataZoomId:r.model.id,start:a[0],end:a[1]})}}),i.length&&n.dispatchAction(i)})}),n}(e,n.model));(o.dataZoomInfoMap||(o.dataZoomInfoMap=mt())).set(t.uid,{dzReferCoordSysInfo:n,model:t,getRange:null})})}),i.each(function(t){var n,r=t.controller,o=t.dataZoomInfoMap;if(o){var a=o.keys()[0];null!=a&&(n=o.get(a))}if(n){var s=function(t,e,n){var i,r,o,a="type_",s={type_true:2,type_move:1,type_false:0,type_undefined:-1},l=!0;return t.each(function(t){var e=t.model,n=!e.get("disabled",!0)&&(!e.get("zoomLock",!0)||"move");s[a+n]>s[a+i]&&(i=n),l=l&&e.get("preventDefaultMouseMove",!0),r=at(e.get("cursorGrab",!0),r),o=at(e.get("cursorGrabbing",!0),o)}),{controlType:i,opt:{zoomOnMouseWheel:!0,moveOnMouseMove:!0,moveOnMouseWheel:!0,preventDefaultMouseMove:!!l,api:n,zInfo:{component:e.model},triggerInfo:{roamTrigger:null,isInSelf:e.containsPoint},cursorGrab:r,cursorGrabbing:o}}}(o,t,e);r.enable(s.controlType,s.opt),cm(t,"dispatchAction",n.model.get("throttle",!0),"fixRate")}else LZ(i,t)})})}var NZ=function(t){function e(){var e=null!==t&&t.apply(this,arguments)||this;return e.type="dataZoom.inside",e}return n(e,t),e.prototype.render=function(e,n,i){t.prototype.render.apply(this,arguments),e.noTarget()?this._clear():(this.range=e.getPercentRange(),function(t,e,n){kZ(t).coordSysRecordMap.each(function(t){var i=t.dataZoomInfoMap.get(e.uid);i&&(i.getRange=n)})}(i,e,{pan:U(EZ.pan,this),zoom:U(EZ.zoom,this),scrollMove:U(EZ.scrollMove,this)}))},e.prototype.dispose=function(){this._clear(),t.prototype.dispose.apply(this,arguments)},e.prototype._clear=function(){!function(t,e){for(var n=kZ(t).coordSysRecordMap,i=n.keys(),r=0;r0?s.pixelStart+s.pixelLength-s.pixel:s.pixel-s.pixelStart)/s.pixelLength*(o[1]-o[0])+o[0],u=Math.max(1/i.scale,0);o[0]=(o[0]-l)*u+l,o[1]=(o[1]-l)*u+l;var c=this.dataZoomModel.findRepresentativeAxisProxy().getMinMaxSpan();return ez(0,o,[0,100],0,c.minSpan,c.maxSpan),this.range=o,r[0]!==o[0]||r[1]!==o[1]?o:void 0}},pan:zZ(function(t,e,n,i,r,o){var a=BZ[i]([o.oldX,o.oldY],[o.newX,o.newY],e,r,n);return a.signal*(t[1]-t[0])*a.pixel/a.pixelLength}),scrollMove:zZ(function(t,e,n,i,r,o){return BZ[i]([0,0],[o.scrollDelta,o.scrollDelta],e,r,n).signal*(t[1]-t[0])*o.scrollDelta})};function zZ(t){return function(e,n,i,r){var o=this.range,a=o.slice(),s=e.axisModels[0];if(s)return ez(t(a,s,e,n,i,r),a,[0,100],"all"),this.range=a,o[0]!==a[0]||o[1]!==a[1]?a:void 0}}var BZ={grid:function(t,e,n,i,r){var o=n.axis,a={},s=r.model.coordinateSystem.getRect();return t=t||[0,0],"x"===o.dim?(a.pixel=e[0]-t[0],a.pixelLength=s.width,a.pixelStart=s.x,a.signal=o.inverse?1:-1):(a.pixel=e[1]-t[1],a.pixelLength=s.height,a.pixelStart=s.y,a.signal=o.inverse?-1:1),a},polar:function(t,e,n,i,r){var o=n.axis,a={},s=r.model.coordinateSystem,l=s.getRadiusAxis().getExtent(),u=s.getAngleAxis().getExtent();return t=t?s.pointToCoord(t):[0,0],e=s.pointToCoord(e),"radiusAxis"===n.mainType?(a.pixel=e[0]-t[0],a.pixelLength=l[1]-l[0],a.pixelStart=l[0],a.signal=o.inverse?1:-1):(a.pixel=e[1]-t[1],a.pixelLength=u[1]-u[0],a.pixelStart=u[0],a.signal=o.inverse?-1:1),a},singleAxis:function(t,e,n,i,r){var o=n.axis,a=r.model.coordinateSystem.getRect(),s={};return t=t||[0,0],"horizontal"===o.orient?(s.pixel=e[0]-t[0],s.pixelLength=a.width,s.pixelStart=a.x,s.signal=o.inverse?1:-1):(s.pixel=e[1]-t[1],s.pixelLength=a.height,s.pixelStart=a.y,s.signal=o.inverse?-1:1),s}};function VZ(t){RU(t),t.registerComponentModel(AZ),t.registerComponentView(NZ),RZ(t)}var GZ=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.type="dataZoom.slider",e.layoutMode="box",e.defaultOption=Op(TU.defaultOption,{show:!0,right:"ph",top:"ph",width:"ph",height:"ph",left:null,bottom:null,borderColor:ug.color.accent10,borderRadius:0,backgroundColor:ug.color.transparent,dataBackground:{lineStyle:{color:ug.color.accent30,width:.5},areaStyle:{color:ug.color.accent20,opacity:.2}},selectedDataBackground:{lineStyle:{color:ug.color.accent40,width:.5},areaStyle:{color:ug.color.accent20,opacity:.3}},fillerColor:"rgba(135,175,274,0.2)",handleIcon:"path://M-9.35,34.56V42m0-40V9.5m-2,0h4a2,2,0,0,1,2,2v21a2,2,0,0,1-2,2h-4a2,2,0,0,1-2-2v-21A2,2,0,0,1-11.35,9.5Z",handleSize:"100%",handleStyle:{color:ug.color.neutral00,borderColor:ug.color.accent20},moveHandleSize:7,moveHandleIcon:"path://M-320.9-50L-320.9-50c18.1,0,27.1,9,27.1,27.1V85.7c0,18.1-9,27.1-27.1,27.1l0,0c-18.1,0-27.1-9-27.1-27.1V-22.9C-348-41-339-50-320.9-50z M-212.3-50L-212.3-50c18.1,0,27.1,9,27.1,27.1V85.7c0,18.1-9,27.1-27.1,27.1l0,0c-18.1,0-27.1-9-27.1-27.1V-22.9C-239.4-41-230.4-50-212.3-50z M-103.7-50L-103.7-50c18.1,0,27.1,9,27.1,27.1V85.7c0,18.1-9,27.1-27.1,27.1l0,0c-18.1,0-27.1-9-27.1-27.1V-22.9C-130.9-41-121.8-50-103.7-50z",moveHandleStyle:{color:ug.color.accent40,opacity:.5},showDetail:!0,showDataShadow:"auto",realtime:!0,zoomLock:!1,textStyle:{color:ug.color.tertiary},brushSelect:!0,brushStyle:{color:ug.color.accent30,opacity:.3},emphasis:{handleLabel:{show:!0},handleStyle:{borderColor:ug.color.accent40},moveHandleStyle:{opacity:.8}},defaultLocationEdgeGap:15}),e}(TU),FZ=lu,HZ="horizontal",WZ="vertical",UZ=["line","bar","candlestick","scatter"],YZ={easing:"cubicOut",duration:100,delay:0},XZ=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n._displayables={},n}return n(e,t),e.prototype.init=function(t,e){this.api=e,this._onBrush=U(this._onBrush,this),this._onBrushEnd=U(this._onBrushEnd,this)},e.prototype.render=function(e,n,i,r){if(t.prototype.render.apply(this,arguments),cm(this,"_dispatchZoomAction",e.get("throttle"),"fixRate"),this._orient=e.getOrient(),!1!==e.get("show")){if(e.noTarget())return this._clear(),void this.group.removeAll();r&&"dataZoom"===r.type&&r.from===this.uid||this._buildView(),this._updateView()}else this.group.removeAll()},e.prototype.dispose=function(){this._clear(),t.prototype.dispose.apply(this,arguments)},e.prototype._clear=function(){hm(this,"_dispatchZoomAction");var t=this.api.getZr();t.off("mousemove",this._onBrush),t.off("mouseup",this._onBrushEnd)},e.prototype._buildView=function(){var t=this.group;t.removeAll(),this._brushing=!1,this._displayables.brushRect=null,this._resetLocation(),this._resetInterval();var e=this._displayables.sliderGroup=new vo;this._renderBackground(),this._renderHandle(),this._renderDataShadow(),t.add(e),this._positionGroup()},e.prototype._resetLocation=function(){var t=this.dataZoomModel,e=this.api,n=t.get("brushSelect")?7:0,i=eg(t,e).refContainer,r=this._findCoordRect(),o=t.get("defaultLocationEdgeGap",!0)||0,a=this._orient===HZ?{right:i.width-r.x-r.width,top:i.height-30-o-n,width:r.width,height:30}:{right:o,top:r.y,width:30,height:r.height},s=og(t.option);B(["right","top","width","height"],function(t){"ph"===s[t]&&(s[t]=a[t])});var l=Jf(s,i);this._location={x:l.x,y:l.y},this._size=[l.width,l.height],this._orient===WZ&&this._size.reverse()},e.prototype._positionGroup=function(){var t=this.group,e=this._location,n=this._orient,i=this.dataZoomModel.getFirstTargetAxisModel(),r=i&&i.get("inverse"),o=this._displayables.sliderGroup,a=(this._dataShadowInfo||{}).otherAxisInverse;o.attr(n!==HZ||r?n===HZ&&r?{scaleY:a?1:-1,scaleX:-1}:n!==WZ||r?{scaleY:a?-1:1,scaleX:-1,rotation:Math.PI/2}:{scaleY:a?-1:1,scaleX:1,rotation:Math.PI/2}:{scaleY:a?1:-1,scaleX:1});var s=t.getBoundingRect([o]),l=isNaN(s.x)?0:s.x,u=isNaN(s.y)?0:s.y;t.x=e.x-l,t.y=e.y-u,t.markRedraw()},e.prototype._getViewExtent=function(){return[0,this._size[0]]},e.prototype._renderBackground=function(){var t=this.dataZoomModel,e=this._size,n=this._displayables.sliderGroup,i=t.get("brushSelect");n.add(new FZ({silent:!0,shape:{x:0,y:0,width:e[0],height:e[1]},style:{fill:t.get("backgroundColor")},z2:-40}));var r=new FZ({shape:{x:0,y:0,width:e[0],height:e[1]},style:{fill:"transparent"},z2:0,onclick:U(this._onClickPanel,this)}),o=this.api.getZr();i?(r.on("mousedown",this._onBrushStart,this),r.cursor="crosshair",o.on("mousemove",this._onBrush),o.on("mouseup",this._onBrushEnd)):(o.off("mousemove",this._onBrush),o.off("mouseup",this._onBrushEnd)),n.add(r)},e.prototype._renderDataShadow=function(){var t=this._dataShadowInfo=this._prepareDataShadowInfo();if(this._displayables.dataShadowSegs=[],t){var e=this._size,n=this._shadowSize||[],i=t.series,r=i.getRawData(),o=i.getShadowDim&&i.getShadowDim(),a=o&&r.getDimensionInfo(o)?i.getShadowDim():t.otherDim;if(null!=a){var s=this._shadowPolygonPts,l=this._shadowPolylinePts;if(r!==this._shadowData||a!==this._shadowDim||e[0]!==n[0]||e[1]!==n[1]){var u=r.getDataExtent(t.thisDim),c=r.getDataExtent(a),h=.3*(c[1]-c[0]);c=[c[0]-h,c[1]+h];var d,p=[0,e[1]],f=[0,e[0]],g=[[e[0],0],[0,0]],v=[],y=f[1]/Math.max(1,r.count()-1),m=e[0]/(u[1]-u[0]),x="time"===t.thisAxis.type,_=-y,b=Math.round(r.count()/e[0]);r.each([t.thisDim,a],function(t,e,n){if(b>0&&n%b)x||(_+=y);else{_=x?(+t-u[0])*m:_+y;var i=null==e||isNaN(e)||""===e,r=i?0:Bo(e,c,p,!0);i&&!d&&n?(g.push([g[g.length-1][0],0]),v.push([v[v.length-1][0],0])):!i&&d&&(g.push([_,0]),v.push([_,0])),i||(g.push([_,r]),v.push([_,r])),d=i}}),s=this._shadowPolygonPts=g,l=this._shadowPolylinePts=v}this._shadowData=r,this._shadowDim=a,this._shadowSize=[e[0],e[1]];for(var w=this.dataZoomModel,S=0;S<3;S++){var M=I(1===S);this._displayables.sliderGroup.add(M),this._displayables.dataShadowSegs.push(M)}}}function I(t){var e=w.getModel(t?"selectedDataBackground":"dataBackground"),n=new vo,i=new Dh({shape:{points:s},segmentIgnoreThreshold:1,style:e.getModel("areaStyle").getAreaStyle(),silent:!0,z2:-20}),r=new kh({shape:{points:l},segmentIgnoreThreshold:1,style:e.getModel("lineStyle").getLineStyle(),silent:!0,z2:-19});return n.add(i),n.add(r),n}},e.prototype._prepareDataShadowInfo=function(){var t=this.dataZoomModel,e=t.get("showDataShadow");if(!1!==e){var n,i=this.ecModel;return t.eachTargetAxis(function(r,o){B(t.getAxisProxy(r,o).getTargetSeriesModels(),function(t){if(!(n||!0!==e&&R(UZ,t.get("type"))<0)){var a,s=i.getComponent(xU(r),o).axis,l=function(t){var e={x:"y",y:"x",radius:"angle",angle:"radius"};return e[t]}(r),u=t.coordinateSystem;null!=l&&u.getOtherAxis&&(a=u.getOtherAxis(s).inverse),l=t.getData().mapDimension(l);var c=t.getData().mapDimension(r);n={thisAxis:s,series:t,thisDim:c,otherDim:l,otherAxisInverse:a}}},this)},this),n}},e.prototype._renderHandle=function(){var t=this.group,e=this._displayables,n=e.handles=[null,null],i=e.handleLabels=[null,null],r=this._displayables.sliderGroup,o=this._size,a=this.dataZoomModel,s=this.api,l=a.get("borderRadius")||0,u=a.get("brushSelect"),c=e.filler=new FZ({silent:u,style:{fill:a.get("fillerColor")},textConfig:{position:"inside"}});r.add(c),r.add(new FZ({silent:!0,subPixelOptimize:!0,shape:{x:0,y:0,width:o[0],height:o[1],r:l},style:{stroke:a.get("dataBackgroundColor")||a.get("borderColor"),lineWidth:1,fill:ug.color.transparent}})),B([0,1],function(e){var o=a.get("handleIcon");!vx[o]&&o.indexOf("path://")<0&&o.indexOf("image://")<0&&(o="path://"+o);var s,l=xx(o,-1,0,2,2,null,!0);l.attr({cursor:(s=this._orient,"vertical"===s?"ns-resize":"ew-resize"),draggable:!0,drift:U(this._onDragMove,this,e),ondragend:U(this._onDragEnd,this),onmouseover:U(this._onOverDataInfoTriggerArea,this,!0),onmouseout:U(this._onOverDataInfoTriggerArea,this,!1),z2:5});var u=l.getBoundingRect(),c=a.get("handleSize");this._handleHeight=Vo(c,this._size[1]),this._handleWidth=u.width/u.height*this._handleHeight,l.setStyle(a.getModel("handleStyle").getItemStyle()),l.style.strokeNoScale=!0,l.rectHover=!0,l.ensureState("emphasis").style=a.getModel(["emphasis","handleStyle"]).getItemStyle(),Ac(l);var h=a.get("handleColor");null!=h&&(l.style.fill=h),r.add(n[e]=l);var d=a.getModel("textStyle"),p=(a.get("handleLabel")||{}).show||!1;t.add(i[e]=new du({silent:!0,invisible:!p,style:sp(d,{x:0,y:0,text:"",verticalAlign:"middle",align:"center",fill:d.getTextColor(),font:d.getFont()}),z2:10}))},this);var h=c;if(u){var d=Vo(a.get("moveHandleSize"),o[1]),p=e.moveHandle=new lu({style:a.getModel("moveHandleStyle").getItemStyle(),silent:!0,shape:{r:[0,0,2,2],y:o[1]-.5,height:d}}),f=.8*d,g=e.moveHandleIcon=xx(a.get("moveHandleIcon"),-f/2,-f/2,f,f,ug.color.neutral00,!0);g.silent=!0,g.y=o[1]+d/2-.5,p.ensureState("emphasis").style=a.getModel(["emphasis","moveHandleStyle"]).getItemStyle();var v=Math.min(o[1]/2,Math.max(d,10));(h=e.moveZone=new lu({invisible:!0,shape:{y:o[1]-v,height:d+v}})).on("mouseover",function(){s.enterEmphasis(p)}).on("mouseout",function(){s.leaveEmphasis(p)}),r.add(p),r.add(g),r.add(h)}h.attr({draggable:!0,cursor:"grab",drift:U(this._onActualMoveZoneDrift,this),ondragstart:U(this._onActualMoveZoneDragStart,this),ondragend:U(this._onActualMoveZoneDragEnd,this),onmouseover:U(this._onOverDataInfoTriggerArea,this,!0),onmouseout:U(this._onOverDataInfoTriggerArea,this,!1)})},e.prototype._resetInterval=function(){var t=this._range=this.dataZoomModel.getPercentRange(),e=this._getViewExtent();this._handleEnds=[Bo(t[0],[0,100],e,!0),Bo(t[1],[0,100],e,!0)]},e.prototype._updateInterval=function(t,e){var n=this.dataZoomModel,i=this._handleEnds,r=this._getViewExtent(),o=n.findRepresentativeAxisProxy().getMinMaxSpan(),a=[0,100];ez(e,i,r,n.get("zoomLock")?"all":t,null!=o.minSpan?Bo(o.minSpan,a,r,!0):null,null!=o.maxSpan?Bo(o.maxSpan,a,r,!0):null);var s=this._range,l=this._range=Wo([Bo(i[0],r,a,!0),Bo(i[1],r,a,!0)]);return!s||s[0]!==l[0]||s[1]!==l[1]},e.prototype._updateView=function(t){var e=this._displayables,n=this._handleEnds,i=Wo(n.slice()),r=this._size;B([0,1],function(t){var i=e.handles[t],o=this._handleHeight;i.attr({scaleX:o/2,scaleY:o/2,x:n[t]+(t?-1:1),y:r[1]/2-o/2})},this),e.filler.setShape({x:i[0],y:0,width:i[1]-i[0],height:r[1]});var o={x:i[0],width:i[1]-i[0]};e.moveHandle&&(e.moveHandle.setShape(o),e.moveZone.setShape(o),e.moveZone.getBoundingRect(),e.moveHandleIcon&&e.moveHandleIcon.attr("x",o.x+o.width/2));for(var a=e.dataShadowSegs,s=[0,i[0],i[1],r[0]],l=0;le[0]||n[1]<0||n[1]>e[1])){var i=this._handleEnds,r=(i[0]+i[1])/2,o=this._updateInterval("all",n[0]-r);this._updateView(),o&&this._dispatchZoomAction(!1)}},e.prototype._onBrushStart=function(t){var e=t.offsetX,n=t.offsetY;this._brushStart=new Pe(e,n),this._brushing=!0,this._brushStartTime=+new Date},e.prototype._onBrushEnd=function(t){if(this._brushing){var e=this._displayables.brushRect;if(this._brushing=!1,e){e.attr("ignore",!0);var n=e.shape;if(!(+new Date-this._brushStartTime<200&&Math.abs(n.width)<5)){var i=this._getViewExtent(),r=[0,100],o=this._handleEnds=[n.x,n.x+n.width],a=this.dataZoomModel.findRepresentativeAxisProxy().getMinMaxSpan();ez(0,o,i,0,null!=a.minSpan?Bo(a.minSpan,r,i,!0):null,null!=a.maxSpan?Bo(a.maxSpan,r,i,!0):null),this._range=Wo([Bo(o[0],i,r,!0),Bo(o[1],i,r,!0)]),this._updateView(),this._dispatchZoomAction(!1)}}}},e.prototype._onBrush=function(t){this._brushing&&(ye(t.event),this._updateBrushRect(t.offsetX,t.offsetY))},e.prototype._updateBrushRect=function(t,e){var n=this._displayables,i=this.dataZoomModel,r=n.brushRect;r||(r=n.brushRect=new FZ({silent:!0,style:i.getModel("brushStyle").getItemStyle()}),n.sliderGroup.add(r)),r.attr("ignore",!1);var o=this._brushStart,a=this._displayables.sliderGroup,s=a.transformCoordToLocal(t,e),l=a.transformCoordToLocal(o.x,o.y),u=this._size;s[0]=Math.max(Math.min(u[0],s[0]),0),r.setShape({x:l[0],y:0,width:s[0]-l[0],height:u[1]})},e.prototype._dispatchZoomAction=function(t){var e=this._range;this.api.dispatchAction({type:"dataZoom",from:this.uid,dataZoomId:this.dataZoomModel.id,animation:t?YZ:null,start:e[0],end:e[1]})},e.prototype._findCoordRect=function(){var t,e=bU(this.dataZoomModel).infoList;if(!t&&e.length){var n=e[0].model.coordinateSystem;t=n.getRect&&n.getRect()}if(!t){var i=this.api.getWidth(),r=this.api.getHeight();t={x:.2*i,y:.2*r,width:.6*i,height:.6*r}}return t},e.type="dataZoom.slider",e}(AU);function ZZ(t,e,n,i){var r=t.get("labelFormatter"),o=t.get("labelPrecision");null!=o&&"auto"!==o||(o=n.valuePrecision);var a=n.value[e],s=null==a||isNaN(a)?"":vw(i)||fw(i)?i.getLabel({value:Math.round(a)}):isFinite(o)?Ho(a,o,!0):a+"";return Z(r)?r(a,s):j(r)?r.replace("{value}",s):s}function jZ(t){t.registerComponentModel(GZ),t.registerComponentView(XZ),RU(t)}var qZ=function(t,e,n){var i=C((KZ[t]||{})[e]);return n&&X(i)?i[i.length-1]:i},KZ={color:{active:["#006edd","#e0ffff"],inactive:[ug.color.transparent]},colorHue:{active:[0,360],inactive:[0,0]},colorSaturation:{active:[.3,1],inactive:[0,0]},colorLightness:{active:[.9,.5],inactive:[0,0]},colorAlpha:{active:[.3,1],inactive:[0,0]},opacity:{active:[.3,1],inactive:[0,0]},symbol:{active:["circle","roundRect","diamond"],inactive:["none"]},symbolSize:{active:[10,50],inactive:[0,0]}},$Z=ZR.mapVisual,JZ=ZR.eachVisual,QZ=X,tj=B,ej=Wo,nj=Bo,ij=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.stateList=["inRange","outOfRange"],n.replacableOptionKeys=["inRange","outOfRange","target","controller","color"],n.layoutMode={type:"box",ignoreSize:!0},n.dataBound=[-1/0,1/0],n.targetVisuals={},n.controllerVisuals={},n}return n(e,t),e.prototype.init=function(t,e,n){this.mergeDefaultAndTheme(t,n)},e.prototype.optionUpdated=function(t,e){var n=this.option;!e&&JY(n,t,this.replacableOptionKeys),this.textStyleModel=this.getModel("textStyle"),this.resetItemSize(),this.completeVisualOption()},e.prototype.resetVisual=function(t){var e=this.stateList;t=U(t,this),this.controllerVisuals=$Y(this.option.controller,e,t),this.targetVisuals=$Y(this.option.target,e,t)},e.prototype.getItemSymbol=function(){return null},e.prototype.getTargetSeriesIndices=function(){var t=this,e=this.option.seriesTargets;if(e){var n=[];return tj(e,function(e){if(null!=e.seriesIndex)n.push(e.seriesIndex);else if(null!=e.seriesId){var i;t.ecModel.eachSeries(function(t){t.id===e.seriesId&&(i=t)}),i&&n.push(i.componentIndex)}}),n}var i=this.option.seriesId,r=this.option.seriesIndex;return null==r&&null==i&&(r="all"),V(Ba(this.ecModel,"series",{index:r,id:i},{useDefault:!1,enableAll:!0,enableNone:!1}).models,function(t){return t.componentIndex})},e.prototype.eachTargetSeries=function(t,e){B(this.getTargetSeriesIndices(),function(n){var i=this.ecModel.getSeriesByIndex(n);i&&t.call(e,i)},this)},e.prototype.isTargetSeries=function(t){var e=!1;return this.eachTargetSeries(function(n){n===t&&(e=!0)}),e},e.prototype.formatValueText=function(t,e,n){var i,r=this.option,o=r.precision,a=this.dataBound,s=r.formatter;n=n||["<",">"],X(t)&&(t=t.slice(),i=!0);var l=e?t:i?[u(t[0]),u(t[1])]:u(t);return j(s)?s.replace("{value}",i?l[0]:l).replace("{value2}",i?l[1]:l):Z(s)?i?s(t[0],t[1]):s(t):i?t[0]===a[0]?n[0]+" "+l[1]:t[1]===a[1]?n[1]+" "+l[0]:l[0]+" - "+l[1]:l;function u(t){return t===a[0]?"min":t===a[1]?"max":(+t).toFixed(Math.min(o,20))}},e.prototype.resetExtent=function(){var t=this.option,e=ej([t.min,t.max]);this._dataExtent=e},e.prototype.getDimension=function(t){var e=this,n=this.option.seriesTargets;if(n){var i=H(n,function(n){return null!=n.seriesIndex&&n.seriesIndex===t||null!=n.seriesId&&n.seriesId===e.ecModel.getSeriesByIndex(t).id});if(i)return i.dimension}return this.option.dimension},e.prototype.getDataDimensionIndex=function(t){var e=t.hostModel.seriesIndex,n=this.getDimension(e);if(null!=n)return t.getDimensionIndex(n);for(var i=t.dimensions,r=i.length-1;r>=0;r--){var o=i[r],a=t.getDimensionInfo(o);if(!a.isCalculationCoord)return a.storeDimIndex}},e.prototype.getExtent=function(){return this._dataExtent.slice()},e.prototype.completeVisualOption=function(){var t=this.ecModel,e=this.option,n={inRange:e.inRange,outOfRange:e.outOfRange},i=e.target||(e.target={}),r=e.controller||(e.controller={});D(i,n),D(r,n);var o=this.isCategory();function a(n){QZ(e.color)&&!n.inRange&&(n.inRange={color:e.color.slice().reverse()}),n.inRange=n.inRange||{color:t.get("gradientColor")}}a.call(this,i),a.call(this,r),function(t,e,n){var i=t[e],r=t[n];i&&!r&&(r=t[n]={},tj(i,function(t,e){if(ZR.isValidType(e)){var n=qZ(e,"inactive",o);null!=n&&(r[e]=n,"color"!==e||r.hasOwnProperty("opacity")||r.hasOwnProperty("colorAlpha")||(r.opacity=[0,0]))}}))}.call(this,i,"inRange","outOfRange"),function(t){var e=(t.inRange||{}).symbol||(t.outOfRange||{}).symbol,n=(t.inRange||{}).symbolSize||(t.outOfRange||{}).symbolSize,i=this.get("inactiveColor"),r=this.getItemSymbol()||"roundRect";tj(this.stateList,function(a){var s=this.itemSize,l=t[a];l||(l=t[a]={color:o?i:[i]}),null==l.symbol&&(l.symbol=e&&C(e)||(o?r:[r])),null==l.symbolSize&&(l.symbolSize=n&&C(n)||(o?s[0]:[s[0],s[0]])),l.symbol=$Z(l.symbol,function(t){return"none"===t?r:t});var u=l.symbolSize;if(null!=u){var c=-1/0;JZ(u,function(t){t>c&&(c=t)}),l.symbolSize=$Z(u,function(t){return nj(t,[0,c],[0,s[0]],!0)})}},this)}.call(this,r)},e.prototype.resetItemSize=function(){this.itemSize=[parseFloat(this.get("itemWidth")),parseFloat(this.get("itemHeight"))]},e.prototype.isCategory=function(){return!!this.option.categories},e.prototype.setSelected=function(t){},e.prototype.getSelected=function(){return null},e.prototype.getValueState=function(t){return null},e.prototype.getVisualMeta=function(t){return null},e.type="visualMap",e.dependencies=["series"],e.defaultOption={show:!0,z:4,min:0,max:200,left:0,right:null,top:null,bottom:0,itemWidth:null,itemHeight:null,inverse:!1,orient:"vertical",backgroundColor:ug.color.transparent,borderColor:ug.color.borderTint,contentColor:ug.color.theme[0],inactiveColor:ug.color.disabled,borderWidth:0,padding:ug.size.m,textGap:10,precision:0,textStyle:{color:ug.color.secondary}},e}(lg),rj=[20,140],oj=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.optionUpdated=function(e,n){t.prototype.optionUpdated.apply(this,arguments),this.resetExtent(),this.resetVisual(function(t){t.mappingMethod="linear",t.dataExtent=this.getExtent()}),this._resetRange()},e.prototype.resetItemSize=function(){t.prototype.resetItemSize.apply(this,arguments);var e=this.itemSize;(null==e[0]||isNaN(e[0]))&&(e[0]=rj[0]),(null==e[1]||isNaN(e[1]))&&(e[1]=rj[1])},e.prototype._resetRange=function(){var t=this.getExtent(),e=this.option.range;!e||e.auto?(t.auto=1,this.option.range=t):X(e)&&(e[0]>e[1]&&e.reverse(),e[0]=Math.max(e[0],t[0]),e[1]=Math.min(e[1],t[1]))},e.prototype.completeVisualOption=function(){t.prototype.completeVisualOption.apply(this,arguments),B(this.stateList,function(t){var e=this.option.controller[t].symbolSize;e&&e[0]!==e[1]&&(e[0]=e[1]/3)},this)},e.prototype.setSelected=function(t){this.option.range=t.slice(),this._resetRange()},e.prototype.getSelected=function(){var t=this.getExtent(),e=Wo((this.get("range")||[]).slice());return e[0]>t[1]&&(e[0]=t[1]),e[1]>t[1]&&(e[1]=t[1]),e[0]=n[1]||t<=e[1])?"inRange":"outOfRange"},e.prototype.findTargetDataIndices=function(t){var e=[];return this.eachTargetSeries(function(n){var i=[],r=n.getData();r.each(this.getDataDimensionIndex(r),function(e,n){t[0]<=e&&e<=t[1]&&i.push(n)},this),e.push({seriesId:n.id,dataIndex:i})},this),e},e.prototype.getVisualMeta=function(t){var e=aj(this,"outOfRange",this.getExtent()),n=aj(this,"inRange",this.option.range.slice()),i=[];function r(e,n){i.push({value:e,color:t(e,n)})}for(var o=0,a=0,s=n.length,l=e.length;at[1])break;n.push({color:this.getControllerVisual(o,"color",e),offset:r/100})}return n.push({color:this.getControllerVisual(t[1],"color",e),offset:1}),n},e.prototype._createBarPoints=function(t,e){var n=this.visualMapModel.itemSize;return[[n[0]-e[0],t[0]],[n[0],t[0]],[n[0],t[1]],[n[0]-e[1],t[1]]]},e.prototype._createBarGroup=function(t){var e=this._orient,n=this.visualMapModel.get("inverse");return new vo("horizontal"!==e||n?"horizontal"===e&&n?{scaleX:"bottom"===t?-1:1,rotation:-Math.PI/2}:"vertical"!==e||n?{scaleX:"left"===t?1:-1}:{scaleX:"left"===t?1:-1,scaleY:-1}:{scaleX:"bottom"===t?1:-1,rotation:Math.PI/2})},e.prototype._updateHandle=function(t,e){if(this._useHandle){var n=this._shapes,i=this.visualMapModel,r=n.handleThumbs,o=n.handleLabels,a=i.itemSize,s=i.getExtent(),l=this._applyTransform("left",n.mainGroup);dj([0,1],function(u){var c=r[u];c.setStyle("fill",e.handlesColor[u]),c.y=t[u];var h=hj(t[u],[0,a[1]],s,!0),d=this.getControllerVisual(h,"symbolSize");c.scaleX=c.scaleY=d/a[0],c.x=a[0]-d/2;var p=Cd(n.handleLabelPoints[u],Td(c,this.group));if("horizontal"===this._orient){var f="left"===l||"top"===l?(a[0]-d)/2:(a[0]-d)/-2;p[1]+=f}o[u].setStyle({x:p[0],y:p[1],text:i.formatValueText(this._dataInterval[u]),verticalAlign:"middle",align:"vertical"===this._orient?this._applyTransform("left",n.mainGroup):"center"})},this)}},e.prototype._showIndicator=function(t,e,n,i){var r=this.visualMapModel,o=r.getExtent(),a=r.itemSize,s=[0,a[1]],l=this._shapes,u=l.indicator;if(u){u.attr("invisible",!1);var c=this.getControllerVisual(t,"color",{convertOpacityToAlpha:!0}),h=this.getControllerVisual(t,"symbolSize"),d=hj(t,o,s,!0),p=a[0]-h/2,f={x:u.x,y:u.y};u.y=d,u.x=p;var g=Cd(l.indicatorLabelPoint,Td(u,this.group)),v=l.indicatorLabel;v.attr("invisible",!1);var y=this._applyTransform("left",l.mainGroup),m="horizontal"===this._orient;v.setStyle({text:(n||"")+r.formatValueText(e),verticalAlign:m?y:"middle",align:m?"center":y});var x={x:p,y:d,style:{fill:c}},_={style:{x:g[0],y:g[1]}};if(r.ecModel.isAnimationEnabled()&&!this._firstShowIndicator){var b={duration:100,easing:"cubicInOut",additive:!0};u.x=f.x,u.y=f.y,u.animateTo(x,b),v.animateTo(_,b)}else u.attr(x),v.attr(_);this._firstShowIndicator=!1;var w=this._shapes.handleLabels;if(w)for(var S=0;Sr[1]&&(u[1]=1/0),e&&(u[0]===-1/0?this._showIndicator(l,u[1],"< ",a):u[1]===1/0?this._showIndicator(l,u[0],"> ",a):this._showIndicator(l,l,"≈ ",a));var c=this._hoverLinkDataIndices,h=[];(e||yj(n))&&(h=this._hoverLinkDataIndices=n.findTargetDataIndices(u));var d=function(t,e){var n={},i={};return r(t||[],n),r(e||[],i,n),[o(n),o(i)];function r(t,e,n){for(var i=0,r=t.length;i=0&&(r.dimension=o,i.push(r))}}),t.getData().setVisual("visualMeta",i)}}];function wj(t,e,n,i){for(var r=e.targetVisuals[i],o=ZR.prepareVisualTypes(r),a={color:qm(t.getData(),"color")},s=0,l=o.length;s0:t.splitNumber>0)&&!t.calculable?"piecewise":"continuous"}),t.registerAction(xj,_j),B(bj,function(e){t.registerVisual(t.PRIORITY.VISUAL.COMPONENT,e)}),t.registerPreprocessor(Mj))}function Dj(t){t.registerComponentModel(oj),t.registerComponentView(gj),Cj(t)}var Aj=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n._pieceList=[],n}return n(e,t),e.prototype.optionUpdated=function(e,n){t.prototype.optionUpdated.apply(this,arguments),this.resetExtent();var i=this._mode=this._determineMode();this._pieceList=[],kj[this._mode].call(this,this._pieceList),this._resetSelected(e,n);var r=this.option.categories;this.resetVisual(function(t,e){"categories"===i?(t.mappingMethod="category",t.categories=C(r)):(t.dataExtent=this.getExtent(),t.mappingMethod="piecewise",t.pieceList=V(this._pieceList,function(t){return t=C(t),"inRange"!==e&&(t.visual=null),t}))})},e.prototype.completeVisualOption=function(){var e=this.option,n={},i=ZR.listVisualTypes(),r=this.isCategory();function o(t,e,n){return t&&t[e]&&t[e].hasOwnProperty(n)}B(e.pieces,function(t){B(i,function(e){t.hasOwnProperty(e)&&(n[e]=1)})}),B(n,function(t,n){var i=!1;B(this.stateList,function(t){i=i||o(e,t,n)||o(e.target,t,n)},this),!i&&B(this.stateList,function(t){(e[t]||(e[t]={}))[n]=qZ(n,"inRange"===t?"active":"inactive",r)})},this),t.prototype.completeVisualOption.apply(this,arguments)},e.prototype._resetSelected=function(t,e){var n=this.option,i=this._pieceList,r=(e?n:t).selected||{};if(n.selected=r,B(i,function(t,e){var n=this.getSelectedMapKey(t);r.hasOwnProperty(n)||(r[n]=!0)},this),"single"===n.selectedMode){var o=!1;B(i,function(t,e){var n=this.getSelectedMapKey(t);r[n]&&(o?r[n]=!1:o=!0)},this)}},e.prototype.getItemSymbol=function(){return this.get("itemSymbol")},e.prototype.getSelectedMapKey=function(t){return"categories"===this._mode?t.value+"":t.index+""},e.prototype.getPieceList=function(){return this._pieceList},e.prototype._determineMode=function(){var t=this.option;return t.pieces&&t.pieces.length>0?"pieces":this.option.categories?"categories":"splitNumber"},e.prototype.setSelected=function(t){this.option.selected=C(t)},e.prototype.getValueState=function(t){var e=ZR.findPieceIndex(t,this._pieceList);return null!=e&&this.option.selected[this.getSelectedMapKey(this._pieceList[e])]?"inRange":"outOfRange"},e.prototype.findTargetDataIndices=function(t){var e=[],n=this._pieceList;return this.eachTargetSeries(function(i){var r=[],o=i.getData();o.each(this.getDataDimensionIndex(o),function(e,i){ZR.findPieceIndex(e,n)===t&&r.push(i)},this),e.push({seriesId:i.id,dataIndex:r})},this),e},e.prototype.getRepresentValue=function(t){var e;if(this.isCategory())e=t.value;else if(null!=t.value)e=t.value;else{var n=t.interval||[];e=n[0]===-1/0&&n[1]===1/0?0:(n[0]+n[1])/2}return e},e.prototype.getVisualMeta=function(t){if(!this.isCategory()){var e=[],n=["",""],i=this,r=this._pieceList.slice();if(r.length){var o=r[0].interval[0];o!==-1/0&&r.unshift({interval:[-1/0,o]}),(o=r[r.length-1].interval[1])!==1/0&&r.push({interval:[o,1/0]})}else r.push({interval:[-1/0,1/0]});var a=-1/0;return B(r,function(t){var e=t.interval;e&&(e[0]>a&&s([a,e[0]],"outOfRange"),s(e.slice()),a=e[1])},this),{stops:e,outerColors:n}}function s(r,o){var a=i.getRepresentValue({interval:r});o||(o=i.getValueState(a));var s=t(a,o);r[0]===-1/0?n[0]=s:r[1]===1/0?n[1]=s:e.push({value:r[0],color:s},{value:r[1],color:s})}},e.type="visualMap.piecewise",e.defaultOption=Op(ij.defaultOption,{selected:null,minOpen:!1,maxOpen:!1,align:"auto",itemWidth:20,itemHeight:14,itemSymbol:"roundRect",pieces:null,categories:null,splitNumber:5,selectedMode:"multiple",itemGap:10,hoverLink:!0}),e}(ij),kj={splitNumber:function(t){var e=this.option,n=Math.min(e.precision,20),i=this.getExtent(),r=e.splitNumber;r=Math.max(parseInt(r,10),1),e.splitNumber=r;for(var o=(i[1]-i[0])/r;+o.toFixed(n)!==o&&n<5;)n++;e.precision=n,o=+o.toFixed(n),e.minOpen&&t.push({interval:[-1/0,i[0]],close:[0,0]});for(var a=0,s=i[0];a","≥"][e[0]]];t.text=t.text||this.formatValueText(null!=t.value?t.value:t.interval,!1,n)},this)}};function Lj(t,e){var n=t.inverse;("vertical"===t.orient?!n:n)&&e.reverse()}var Pj=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.doRender=function(){var t=this.group;t.removeAll();var e=this.visualMapModel,n=e.get("textGap"),i=e.textStyleModel,r=this._getItemAlign(),o=e.itemSize,a=this._getViewData(),s=a.endsText,l=ot(e.get("showLabel",!0),!s),u=!e.get("selectedMode");s&&this._renderEndsText(t,s[0],o,l,r),B(a.viewPieceList,function(a){var s=a.piece,c=new vo;c.onclick=U(this._onItemClick,this,s),this._enableHoverLink(c,a.indexInModelPieceList);var h=e.getRepresentValue(s);if(this._createItemSymbol(c,h,[0,0,o[0],o[1]],u),l){var d=this.visualMapModel.getValueState(h),p=i.get("align")||r;c.add(new du({style:sp(i,{x:"right"===p?-n:o[0]+n,y:o[1]/2,text:s.text,verticalAlign:i.get("verticalAlign")||"middle",align:p,opacity:at(i.get("opacity"),"outOfRange"===d?.5:1)}),silent:u}))}t.add(c)},this),s&&this._renderEndsText(t,s[1],o,l,r),qf(e.get("orient"),t,e.get("itemGap")),this.renderBackground(t),this.positionGroup(t)},e.prototype._enableHoverLink=function(t,e){var n=this;t.on("mouseover",function(){return i("highlight")}).on("mouseout",function(){return i("downplay")});var i=function(t){var i=n.visualMapModel;i.option.hoverLink&&n.api.dispatchAction({type:t,batch:cj(i.findTargetDataIndices(e),i)})}},e.prototype._getItemAlign=function(){var t=this.visualMapModel,e=t.option;if("vertical"===e.orient)return uj(t,this.api,t.itemSize);var n=e.align;return n&&"auto"!==n||(n="left"),n},e.prototype._renderEndsText=function(t,e,n,i,r){if(e){var o=new vo,a=this.visualMapModel.textStyleModel;o.add(new du({style:sp(a,{x:i?"right"===r?n[0]:0:n[0]/2,y:n[1]/2,verticalAlign:"middle",align:i?r:"center",text:e})})),t.add(o)}},e.prototype._getViewData=function(){var t=this.visualMapModel,e=V(t.getPieceList(),function(t,e){return{piece:t,indexInModelPieceList:e}}),n=t.get("text"),i=t.get("orient"),r=t.get("inverse");return("horizontal"===i?r:!r)?e.reverse():n&&(n=n.slice().reverse()),{viewPieceList:e,endsText:n}},e.prototype._createItemSymbol=function(t,e,n,i){var r=xx(this.getControllerVisual(e,"symbol"),n[0],n[1],n[2],n[3],this.getControllerVisual(e,"color"));r.silent=i,t.add(r)},e.prototype._onItemClick=function(t){var e=this.visualMapModel,n=e.option,i=n.selectedMode;if(i){var r=C(n.selected),o=e.getSelectedMapKey(t);"single"===i||!0===i?(r[o]=!0,B(r,function(t,e){r[e]=e===o})):r[o]=!r[o],this.api.dispatchAction({type:"selectDataRange",from:this.uid,visualMapId:this.visualMapModel.id,selected:r})}},e.type="visualMap.piecewise",e}(sj);function Oj(t){t.registerComponentModel(Aj),t.registerComponentView(Pj),Cj(t)}var Rj=function(){function t(t){this._thumbnailModel=t}return t.prototype.reset=function(t){this._renderVersion=t.getECUpdateCycleVersion()},t.prototype.renderContent=function(t){var e=t.api.getViewOfComponentModel(this._thumbnailModel);e&&(t.group.silent=!0,e.renderContent({group:t.group,targetTrans:t.targetTrans,z2Range:jd(t.group),roamType:t.roamType,viewportRect:t.viewportRect,renderVersion:this._renderVersion}))},t.prototype.updateWindow=function(t,e){var n=e.getViewOfComponentModel(this._thumbnailModel);n&&n.updateWindow({targetTrans:t,renderVersion:this._renderVersion})},t}(),Nj=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n.preventAutoZ=!0,n}return n(e,t),e.prototype.optionUpdated=function(t,e){this._updateBridge()},e.prototype._updateBridge=function(){var t=this._birdge=this._birdge||new Rj(this);(this._target=null,this.ecModel.eachSeries(function(t){dE(t,null)}),this.shouldShow())&&dE(this.getTarget().baseMapProvider,t)},e.prototype.shouldShow=function(){return this.getShallow("show",!0)},e.prototype.getBridge=function(){return this._birdge},e.prototype.getTarget=function(){if(this._target)return this._target;var t=this.getReferringComponents("series",{useDefault:!1,enableAll:!1,enableNone:!1}).models[0];return t?"graph"!==t.subType&&(t=null):t=this.ecModel.queryComponents({mainType:"series",subType:"graph"})[0],this._target={baseMapProvider:t},this._target},e.type="thumbnail",e.layoutMode="box",e.dependencies=["series","geo"],e.defaultOption={show:!0,right:1,bottom:1,height:"25%",width:"25%",itemStyle:{borderColor:ug.color.border,borderWidth:2},windowStyle:{borderWidth:1,color:ug.color.neutral30,borderColor:ug.color.neutral40,opacity:.3},z:10},e}(lg),Ej=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.render=function(t,e,n){if(this._api=n,this._model=t,this._coordSys||(this._coordSys=new nP),this._isEnabled()){this._renderVersion=n.getECUpdateCycleVersion();var i=this.group;i.removeAll();var r=t.getModel("itemStyle"),o=r.getItemStyle();null==o.fill&&(o.fill=e.get("backgroundColor")||ug.color.neutral00);var a=eg(t,n).refContainer,s=Jf(Kf(t,!0),a),l=o.lineWidth||0,u=this._contentRect=zd(s.clone(),l/2,!0,!0),c=new vo;i.add(c),c.setClipPath(new lu({shape:u.plain()}));var h=this._targetGroup=new vo;c.add(h);var d=s.plain();d.r=r.getShallow("borderRadius",!0),i.add(this._bgRect=new lu({style:o,shape:d,silent:!1,cursor:"grab"}));var p=t.getModel("windowStyle"),f=p.getShallow("borderRadius",!0);c.add(this._windowRect=new lu({shape:{x:0,y:0,width:0,height:0,r:f},style:p.getItemStyle(),silent:!1,cursor:"grab"})),this._dealRenderContent(),this._dealUpdateWindow(),Bj(t,this)}else this._clear()},e.prototype.renderContent=function(t){this._bridgeRendered=t,this._isEnabled()&&(this._dealRenderContent(),this._dealUpdateWindow(),Bj(this._model,this))},e.prototype._dealRenderContent=function(){var t=this._bridgeRendered;if(t&&t.renderVersion===this._renderVersion){var e=this._targetGroup,n=this._coordSys,i=this._contentRect;if(e.removeAll(),t){var r=t.group,o=r.getBoundingRect();e.add(r),this._bgRect.z2=t.z2Range.min-10,pP(n,o.x,o.y,o.width,o.height);var a=Jf({left:"center",top:"center",aspect:o.width/o.height},i);fP(n,a.x,a.y,a.width,a.height),lP(r,n,0),r.dirty(),this._windowRect.z2=t.z2Range.max+10,this._resetRoamController(t.roamType)}}},e.prototype.updateWindow=function(t){var e=this._bridgeRendered;e&&e.renderVersion===t.renderVersion&&(e.targetTrans=t.targetTrans),this._isEnabled()&&this._dealUpdateWindow()},e.prototype._dealUpdateWindow=function(){var t=this._bridgeRendered;if(t&&t.renderVersion===this._renderVersion){var e=Ae([],t.targetTrans),n=Ie([],rP(null,this._coordSys),e);this._transThisToTarget=Ae([],n);var i=t.viewportRect;(i=i?i.clone():new Xe(0,0,this._api.getWidth(),this._api.getHeight())).applyTransform(n);var r=this._windowRect,o=r.shape.r;r.setShape(P({r:o},i))}},e.prototype._resetRoamController=function(t){var e=this,n=this._api,i=this._roamController;i||(i=this._roamController=new uL(n.getZr())),t&&this._isEnabled()?(i.enable(t,{api:n,zInfo:{component:this._model},triggerInfo:{roamTrigger:null,isInSelf:function(t,n,i){return e._contentRect.contain(n,i)}}}),i.off("pan").off("zoom").on("pan",U(this._onPan,this)).on("zoom",U(this._onZoom,this))):i.disable()},e.prototype._onPan=function(t){var e=this._transThisToTarget;if(this._isEnabled()&&e){var n=Yt([],[t.oldX,t.oldY],e),i=Yt([],[t.oldX-t.dx,t.oldY-t.dy],e);this._api.dispatchAction(zj(this._model.getTarget().baseMapProvider,{dx:i[0]-n[0],dy:i[1]-n[1]}))}},e.prototype._onZoom=function(t){var e=this._transThisToTarget;if(this._isEnabled()&&e){var n=Yt([],[t.originX,t.originY],e);this._api.dispatchAction(zj(this._model.getTarget().baseMapProvider,{zoom:1/t.scale,originX:n[0],originY:n[1]}))}},e.prototype._isEnabled=function(){var t=this._model;return!(!t||!t.shouldShow())&&!!t.getTarget().baseMapProvider},e.prototype._clear=function(){this.group.removeAll(),this._bridgeRendered=null,this._roamController&&this._roamController.disable()},e.prototype.remove=function(){this._clear()},e.prototype.dispose=function(){this._clear()},e.type="thumbnail",e}(Ky);function zj(t,e){var n={type:"series"===t.mainType?t.subType+"Roam":t.mainType+"Roam"};return n[t.mainType+"Id"]=t.id,k(n,e),n}function Bj(t,e){var n=Zd(t);qd(e.group,n.z,n.zlevel)}var Vj={label:{enabled:!0},decal:{show:!1}},Gj=Pa(),Fj=Pa(),Hj=as(function(t,e){var n=t.getModel("aria");if(!n.get("enabled"))return;var i=Fj(t).scope||(Fj(t).scope={}),r=C(Vj);function o(t,e){if(!j(t))return t;var n=t;return B(e,function(t,e){n=n.replace(new RegExp("\\{\\s*"+e+"\\s*\\}","g"),t)}),n}D(r.label,t.getLocaleModel().get("aria"),!1),D(n.option,r,!1),function(){if(n.getModel("decal").get("show")){var e=mt();t.eachSeries(function(t){t.isColorBySeries()||(Gj(t).scope=e.get(t.type)||e.set(t.type,{}))}),t.eachSeries(function(e){if(Z(e.enableAriaDecal))e.enableAriaDecal();else{var n=e.getData();if(e.isColorBySeries()){var r=Rg(e.ecModel,e.name,i,t.getSeriesCount()),o=n.getVisual("decal");n.setVisual("decal",c(o,r))}else{var a=e.getRawData(),s={},l=Gj(e).scope;n.each(function(t){var e=n.getRawIndex(t);s[e]=t});var u=a.count();a.each(function(t){var i=s[t],r=a.getName(t)||t+"",o=Rg(e.ecModel,r,l,u),h=n.getItemVisual(i,"decal");n.setItemVisual(i,"decal",c(h,o))})}}function c(t,e){var n=t?k(k({},e),t):e;return n.dirty=!0,n}})}}(),function(){var i=e.getZr().dom;if(i){var r=t.getLocaleModel().get("aria"),a=n.getModel("label");if(a.option=P(a.option,r),a.get("enabled"))if(i.setAttribute("role","img"),a.get("description"))i.setAttribute("aria-label",a.get("description"));else{var s,l=t.getSeriesCount(),u=a.get(["data","maxCount"])||10,c=a.get(["series","maxCount"])||10,h=Math.min(l,c);if(!(l<1)){var d=function(){var e=t.get("title");return e&&e.length&&(e=e[0]),e&&e.text}();s=d?o(a.get(["general","withTitle"]),{title:d}):a.get(["general","withoutTitle"]);var p=[];s+=o(l>1?a.get(["series","multiple","prefix"]):a.get(["series","single","prefix"]),{seriesCount:l}),t.eachSeries(function(e,n){if(n1?a.get(["series","multiple",r]):a.get(["series","single",r]),{seriesId:e.seriesIndex,seriesName:e.get("name"),seriesType:(_=e.subType,b=t.getLocaleModel().get(["series","typeNames"]),b[_]||b.chart)});var s=e.getData();s.count()>u?i+=o(a.get(["data","partialData"]),{displayCnt:u}):i+=a.get(["data","allData"]);for(var c=a.get(["data","separator","middle"]),d=a.get(["data","separator","end"]),f=a.get(["data","excludeDimensionId"]),g=[],v=0;v":"gt",">=":"gte","=":"eq","!=":"ne","<>":"ne"},Yj=function(){function t(t){if(null==(this._condVal=j(t)?new RegExp(t):it(t)?t:null)){var e="";0,va(e)}}return t.prototype.evaluate=function(t){var e=typeof t;return j(e)?this._condVal.test(t):!!K(e)&&this._condVal.test(t+"")},t}(),Xj=function(){function t(){}return t.prototype.evaluate=function(){return this.value},t}(),Zj=function(){function t(){}return t.prototype.evaluate=function(){for(var t=this.children,e=0;e2&&l.push(e),e=[t,n]}function f(t,n,i,r){aq(t,i)&&aq(n,r)||e.push(t,n,i,r,i,r)}function g(t,n,i,r,o,a){var s=Math.abs(n-t),l=4*Math.tan(s/4)/3,u=nM:C2&&l.push(e),l}function lq(t,e,n,i,r,o,a,s,l,u){if(aq(t,n)&&aq(e,i)&&aq(r,a)&&aq(o,s))l.push(a,s);else{var c=2/u,h=c*c,d=a-t,p=s-e,f=Math.sqrt(d*d+p*p);d/=f,p/=f;var g=n-t,v=i-e,y=r-a,m=o-s,x=g*g+v*v,_=y*y+m*m;if(x=0&&_-w*w=0)l.push(a,s);else{var S=[],M=[];Hn(t,n,r,a,.5,S),Hn(e,i,o,s,.5,M),lq(S[0],M[0],S[1],M[1],S[2],M[2],S[3],M[3],l,u),lq(S[4],M[4],S[5],M[5],S[6],M[6],S[7],M[7],l,u)}}}}function uq(t,e,n){var i=t[e],r=t[1-e],o=Math.abs(i/r),a=Math.ceil(Math.sqrt(o*n)),s=Math.floor(n/a);0===s&&(s=1,a=n);for(var l=[],u=0;u0)for(u=0;uMath.abs(u),h=uq([l,u],c?0:1,e),d=(c?s:u)/h.length,p=0;p1?null:new Pe(p*l+t,p*u+e)}function pq(t,e,n){var i=new Pe;Pe.sub(i,n,e),i.normalize();var r=new Pe;return Pe.sub(r,t,e),r.dot(i)}function fq(t,e){var n=t[t.length-1];n&&n[0]===e[0]&&n[1]===e[1]||t.push(e)}function gq(t){var e=t.points,n=[],i=[];rl(e,n,i);var r=new Xe(n[0],n[1],i[0]-n[0],i[1]-n[1]),o=r.width,a=r.height,s=r.x,l=r.y,u=new Pe,c=new Pe;return o>a?(u.x=c.x=s+o/2,u.y=l,c.y=l+a):(u.y=c.y=l+a/2,u.x=s,c.x=s+o),function(t,e,n){for(var i=t.length,r=[],o=0;or,a=uq([i,r],o?0:1,e),s=o?"width":"height",l=o?"height":"width",u=o?"x":"y",c=o?"y":"x",h=t[s]/a.length,d=0;d0)for(var b=i/n,w=-i/2;w<=i/2;w+=b){var S=Math.sin(w),M=Math.cos(w),I=0;for(x=0;x0;l/=2){var u=0,c=0;(t&l)>0&&(u=1),(e&l)>0&&(c=1),s+=l*l*(3*u^c),0===c&&(1===u&&(t=l-1-t,e=l-1-e),a=t,t=e,e=a)}return s}function Pq(t){var e=1/0,n=1/0,i=-1/0,r=-1/0,o=V(t,function(t){var o=t.getBoundingRect(),a=t.getComputedTransform(),s=o.x+o.width/2+(a?a[4]:0),l=o.y+o.height/2+(a?a[5]:0);return e=Math.min(s,e),n=Math.min(l,n),i=Math.max(s,i),r=Math.max(l,r),[s,l]});return V(o,function(o,a){return{cp:o,z:Lq(o[0],o[1],e,n,i,r),path:t[a]}}).sort(function(t,e){return t.z-e.z}).map(function(t){return t.path})}function Oq(t){return mq(t.path,t.count)}function Rq(t){return X(t[0])}function Nq(t,e){for(var n=[],i=t.length,r=0;r=0;r--)if(!n[r].many.length){var l=n[s].many;if(l.length<=1){if(!s)return n;s=0}o=l.length;var u=Math.ceil(o/2);n[r].many=l.slice(u,o),n[s].many=l.slice(0,u),s++}return n}var Eq={clone:function(t){for(var e=[],n=1-Math.pow(1-t.path.style.opacity,1/t.count),i=0;i0){var s,l,u=i.getModel("universalTransition").get("delay"),c=k({setToFinal:!0},a);Rq(t)&&(s=t,l=e),Rq(e)&&(s=e,l=t);for(var h=s?s===t:t.length>e.length,d=s?Nq(l,s):Nq(h?e:t,[h?t:e]),p=0,f=0;f1e4))for(var r=n.getIndices(),o=0;o0&&i.group.traverse(function(t){t instanceof Kl&&!t.animators.length&&t.animateFrom({style:{opacity:0}},r)})})}function Zq(t){var e=t.getModel("universalTransition").get("seriesKey");return e||t.id}function jq(t){return X(t)?t.sort().join(","):t}function qq(t){if(t.hostModel)return t.hostModel.getModel("universalTransition").get("divideShape")}function Kq(t,e){for(var n=0;na.vmin?n+=a.vmin-i+(t-a.vmin)/(a.vmax-a.vmin)*a.gapReal:n+=t-i,i=a.vmax,r=!1;break}n+=a.vmin-i+a.gapReal,i=a.vmax}return r&&(n+=t-i),n},transformOut:function(t,e){if(e&&2===e.depth)return t;for(var n=Qq,i=tK,r=!0,o=0,a=0;al?s.vmin+(t-l)/(u-l)*(s.vmax-s.vmin):i+t-n,i=s.vmax,r=!1;break}n=u,i=s.vmax}return r&&(o=i+t-n),o}},t}();function Jq(t,e){return new $q(t,e)}var Qq=0,tK=0;function eK(t,e,n,i,r,o){"no"!==t&&B(n,function(n){var a=iK(n,o);if(a)for(var s=e.length-1;s>=0;s--){var l=e[s],u=i(l),c=3*r/4;u>a.vmin-c&&ue[0]&&n=0&&t<.99999})(s)||(s=0),r.gapParsed.type="tpPrct",r.gapParsed.val=s,o=!0}}if(!o){var l=e.parse(t.gap);(!isFinite(l)||l<0)&&(l=0),r.gapParsed.type="tpAbs",r.gapParsed.val=l}}if(r.vmin===r.vmax&&(r.gapParsed.type="tpAbs",r.gapParsed.val=0),n&&n.noNegative&&B(["vmin","vmax"],function(t){r[t]<0&&(r[t]=0)}),r.vmin>r.vmax){var u=r.vmax;r.vmax=r.vmin,r.vmin=u}i.push(r)}}),i.sort(function(t,e){return t.vmin-e.vmin});var r=-1/0;return B(i,function(t,e){r>t.vmin&&(i[e]=null),r=t.vmax}),{breaks:F(i,function(t){return!!t})}}function oK(t,e){return aK(e)===aK(t)}function aK(t){return t.start+"_\0_"+t.end}function sK(t,e,n){var i=[];B(t,function(t,n){var r=e(t);r&&"vmin"===r.type&&i.push([n])}),B(t,function(n,r){var o=e(n);if(o&&"vmax"===o.type){var a=H(i,function(n){return oK(e(t[n[0]]).parsedBreak.breakOption,o.parsedBreak.breakOption)});a&&a.push(r)}});var r=[];return B(i,function(e){2===e.length&&r.push(n?e:[t[e[0]],t[e[1]]])}),r}function lK(t,e,n,i){if(e.break){var r=e.break.parsedBreak,o=H(n,function(t){return oK(t.breakOption,e.break.parsedBreak.breakOption)}),a={lookup:i,depth:2},s={vmin:t.transformOut(r.vmin,a),vmax:t.transformOut(r.vmax,a),breakOption:r.breakOption,gapParsed:C(o.gapParsed),gapReal:r.gapReal};return{tickVal:s[e.break.type],vBreak:{type:e.break.type,parsedBreak:s}}}}function uK(t,e,n,i,r){r.original=rK(t,e,n);var o=r.transformed=rK(t,e,n),a=r.lookup;o.breaks=V(o.breaks,function(t,n){var r={depth:2},o=e.transformIn(t.vmin,r),s=e.transformIn(t.vmax,r),l={type:t.gapParsed.type,val:"tpAbs"===t.gapParsed.type?e.transformIn(t.vmin+t.gapParsed.val,r)-o:t.gapParsed.val};return a.from[i+n]=o,a.to[i+n]=t.vmin,a.from[i+n+1]=s,a.to[i+n+1]=t.vmax,{vmin:o,vmax:s,gapParsed:l,gapReal:t.gapReal,breakOption:t.breakOption}})}var cK={vmin:"start",vmax:"end"};function hK(t,e){return e&&((t=t||{}).break={type:cK[e.type],start:e.parsedBreak.vmin,end:e.parsedBreak.vmax}),t}function dK(){var t;t={createBreakScaleMapper:Jq,pruneTicksByBreak:eK,addBreaksToTicks:nK,parseAxisBreakOption:rK,identifyAxisBreak:oK,serializeAxisBreakIdentifier:aK,retrieveAxisBreakPairs:sK,getTicksBreakOutwardTransform:lK,parseAxisBreakOptionInwardTransform:uK,makeAxisLabelFormatterParamBreak:hK},Hp||(Hp=t)}var pK=Pa();function fK(t,e,n,i,r){var o=n.axis;if(!o.scale.isBlank()&&Wp()){var a=Wp().retrieveAxisBreakPairs(o.scale.getTicks({breakTicks:"only_break"}),function(t){return t.break},!1);if(a.length){var s=n.getModel("breakArea"),l=s.get("zigzagAmplitude"),u=s.get("zigzagMinSpan"),c=s.get("zigzagMaxSpan");u=Math.max(2,u||0),c=Math.max(u,c||0);var h=s.get("expandOnClick"),d=s.get("zigzagZ"),p=s.getModel("itemStyle").getItemStyle(),f=p.stroke,g=p.lineWidth,v=p.lineDash,y=p.fill,m=new vo({ignoreModelZ:!0}),x=o.isHorizontal(),_=pK(e).visualList||(pK(e).visualList=[]);B(_,function(t){return t.shouldRemove=!0});for(var b=function(t){var e=a[t][0].break.parsedBreak,s=[];s[0]=o.toGlobalCoord(o.dataToCoord(e.vmin,!0)),s[1]=o.toGlobalCoord(o.dataToCoord(e.vmax,!0)),s[1]=x;C&&(M=x);var D=[],A=[];D[h]=n,A[h]=r,T||C||(D[h]+=S?-l:l,A[h]-=S?l:-l),D[m]=M,A[m]=M,b.push(D),w.push(A);var k=void 0;if(I=0;e--)t[e].shouldRemove&&t.splice(e,1)}(_)}}}function gK(t,e,n,i){var r=t.axis,o=n.transform;ct(i.style);var a=r.getExtent();r.inverse&&(a=a.slice()).reverse();var s=V(Wp().retrieveAxisBreakPairs(r.scale.getTicks({breakTicks:"only_break"}),function(t){return t.break},!1),function(t){var e=t[0].break.parsedBreak,n=[r.dataToCoord(e.vmin,!0),r.dataToCoord(e.vmax,!0)];return n[0]>n[1]&&n.reverse(),{coordPair:n,brkId:Wp().serializeAxisBreakIdentifier(e.breakOption)}});s.sort(function(t,e){return t.coordPair[0]-e.coordPair[0]});for(var l=a[0],u=null,c=0;c=0?s[0].width:s[1].width)+u.x)/2-l.x,h=Math.min(c,c-u.x),d=Math.max(c,c-u.x);a=(c-(d<0?d:h>0?h:0))/u.x}var p=new Pe,f=new Pe;Pe.scale(p,i,-a),Pe.scale(f,i,1-a),eI(n[0],p),eI(n[1],f)}}function g(t){var e=n[0].localRect,i=new Pe(e[pd[t]]*o[0][0],e[pd[t]]*o[0][1]);return Math.abs(i.y)<1e-5}}function yK(t,e){var n={breaks:[]};return B(e.breaks,function(i){if(i){var r=H(t.get("breaks",!0),function(t){return Wp().identifyAxisBreak(t,i)});if(r){var o=e.type,a={isExpanded:!!r.isExpanded};r.isExpanded=o===ZC||o!==jC&&(o===qC?!r.isExpanded:r.isExpanded),n.breaks.push({start:r.start,end:r.end,isExpanded:!!r.isExpanded,old:a})}}}),n}function mK(){var t;t={adjustBreakLabelPair:vK,buildAxisBreakLine:gK,rectCoordBuildBreakAxis:fK,updateModelAxisBreak:yK},YC||(YC=t)}function xK(t,e){B(t,function(t){if(!t.model.get(["axisLabel","inside"])){var n=function(t){var e,n,i=t.model,r=t.scale;if(!i.get(["axisLabel","show"])||r.isBlank())return;var o=r.getExtent();n=r instanceof Mw?r.count():(e=r.getTicks()).length;var a,s=t.getLabelModel(),l=Zw(t),u=1;n>40&&(u=Math.ceil(n/40));for(var c=0;c=0&&r.push({dataGroupId:e.oldDataGroupIds[n],data:e.oldData[n],divide:qq(e.oldData[n]),groupIdDim:t.dimension})}),B(_a(t.to),function(t){var i=Kq(n.updatedSeries,t);if(i>=0){var r=n.updatedSeries[i].getData();o.push({dataGroupId:e.oldDataGroupIds[i],data:r,divide:qq(r),groupIdDim:t.dimension})}}),r.length>0&&o.length>0&&Xq(r,o,i)}(t,i,n,e)});else{var o=function(t,e){var n=mt(),i=mt(),r=mt();return B(t.oldSeries,function(e,n){var o=t.oldDataGroupIds[n],a=t.oldData[n],s=Zq(e),l=jq(s);i.set(l,{dataGroupId:o,data:a}),X(s)&&B(s,function(t){r.set(t,{key:l,dataGroupId:o,data:a})})}),B(e.updatedSeries,function(t){if(t.isUniversalTransitionEnabled()&&t.isAnimationEnabled()){var e=t.get("dataGroupId"),o=t.getData(),a=Zq(t),s=jq(a),l=i.get(s);if(l)n.set(s,{oldSeries:[{dataGroupId:l.dataGroupId,divide:qq(l.data),data:l.data}],newSeries:[{dataGroupId:e,divide:qq(o),data:o}]});else if(X(a)){var u=[];B(a,function(t){var e=i.get(t);e.data&&u.push({dataGroupId:e.dataGroupId,divide:qq(e.data),data:e.data})}),u.length&&n.set(s,{oldSeries:u,newSeries:[{dataGroupId:e,data:o,divide:qq(o)}]})}else{var c=r.get(a);if(c){var h=n.get(c.key);h||(h={oldSeries:[{dataGroupId:c.dataGroupId,data:c.data,divide:qq(c.data)}],newSeries:[]},n.set(c.key,h)),h.newSeries.push({dataGroupId:e,data:o,divide:qq(o)})}}}}),n}(i,n);B(o.keys(),function(t){var n=o.get(t);Xq(n.oldSeries,n.newSeries,e)})}B(n.updatedSeries,function(t){t[Fy]&&(t[Fy]=!1)})}for(var a=t.getSeries(),s=i.oldSeries=[],l=i.oldDataGroupIds=[],u=i.oldData=[],c=0;c