diff --git a/application/src/main/data/json/system/widget_bundles/charts.json b/application/src/main/data/json/system/widget_bundles/charts.json index 07929028ff..5421cfb607 100644 --- a/application/src/main/data/json/system/widget_bundles/charts.json +++ b/application/src/main/data/json/system/widget_bundles/charts.json @@ -8,6 +8,7 @@ "name": "Charts" }, "widgetTypeFqns": [ + "time_series_chart", "charts.basic_timeseries", "charts.state_chart", "range_chart", diff --git a/application/src/main/data/json/system/widget_types/time_series_chart.json b/application/src/main/data/json/system/widget_types/time_series_chart.json new file mode 100644 index 0000000000..b8cd24a151 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/time_series_chart.json @@ -0,0 +1,34 @@ +{ + "fqn": "time_series_chart", + "name": "Time series chart", + "deprecated": false, + "image": "tb-image:Y2hhcnQuc3Zn:IlRpbWUgc2VyaWVzIGNoYXJ0IiBzeXN0ZW0gd2lkZ2V0IGltYWdl;data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIwMCAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF80MDUzXzE4NTExMykiPgo8cGF0aCBkPSJNMi44NDc2NiAxLjI4MTI1VjdIMi4xMjVWMi4xODM1OUwwLjY2Nzk2OSAyLjcxNDg0VjIuMDYyNUwyLjczNDM4IDEuMjgxMjVIMi44NDc2NlpNOC42NzUxNyAzLjcwMzEyVjQuNTcwMzFDOC42NzUxNyA1LjAzNjQ2IDguNjMzNTEgNS40Mjk2OSA4LjU1MDE3IDUuNzVDOC40NjY4NCA2LjA3MDMxIDguMzQ3MDUgNi4zMjgxMiA4LjE5MDggNi41MjM0NEM4LjAzNDU1IDYuNzE4NzUgNy44NDU3NSA2Ljg2MDY4IDcuNjI0MzkgNi45NDkyMkM3LjQwNTY0IDcuMDM1MTYgNy4xNTgyNSA3LjA3ODEyIDYuODgyMiA3LjA3ODEyQzYuNjYzNDUgNy4wNzgxMiA2LjQ2MTYzIDcuMDUwNzggNi4yNzY3MyA2Ljk5NjA5QzYuMDkxODQgNi45NDE0MSA1LjkyNTE3IDYuODU0MTcgNS43NzY3MyA2LjczNDM4QzUuNjMwOSA2LjYxMTk4IDUuNTA1OSA2LjQ1MzEyIDUuNDAxNzMgNi4yNTc4MUM1LjI5NzU3IDYuMDYyNSA1LjIxODE0IDUuODI1NTIgNS4xNjM0NSA1LjU0Njg4QzUuMTA4NzcgNS4yNjgyMyA1LjA4MTQyIDQuOTQyNzEgNS4wODE0MiA0LjU3MDMxVjMuNzAzMTJDNS4wODE0MiAzLjIzNjk4IDUuMTIzMDkgMi44NDYzNSA1LjIwNjQyIDIuNTMxMjVDNS4yOTIzNiAyLjIxNjE1IDUuNDEzNDUgMS45NjM1NCA1LjU2OTcgMS43NzM0NEM1LjcyNTk1IDEuNTgwNzMgNS45MTM0NSAxLjQ0MjcxIDYuMTMyMiAxLjM1OTM4QzYuMzUzNTYgMS4yNzYwNCA2LjYwMDk1IDEuMjM0MzggNi44NzQzOSAxLjIzNDM4QzcuMDk1NzUgMS4yMzQzOCA3LjI5ODg3IDEuMjYxNzIgNy40ODM3NyAxLjMxNjQxQzcuNjcxMjcgMS4zNjg0OSA3LjgzNzkzIDEuNDUzMTIgNy45ODM3NyAxLjU3MDMxQzguMTI5NiAxLjY4NDkgOC4yNTMzIDEuODM4NTQgOC4zNTQ4NiAyLjAzMTI1QzguNDU5MDMgMi4yMjEzNSA4LjUzODQ1IDIuNDU0NDMgOC41OTMxNCAyLjczMDQ3QzguNjQ3ODMgMy4wMDY1MSA4LjY3NTE3IDMuMzMwNzMgOC42NzUxNyAzLjcwMzEyWk03Ljk0ODYxIDQuNjg3NVYzLjU4MjAzQzcuOTQ4NjEgMy4zMjY4MiA3LjkzMjk4IDMuMTAyODYgNy45MDE3MyAyLjkxMDE2QzcuODczMDkgMi43MTQ4NCA3LjgzMDEyIDIuNTQ4MTggNy43NzI4MyAyLjQxMDE2QzcuNzE1NTQgMi4yNzIxNCA3LjY0MjYyIDIuMTYwMTYgNy41NTQwOCAyLjA3NDIyQzcuNDY4MTQgMS45ODgyOCA3LjM2Nzg4IDEuOTI1NzggNy4yNTMzIDEuODg2NzJDNy4xNDEzMiAxLjg0NTA1IDcuMDE1MDIgMS44MjQyMiA2Ljg3NDM5IDEuODI0MjJDNi43MDI1MiAxLjgyNDIyIDYuNTUwMTcgMS44NTY3NyA2LjQxNzM2IDEuOTIxODhDNi4yODQ1NSAxLjk4NDM4IDYuMTcyNTcgMi4wODQ2NCA2LjA4MTQyIDIuMjIyNjZDNS45OTI4OCAyLjM2MDY4IDUuOTI1MTcgMi41NDE2NyA1Ljg3ODMgMi43NjU2MkM1LjgzMTQyIDIuOTg5NTggNS44MDc5OCAzLjI2MTcyIDUuODA3OTggMy41ODIwM1Y0LjY4NzVDNS44MDc5OCA0Ljk0MjcxIDUuODIyMzEgNS4xNjc5NyA1Ljg1MDk1IDUuMzYzMjhDNS44ODIyIDUuNTU4NTkgNS45Mjc3OCA1LjcyNzg2IDUuOTg3NjcgNS44NzEwOUM2LjA0NzU3IDYuMDExNzIgNi4xMjA0OCA2LjEyNzYgNi4yMDY0MiA2LjIxODc1QzYuMjkyMzYgNi4zMDk5IDYuMzkxMzIgNi4zNzc2IDYuNTAzMyA2LjQyMTg4QzYuNjE3ODggNi40NjM1NCA2Ljc0NDE4IDYuNDg0MzggNi44ODIyIDYuNDg0MzhDNy4wNTkyOSA2LjQ4NDM4IDcuMjE0MjMgNi40NTA1MiA3LjM0NzA1IDYuMzgyODFDNy40Nzk4NiA2LjMxNTEgNy41OTA1NCA2LjIwOTY0IDcuNjc5MDggNi4wNjY0MUM3Ljc3MDIyIDUuOTIwNTcgNy44Mzc5MyA1LjczNDM4IDcuODgyMiA1LjUwNzgxQzcuOTI2NDcgNS4yNzg2NSA3Ljk0ODYxIDUuMDA1MjEgNy45NDg2MSA0LjY4NzVaTTEzLjMwNzQgMy43MDMxMlY0LjU3MDMxQzEzLjMwNzQgNS4wMzY0NiAxMy4yNjU3IDUuNDI5NjkgMTMuMTgyNCA1Ljc1QzEzLjA5OSA2LjA3MDMxIDEyLjk3OTMgNi4zMjgxMiAxMi44MjMgNi41MjM0NEMxMi42NjY4IDYuNzE4NzUgMTIuNDc3OSA2Ljg2MDY4IDEyLjI1NjYgNi45NDkyMkMxMi4wMzc4IDcuMDM1MTYgMTEuNzkwNCA3LjA3ODEyIDExLjUxNDQgNy4wNzgxMkMxMS4yOTU3IDcuMDc4MTIgMTEuMDkzOCA3LjA1MDc4IDEwLjkwODkgNi45OTYwOUMxMC43MjQgNi45NDE0MSAxMC41NTc0IDYuODU0MTcgMTAuNDA4OSA2LjczNDM4QzEwLjI2MzEgNi42MTE5OCAxMC4xMzgxIDYuNDUzMTIgMTAuMDMzOSA2LjI1NzgxQzkuOTI5NzcgNi4wNjI1IDkuODUwMzQgNS44MjU1MiA5Ljc5NTY2IDUuNTQ2ODhDOS43NDA5NyA1LjI2ODIzIDkuNzEzNjMgNC45NDI3MSA5LjcxMzYzIDQuNTcwMzFWMy43MDMxMkM5LjcxMzYzIDMuMjM2OTggOS43NTUyOSAyLjg0NjM1IDkuODM4NjMgMi41MzEyNUM5LjkyNDU2IDIuMjE2MTUgMTAuMDQ1NyAxLjk2MzU0IDEwLjIwMTkgMS43NzM0NEMxMC4zNTgyIDEuNTgwNzMgMTAuNTQ1NyAxLjQ0MjcxIDEwLjc2NDQgMS4zNTkzOEMxMC45ODU4IDEuMjc2MDQgMTEuMjMzMiAxLjIzNDM4IDExLjUwNjYgMS4yMzQzOEMxMS43Mjc5IDEuMjM0MzggMTEuOTMxMSAxLjI2MTcyIDEyLjExNiAxLjMxNjQxQzEyLjMwMzUgMS4zNjg0OSAxMi40NzAxIDEuNDUzMTIgMTIuNjE2IDEuNTcwMzFDMTIuNzYxOCAxLjY4NDkgMTIuODg1NSAxLjgzODU0IDEyLjk4NzEgMi4wMzEyNUMxMy4wOTEyIDIuMjIxMzUgMTMuMTcwNyAyLjQ1NDQzIDEzLjIyNTMgMi43MzA0N0MxMy4yOCAzLjAwNjUxIDEzLjMwNzQgMy4zMzA3MyAxMy4zMDc0IDMuNzAzMTJaTTEyLjU4MDggNC42ODc1VjMuNTgyMDNDMTIuNTgwOCAzLjMyNjgyIDEyLjU2NTIgMy4xMDI4NiAxMi41MzM5IDIuOTEwMTZDMTIuNTA1MyAyLjcxNDg0IDEyLjQ2MjMgMi41NDgxOCAxMi40MDUgMi40MTAxNkMxMi4zNDc3IDIuMjcyMTQgMTIuMjc0OCAyLjE2MDE2IDEyLjE4NjMgMi4wNzQyMkMxMi4xMDAzIDEuOTg4MjggMTIuMDAwMSAxLjkyNTc4IDExLjg4NTUgMS44ODY3MkMxMS43NzM1IDEuODQ1MDUgMTEuNjQ3MiAxLjgyNDIyIDExLjUwNjYgMS44MjQyMkMxMS4zMzQ3IDEuODI0MjIgMTEuMTgyNCAxLjg1Njc3IDExLjA0OTYgMS45MjE4OEMxMC45MTY4IDEuOTg0MzggMTAuODA0OCAyLjA4NDY0IDEwLjcxMzYgMi4yMjI2NkMxMC42MjUxIDIuMzYwNjggMTAuNTU3NCAyLjU0MTY3IDEwLjUxMDUgMi43NjU2MkMxMC40NjM2IDIuOTg5NTggMTAuNDQwMiAzLjI2MTcyIDEwLjQ0MDIgMy41ODIwM1Y0LjY4NzVDMTAuNDQwMiA0Ljk0MjcxIDEwLjQ1NDUgNS4xNjc5NyAxMC40ODMyIDUuMzYzMjhDMTAuNTE0NCA1LjU1ODU5IDEwLjU2IDUuNzI3ODYgMTAuNjE5OSA1Ljg3MTA5QzEwLjY3OTggNi4wMTE3MiAxMC43NTI3IDYuMTI3NiAxMC44Mzg2IDYuMjE4NzVDMTAuOTI0NiA2LjMwOTkgMTEuMDIzNSA2LjM3NzYgMTEuMTM1NSA2LjQyMTg4QzExLjI1MDEgNi40NjM1NCAxMS4zNzY0IDYuNDg0MzggMTEuNTE0NCA2LjQ4NDM4QzExLjY5MTUgNi40ODQzOCAxMS44NDY0IDYuNDUwNTIgMTEuOTc5MyA2LjM4MjgxQzEyLjExMjEgNi4zMTUxIDEyLjIyMjcgNi4yMDk2NCAxMi4zMTEzIDYuMDY2NDFDMTIuNDAyNCA1LjkyMDU3IDEyLjQ3MDEgNS43MzQzOCAxMi41MTQ0IDUuNTA3ODFDMTIuNTU4NyA1LjI3ODY1IDEyLjU4MDggNS4wMDUyMSAxMi41ODA4IDQuNjg3NVpNMTQuMzA2OCAyLjcwNzAzVjIuNDA2MjVDMTQuMzA2OCAyLjE5MDEgMTQuMzUzNiAxLjk5MzQ5IDE0LjQ0NzQgMS44MTY0MUMxNC41NDExIDEuNjM5MzIgMTQuNjc1MyAxLjQ5NzQgMTQuODQ5NyAxLjM5MDYyQzE1LjAyNDIgMS4yODM4NSAxNS4yMzEyIDEuMjMwNDcgMTUuNDcwOCAxLjIzMDQ3QzE1LjcxNTYgMS4yMzA0NyAxNS45MjQgMS4yODM4NSAxNi4wOTU4IDEuMzkwNjJDMTYuMjcwMyAxLjQ5NzQgMTYuNDA0NCAxLjYzOTMyIDE2LjQ5ODIgMS44MTY0MUMxNi41OTE5IDEuOTkzNDkgMTYuNjM4OCAyLjE5MDEgMTYuNjM4OCAyLjQwNjI1VjIuNzA3MDNDMTYuNjM4OCAyLjkxNzk3IDE2LjU5MTkgMy4xMTE5OCAxNi40OTgyIDMuMjg5MDZDMTYuNDA3IDMuNDY2MTUgMTYuMjc0MiAzLjYwODA3IDE2LjA5OTcgMy43MTQ4NEMxNS45Mjc5IDMuODIxNjEgMTUuNzIwOCAzLjg3NSAxNS40Nzg2IDMuODc1QzE1LjIzNjUgMy44NzUgMTUuMDI2OCAzLjgyMTYxIDE0Ljg0OTcgMy43MTQ4NEMxNC42NzUzIDMuNjA4MDcgMTQuNTQxMSAzLjQ2NjE1IDE0LjQ0NzQgMy4yODkwNkMxNC4zNTM2IDMuMTExOTggMTQuMzA2OCAyLjkxNzk3IDE0LjMwNjggMi43MDcwM1pNMTQuODQ5NyAyLjQwNjI1VjIuNzA3MDNDMTQuODQ5NyAyLjgyNjgyIDE0Ljg3MTkgMi45NDAxIDE0LjkxNjEgMy4wNDY4OEMxNC45NjMgMy4xNTM2NSAxNS4wMzMzIDMuMjQwODkgMTUuMTI3MSAzLjMwODU5QzE1LjIyMDggMy4zNzM3IDE1LjMzOCAzLjQwNjI1IDE1LjQ3ODYgMy40MDYyNUMxNS42MTkzIDMuNDA2MjUgMTUuNzM1MiAzLjM3MzcgMTUuODI2MyAzLjMwODU5QzE1LjkxNzQgMy4yNDA4OSAxNS45ODUyIDMuMTUzNjUgMTYuMDI5NCAzLjA0Njg4QzE2LjA3MzcgMi45NDAxIDE2LjA5NTggMi44MjY4MiAxNi4wOTU4IDIuNzA3MDNWMi40MDYyNUMxNi4wOTU4IDIuMjgzODUgMTYuMDcyNCAyLjE2OTI3IDE2LjAyNTUgMi4wNjI1QzE1Ljk4MTIgMS45NTMxMiAxNS45MTIyIDEuODY1ODkgMTUuODE4NSAxLjgwMDc4QzE1LjcyNzMgMS43MzMwNyAxNS42MTE1IDEuNjk5MjIgMTUuNDcwOCAxLjY5OTIyQzE1LjMzMjggMS42OTkyMiAxNS4yMTY5IDEuNzMzMDcgMTUuMTIzMiAxLjgwMDc4QzE1LjAzMiAxLjg2NTg5IDE0Ljk2MyAxLjk1MzEyIDE0LjkxNjEgMi4wNjI1QzE0Ljg3MTkgMi4xNjkyNyAxNC44NDk3IDIuMjgzODUgMTQuODQ5NyAyLjQwNjI1Wk0xNy4wNzYzIDUuOTEwMTZWNS42MDU0N0MxNy4wNzYzIDUuMzkxOTMgMTcuMTIzMiA1LjE5NjYxIDE3LjIxNjkgNS4wMTk1M0MxNy4zMTA3IDQuODQyNDUgMTcuNDQ0OCA0LjcwMDUyIDE3LjYxOTMgNC41OTM3NUMxNy43OTM3IDQuNDg2OTggMTguMDAwOCA0LjQzMzU5IDE4LjI0MDQgNC40MzM1OUMxOC40ODUyIDQuNDMzNTkgMTguNjkzNSA0LjQ4Njk4IDE4Ljg2NTQgNC41OTM3NUMxOS4wMzk4IDQuNzAwNTIgMTkuMTc0IDQuODQyNDUgMTkuMjY3NyA1LjAxOTUzQzE5LjM2MTUgNS4xOTY2MSAxOS40MDgzIDUuMzkxOTMgMTkuNDA4MyA1LjYwNTQ3VjUuOTEwMTZDMTkuNDA4MyA2LjEyMzcgMTkuMzYxNSA2LjMxOTAxIDE5LjI2NzcgNi40OTYwOUMxOS4xNzY2IDYuNjczMTggMTkuMDQzNyA2LjgxNTEgMTguODY5MyA2LjkyMTg4QzE4LjY5NzQgNy4wMjg2NSAxOC40OTA0IDcuMDgyMDMgMTguMjQ4MiA3LjA4MjAzQzE4LjAwNiA3LjA4MjAzIDE3Ljc5NzcgNy4wMjg2NSAxNy42MjMyIDYuOTIxODhDMTcuNDQ4NyA2LjgxNTEgMTcuMzEzMyA2LjY3MzE4IDE3LjIxNjkgNi40OTYwOUMxNy4xMjMyIDYuMzE5MDEgMTcuMDc2MyA2LjEyMzcgMTcuMDc2MyA1LjkxMDE2Wk0xNy42MTkzIDUuNjA1NDdWNS45MTAxNkMxNy42MTkzIDYuMDI5OTUgMTcuNjQxNCA2LjE0NDUzIDE3LjY4NTcgNi4yNTM5MUMxNy43MzI1IDYuMzYwNjggMTcuODAyOSA2LjQ0NzkyIDE3Ljg5NjYgNi41MTU2MkMxNy45OTA0IDYuNTgwNzMgMTguMTA3NSA2LjYxMzI4IDE4LjI0ODIgNi42MTMyOEMxOC4zODg4IDYuNjEzMjggMTguNTA0NyA2LjU4MDczIDE4LjU5NTggNi41MTU2MkMxOC42ODk2IDYuNDQ3OTIgMTguNzU4NiA2LjM2MDY4IDE4LjgwMjkgNi4yNTM5MUMxOC44NDcxIDYuMTQ3MTQgMTguODY5MyA2LjAzMjU1IDE4Ljg2OTMgNS45MTAxNlY1LjYwNTQ3QzE4Ljg2OTMgNS40ODMwNyAxOC44NDU4IDUuMzY4NDkgMTguNzk5IDUuMjYxNzJDMTguNzU0NyA1LjE1NDk1IDE4LjY4NTcgNS4wNjkwMSAxOC41OTE5IDUuMDAzOTFDMTguNTAwOCA0LjkzNjIgMTguMzgzNiA0LjkwMjM0IDE4LjI0MDQgNC45MDIzNEMxOC4xMDIzIDQuOTAyMzQgMTcuOTg2NSA0LjkzNjIgMTcuODkyNyA1LjAwMzkxQzE3LjgwMTYgNS4wNjkwMSAxNy43MzI1IDUuMTU0OTUgMTcuNjg1NyA1LjI2MTcyQzE3LjY0MTQgNS4zNjg0OSAxNy42MTkzIDUuNDgzMDcgMTcuNjE5MyA1LjYwNTQ3Wk0xOC40MiAyLjEyMTA5TDE1LjY0MjcgNi41NjY0MUwxNS4yMzY1IDYuMzA4NTlMMTguMDEzOCAxLjg2MzI4TDE4LjQyIDIuMTIxMDlaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjU0Ii8+CjxwYXRoIGQ9Ik04LjA1ODU5IDMzLjY2OEM4LjA1ODU5IDM0LjAxNDMgNy45Nzc4NiAzNC4zMDg2IDcuODE2NDEgMzQuNTUwOEM3LjY1NzU1IDM0Ljc5MDQgNy40NDE0MSAzNC45NzI3IDcuMTY3OTcgMzUuMDk3N0M2Ljg5NzE0IDM1LjIyMjcgNi41OTExNSAzNS4yODUyIDYuMjUgMzUuMjg1MkM1LjkwODg1IDM1LjI4NTIgNS42MDE1NiAzNS4yMjI3IDUuMzI4MTIgMzUuMDk3N0M1LjA1NDY5IDM0Ljk3MjcgNC44Mzg1NCAzNC43OTA0IDQuNjc5NjkgMzQuNTUwOEM0LjUyMDgzIDM0LjMwODYgNC40NDE0MSAzNC4wMTQzIDQuNDQxNDEgMzMuNjY4QzQuNDQxNDEgMzMuNDQxNCA0LjQ4NDM4IDMzLjIzNDQgNC41NzAzMSAzMy4wNDY5QzQuNjU4ODUgMzIuODU2OCA0Ljc4MjU1IDMyLjY5MTQgNC45NDE0MSAzMi41NTA4QzUuMTAyODYgMzIuNDEwMiA1LjI5Mjk3IDMyLjMwMjEgNS41MTE3MiAzMi4yMjY2QzUuNzMzMDcgMzIuMTQ4NCA1Ljk3NjU2IDMyLjEwOTQgNi4yNDIxOSAzMi4xMDk0QzYuNTkxMTUgMzIuMTA5NCA2LjkwMjM0IDMyLjE3NzEgNy4xNzU3OCAzMi4zMTI1QzcuNDQ5MjIgMzIuNDQ1MyA3LjY2NDA2IDMyLjYyODkgNy44MjAzMSAzMi44NjMzQzcuOTc5MTcgMzMuMDk3NyA4LjA1ODU5IDMzLjM2NTkgOC4wNTg1OSAzMy42NjhaTTcuMzMyMDMgMzMuNjUyM0M3LjMzMjAzIDMzLjQ0MTQgNy4yODY0NiAzMy4yNTUyIDcuMTk1MzEgMzMuMDkzOEM3LjEwNDE3IDMyLjkyOTcgNi45NzY1NiAzMi44MDIxIDYuODEyNSAzMi43MTA5QzYuNjQ4NDQgMzIuNjE5OCA2LjQ1ODMzIDMyLjU3NDIgNi4yNDIxOSAzMi41NzQyQzYuMDIwODMgMzIuNTc0MiA1LjgyOTQzIDMyLjYxOTggNS42Njc5NyAzMi43MTA5QzUuNTA5MTEgMzIuODAyMSA1LjM4NTQyIDMyLjkyOTcgNS4yOTY4OCAzMy4wOTM4QzUuMjA4MzMgMzMuMjU1MiA1LjE2NDA2IDMzLjQ0MTQgNS4xNjQwNiAzMy42NTIzQzUuMTY0MDYgMzMuODcxMSA1LjIwNzAzIDM0LjA1ODYgNS4yOTI5NyAzNC4yMTQ4QzUuMzgxNTEgMzQuMzY4NSA1LjUwNjUxIDM0LjQ4NyA1LjY2Nzk3IDM0LjU3MDNDNS44MzIwMyAzNC42NTEgNi4wMjYwNCAzNC42OTE0IDYuMjUgMzQuNjkxNEM2LjQ3Mzk2IDM0LjY5MTQgNi42NjY2NyAzNC42NTEgNi44MjgxMiAzNC41NzAzQzYuOTg5NTggMzQuNDg3IDcuMTEzMjggMzQuMzY4NSA3LjE5OTIyIDM0LjIxNDhDNy4yODc3NiAzNC4wNTg2IDcuMzMyMDMgMzMuODcxMSA3LjMzMjAzIDMzLjY1MjNaTTcuOTI1NzggMzFDNy45MjU3OCAzMS4yNzYgNy44NTI4NiAzMS41MjQ3IDcuNzA3MDMgMzEuNzQ2MUM3LjU2MTIgMzEuOTY3NCA3LjM2MTk4IDMyLjE0MTkgNy4xMDkzOCAzMi4yNjk1QzYuODU2NzcgMzIuMzk3MSA2LjU3MDMxIDMyLjQ2MDkgNi4yNSAzMi40NjA5QzUuOTI0NDggMzIuNDYwOSA1LjYzNDExIDMyLjM5NzEgNS4zNzg5MSAzMi4yNjk1QzUuMTI2MyAzMi4xNDE5IDQuOTI4MzkgMzEuOTY3NCA0Ljc4NTE2IDMxLjc0NjFDNC42NDE5MyAzMS41MjQ3IDQuNTcwMzEgMzEuMjc2IDQuNTcwMzEgMzFDNC41NzAzMSAzMC42NjkzIDQuNjQxOTMgMzAuMzg4IDQuNzg1MTYgMzAuMTU2MkM0LjkzMDk5IDI5LjkyNDUgNS4xMzAyMSAyOS43NDc0IDUuMzgyODEgMjkuNjI1QzUuNjM1NDIgMjkuNTAyNiA1LjkyMzE4IDI5LjQ0MTQgNi4yNDYwOSAyOS40NDE0QzYuNTcxNjEgMjkuNDQxNCA2Ljg2MDY4IDI5LjUwMjYgNy4xMTMyOCAyOS42MjVDNy4zNjU4OSAyOS43NDc0IDcuNTYzOCAyOS45MjQ1IDcuNzA3MDMgMzAuMTU2MkM3Ljg1Mjg2IDMwLjM4OCA3LjkyNTc4IDMwLjY2OTMgNy45MjU3OCAzMVpNNy4yMDMxMiAzMS4wMTE3QzcuMjAzMTIgMzAuODIxNiA3LjE2Mjc2IDMwLjY1MzYgNy4wODIwMyAzMC41MDc4QzcuMDAxMyAzMC4zNjIgNi44ODkzMiAzMC4yNDc0IDYuNzQ2MDkgMzAuMTY0MUM2LjYwMjg2IDMwLjA3ODEgNi40MzYyIDMwLjAzNTIgNi4yNDYwOSAzMC4wMzUyQzYuMDU1OTkgMzAuMDM1MiA1Ljg4OTMyIDMwLjA3NTUgNS43NDYwOSAzMC4xNTYyQzUuNjA1NDcgMzAuMjM0NCA1LjQ5NDc5IDMwLjM0NjQgNS40MTQwNiAzMC40OTIyQzUuMzM1OTQgMzAuNjM4IDUuMjk2ODggMzAuODExMiA1LjI5Njg4IDMxLjAxMTdDNS4yOTY4OCAzMS4yMDcgNS4zMzU5NCAzMS4zNzc2IDUuNDE0MDYgMzEuNTIzNEM1LjQ5NDc5IDMxLjY2OTMgNS42MDY3NyAzMS43ODI2IDUuNzUgMzEuODYzM0M1Ljg5MzIzIDMxLjk0NCA2LjA1OTkgMzEuOTg0NCA2LjI1IDMxLjk4NDRDNi40NDAxIDMxLjk4NDQgNi42MDU0NyAzMS45NDQgNi43NDYwOSAzMS44NjMzQzYuODg5MzIgMzEuNzgyNiA3LjAwMTMgMzEuNjY5MyA3LjA4MjAzIDMxLjUyMzRDNy4xNjI3NiAzMS4zNzc2IDcuMjAzMTIgMzEuMjA3IDcuMjAzMTIgMzEuMDExN1pNMTIuNjc1MiAzMS45MTAyVjMyLjc3NzNDMTIuNjc1MiAzMy4yNDM1IDEyLjYzMzUgMzMuNjM2NyAxMi41NTAyIDMzLjk1N0MxMi40NjY4IDM0LjI3NzMgMTIuMzQ3IDM0LjUzNTIgMTIuMTkwOCAzNC43MzA1QzEyLjAzNDUgMzQuOTI1OCAxMS44NDU3IDM1LjA2NzcgMTEuNjI0NCAzNS4xNTYyQzExLjQwNTYgMzUuMjQyMiAxMS4xNTgyIDM1LjI4NTIgMTAuODgyMiAzNS4yODUyQzEwLjY2MzUgMzUuMjg1MiAxMC40NjE2IDM1LjI1NzggMTAuMjc2NyAzNS4yMDMxQzEwLjA5MTggMzUuMTQ4NCA5LjkyNTE3IDM1LjA2MTIgOS43NzY3MyAzNC45NDE0QzkuNjMwOSAzNC44MTkgOS41MDU5IDM0LjY2MDIgOS40MDE3MyAzNC40NjQ4QzkuMjk3NTcgMzQuMjY5NSA5LjIxODE0IDM0LjAzMjYgOS4xNjM0NSAzMy43NTM5QzkuMTA4NzcgMzMuNDc1MyA5LjA4MTQyIDMzLjE0OTcgOS4wODE0MiAzMi43NzczVjMxLjkxMDJDOS4wODE0MiAzMS40NDQgOS4xMjMwOSAzMS4wNTM0IDkuMjA2NDIgMzAuNzM4M0M5LjI5MjM2IDMwLjQyMzIgOS40MTM0NSAzMC4xNzA2IDkuNTY5NyAyOS45ODA1QzkuNzI1OTUgMjkuNzg3OCA5LjkxMzQ1IDI5LjY0OTcgMTAuMTMyMiAyOS41NjY0QzEwLjM1MzYgMjkuNDgzMSAxMC42MDEgMjkuNDQxNCAxMC44NzQ0IDI5LjQ0MTRDMTEuMDk1NyAyOS40NDE0IDExLjI5ODkgMjkuNDY4OCAxMS40ODM4IDI5LjUyMzRDMTEuNjcxMyAyOS41NzU1IDExLjgzNzkgMjkuNjYwMiAxMS45ODM4IDI5Ljc3NzNDMTIuMTI5NiAyOS44OTE5IDEyLjI1MzMgMzAuMDQ1NiAxMi4zNTQ5IDMwLjIzODNDMTIuNDU5IDMwLjQyODQgMTIuNTM4NSAzMC42NjE1IDEyLjU5MzEgMzAuOTM3NUMxMi42NDc4IDMxLjIxMzUgMTIuNjc1MiAzMS41Mzc4IDEyLjY3NTIgMzEuOTEwMlpNMTEuOTQ4NiAzMi44OTQ1VjMxLjc4OTFDMTEuOTQ4NiAzMS41MzM5IDExLjkzMyAzMS4zMDk5IDExLjkwMTcgMzEuMTE3MkMxMS44NzMxIDMwLjkyMTkgMTEuODMwMSAzMC43NTUyIDExLjc3MjggMzAuNjE3MkMxMS43MTU1IDMwLjQ3OTIgMTEuNjQyNiAzMC4zNjcyIDExLjU1NDEgMzAuMjgxMkMxMS40NjgxIDMwLjE5NTMgMTEuMzY3OSAzMC4xMzI4IDExLjI1MzMgMzAuMDkzOEMxMS4xNDEzIDMwLjA1MjEgMTEuMDE1IDMwLjAzMTIgMTAuODc0NCAzMC4wMzEyQzEwLjcwMjUgMzAuMDMxMiAxMC41NTAyIDMwLjA2MzggMTAuNDE3NCAzMC4xMjg5QzEwLjI4NDUgMzAuMTkxNCAxMC4xNzI2IDMwLjI5MTcgMTAuMDgxNCAzMC40Mjk3QzkuOTkyODggMzAuNTY3NyA5LjkyNTE3IDMwLjc0ODcgOS44NzgzIDMwLjk3MjdDOS44MzE0MiAzMS4xOTY2IDkuODA3OTggMzEuNDY4OCA5LjgwNzk4IDMxLjc4OTFWMzIuODk0NUM5LjgwNzk4IDMzLjE0OTcgOS44MjIzMSAzMy4zNzUgOS44NTA5NSAzMy41NzAzQzkuODgyMiAzMy43NjU2IDkuOTI3NzggMzMuOTM0OSA5Ljk4NzY3IDM0LjA3ODFDMTAuMDQ3NiAzNC4yMTg4IDEwLjEyMDUgMzQuMzM0NiAxMC4yMDY0IDM0LjQyNThDMTAuMjkyNCAzNC41MTY5IDEwLjM5MTMgMzQuNTg0NiAxMC41MDMzIDM0LjYyODlDMTAuNjE3OSAzNC42NzA2IDEwLjc0NDIgMzQuNjkxNCAxMC44ODIyIDM0LjY5MTRDMTEuMDU5MyAzNC42OTE0IDExLjIxNDIgMzQuNjU3NiAxMS4zNDcgMzQuNTg5OEMxMS40Nzk5IDM0LjUyMjEgMTEuNTkwNSAzNC40MTY3IDExLjY3OTEgMzQuMjczNEMxMS43NzAyIDM0LjEyNzYgMTEuODM3OSAzMy45NDE0IDExLjg4MjIgMzMuNzE0OEMxMS45MjY1IDMzLjQ4NTcgMTEuOTQ4NiAzMy4yMTIyIDExLjk0ODYgMzIuODk0NVpNMTMuNjc0NiAzMC45MTQxVjMwLjYxMzNDMTMuNjc0NiAzMC4zOTcxIDEzLjcyMTQgMzAuMjAwNSAxMy44MTUyIDMwLjAyMzRDMTMuOTA4OSAyOS44NDY0IDE0LjA0MzEgMjkuNzA0NCAxNC4yMTc1IDI5LjU5NzdDMTQuMzkyIDI5LjQ5MDkgMTQuNTk5IDI5LjQzNzUgMTQuODM4NiAyOS40Mzc1QzE1LjA4MzQgMjkuNDM3NSAxNS4yOTE4IDI5LjQ5MDkgMTUuNDYzNiAyOS41OTc3QzE1LjYzODEgMjkuNzA0NCAxNS43NzIyIDI5Ljg0NjQgMTUuODY2IDMwLjAyMzRDMTUuOTU5NyAzMC4yMDA1IDE2LjAwNjYgMzAuMzk3MSAxNi4wMDY2IDMwLjYxMzNWMzAuOTE0MUMxNi4wMDY2IDMxLjEyNSAxNS45NTk3IDMxLjMxOSAxNS44NjYgMzEuNDk2MUMxNS43NzQ4IDMxLjY3MzIgMTUuNjQyIDMxLjgxNTEgMTUuNDY3NSAzMS45MjE5QzE1LjI5NTcgMzIuMDI4NiAxNS4wODg2IDMyLjA4MiAxNC44NDY0IDMyLjA4MkMxNC42MDQzIDMyLjA4MiAxNC4zOTQ2IDMyLjAyODYgMTQuMjE3NSAzMS45MjE5QzE0LjA0MzEgMzEuODE1MSAxMy45MDg5IDMxLjY3MzIgMTMuODE1MiAzMS40OTYxQzEzLjcyMTQgMzEuMzE5IDEzLjY3NDYgMzEuMTI1IDEzLjY3NDYgMzAuOTE0MVpNMTQuMjE3NSAzMC42MTMzVjMwLjkxNDFDMTQuMjE3NSAzMS4wMzM5IDE0LjIzOTcgMzEuMTQ3MSAxNC4yODM5IDMxLjI1MzlDMTQuMzMwOCAzMS4zNjA3IDE0LjQwMTEgMzEuNDQ3OSAxNC40OTQ5IDMxLjUxNTZDMTQuNTg4NiAzMS41ODA3IDE0LjcwNTggMzEuNjEzMyAxNC44NDY0IDMxLjYxMzNDMTQuOTg3MSAzMS42MTMzIDE1LjEwMjkgMzEuNTgwNyAxNS4xOTQxIDMxLjUxNTZDMTUuMjg1MiAzMS40NDc5IDE1LjM1MjkgMzEuMzYwNyAxNS4zOTcyIDMxLjI1MzlDMTUuNDQxNSAzMS4xNDcxIDE1LjQ2MzYgMzEuMDMzOSAxNS40NjM2IDMwLjkxNDFWMzAuNjEzM0MxNS40NjM2IDMwLjQ5MDkgMTUuNDQwMiAzMC4zNzYzIDE1LjM5MzMgMzAuMjY5NUMxNS4zNDkgMzAuMTYwMiAxNS4yOCAzMC4wNzI5IDE1LjE4NjMgMzAuMDA3OEMxNS4wOTUxIDI5Ljk0MDEgMTQuOTc5MyAyOS45MDYyIDE0LjgzODYgMjkuOTA2MkMxNC43MDA2IDI5LjkwNjIgMTQuNTg0NyAyOS45NDAxIDE0LjQ5MSAzMC4wMDc4QzE0LjM5OTggMzAuMDcyOSAxNC4zMzA4IDMwLjE2MDIgMTQuMjgzOSAzMC4yNjk1QzE0LjIzOTcgMzAuMzc2MyAxNC4yMTc1IDMwLjQ5MDkgMTQuMjE3NSAzMC42MTMzWk0xNi40NDQxIDM0LjExNzJWMzMuODEyNUMxNi40NDQxIDMzLjU5OSAxNi40OTEgMzMuNDAzNiAxNi41ODQ3IDMzLjIyNjZDMTYuNjc4NSAzMy4wNDk1IDE2LjgxMjYgMzIuOTA3NiAxNi45ODcxIDMyLjgwMDhDMTcuMTYxNSAzMi42OTQgMTcuMzY4NiAzMi42NDA2IDE3LjYwODIgMzIuNjQwNkMxNy44NTI5IDMyLjY0MDYgMTguMDYxMyAzMi42OTQgMTguMjMzMiAzMi44MDA4QzE4LjQwNzYgMzIuOTA3NiAxOC41NDE4IDMzLjA0OTUgMTguNjM1NSAzMy4yMjY2QzE4LjcyOTMgMzMuNDAzNiAxOC43NzYxIDMzLjU5OSAxOC43NzYxIDMzLjgxMjVWMzQuMTE3MkMxOC43NzYxIDM0LjMzMDcgMTguNzI5MyAzNC41MjYgMTguNjM1NSAzNC43MDMxQzE4LjU0NDQgMzQuODgwMiAxOC40MTE1IDM1LjAyMjEgMTguMjM3MSAzNS4xMjg5QzE4LjA2NTIgMzUuMjM1NyAxNy44NTgyIDM1LjI4OTEgMTcuNjE2IDM1LjI4OTFDMTcuMzczOCAzNS4yODkxIDE3LjE2NTQgMzUuMjM1NyAxNi45OTEgMzUuMTI4OUMxNi44MTY1IDM1LjAyMjEgMTYuNjgxMSAzNC44ODAyIDE2LjU4NDcgMzQuNzAzMUMxNi40OTEgMzQuNTI2IDE2LjQ0NDEgMzQuMzMwNyAxNi40NDQxIDM0LjExNzJaTTE2Ljk4NzEgMzMuODEyNVYzNC4xMTcyQzE2Ljk4NzEgMzQuMjM3IDE3LjAwOTIgMzQuMzUxNiAxNy4wNTM1IDM0LjQ2MDlDMTcuMTAwMyAzNC41Njc3IDE3LjE3MDcgMzQuNjU0OSAxNy4yNjQ0IDM0LjcyMjdDMTcuMzU4MiAzNC43ODc4IDE3LjQ3NTMgMzQuODIwMyAxNy42MTYgMzQuODIwM0MxNy43NTY2IDM0LjgyMDMgMTcuODcyNSAzNC43ODc4IDE3Ljk2MzYgMzQuNzIyN0MxOC4wNTc0IDM0LjY1NDkgMTguMTI2NCAzNC41Njc3IDE4LjE3MDcgMzQuNDYwOUMxOC4yMTQ5IDM0LjM1NDIgMTguMjM3MSAzNC4yMzk2IDE4LjIzNzEgMzQuMTE3MlYzMy44MTI1QzE4LjIzNzEgMzMuNjkwMSAxOC4yMTM2IDMzLjU3NTUgMTguMTY2OCAzMy40Njg4QzE4LjEyMjUgMzMuMzYyIDE4LjA1MzUgMzMuMjc2IDE3Ljk1OTcgMzMuMjEwOUMxNy44Njg2IDMzLjE0MzIgMTcuNzUxNCAzMy4xMDk0IDE3LjYwODIgMzMuMTA5NEMxNy40NzAxIDMzLjEwOTQgMTcuMzU0MyAzMy4xNDMyIDE3LjI2MDUgMzMuMjEwOUMxNy4xNjk0IDMzLjI3NiAxNy4xMDAzIDMzLjM2MiAxNy4wNTM1IDMzLjQ2ODhDMTcuMDA5MiAzMy41NzU1IDE2Ljk4NzEgMzMuNjkwMSAxNi45ODcxIDMzLjgxMjVaTTE3Ljc4NzggMzAuMzI4MUwxNS4wMTA1IDM0Ljc3MzRMMTQuNjA0MyAzNC41MTU2TDE3LjM4MTYgMzAuMDcwM0wxNy43ODc4IDMwLjMyODFaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjU0Ii8+CjxwYXRoIGQ9Ik03LjI0NjA5IDU3LjcxNzhINy4zMDg1OVY1OC4zMzExSDcuMjQ2MDlDNi44NjMyOCA1OC4zMzExIDYuNTQyOTcgNTguMzkzNiA2LjI4NTE2IDU4LjUxODZDNi4wMjczNCA1OC42NDEgNS44MjI5MiA1OC44MDYzIDUuNjcxODggNTkuMDE0NkM1LjUyMDgzIDU5LjIyMDQgNS40MTE0NiA1OS40NTIxIDUuMzQzNzUgNTkuNzFDNS4yNzg2NSA1OS45Njc4IDUuMjQ2MDkgNjAuMjI5NSA1LjI0NjA5IDYwLjQ5NTFWNjEuMzMxMUM1LjI0NjA5IDYxLjU4MzcgNS4yNzYwNCA2MS44MDc2IDUuMzM1OTQgNjIuMDAyOUM1LjM5NTgzIDYyLjE5NTYgNS40Nzc4NiA2Mi4zNTg0IDUuNTgyMDMgNjIuNDkxMkM1LjY4NjIgNjIuNjI0IDUuODAzMzkgNjIuNzI0MyA1LjkzMzU5IDYyLjc5MkM2LjA2NjQxIDYyLjg1OTcgNi4yMDQ0MyA2Mi44OTM2IDYuMzQ3NjYgNjIuODkzNkM2LjUxNDMyIDYyLjg5MzYgNi42NjI3NiA2Mi44NjIzIDYuNzkyOTcgNjIuNzk5OEM2LjkyMzE4IDYyLjczNDcgNy4wMzI1NSA2Mi42NDQ5IDcuMTIxMDkgNjIuNTMwM0M3LjIxMjI0IDYyLjQxMzEgNy4yODEyNSA2Mi4yNzUxIDcuMzI4MTIgNjIuMTE2MkM3LjM3NSA2MS45NTc0IDcuMzk4NDQgNjEuNzgyOSA3LjM5ODQ0IDYxLjU5MjhDNy4zOTg0NCA2MS40MjM1IDcuMzc3NiA2MS4yNjA3IDcuMzM1OTQgNjEuMTA0NUM3LjI5NDI3IDYwLjk0NTYgNy4yMzA0NyA2MC44MDUgNy4xNDQ1MyA2MC42ODI2QzcuMDU4NTkgNjAuNTU3NiA2Ljk1MDUyIDYwLjQ2IDYuODIwMzEgNjAuMzg5NkM2LjY5MjcxIDYwLjMxNjcgNi41NDAzNiA2MC4yODAzIDYuMzYzMjggNjAuMjgwM0M2LjE2Mjc2IDYwLjI4MDMgNS45NzUyNiA2MC4zMjk4IDUuODAwNzggNjAuNDI4N0M1LjYyODkxIDYwLjUyNTEgNS40ODY5OCA2MC42NTI3IDUuMzc1IDYwLjgxMTVDNS4yNjU2MiA2MC45Njc4IDUuMjAzMTIgNjEuMTM4MyA1LjE4NzUgNjEuMzIzMkw0LjgwNDY5IDYxLjMxOTNDNC44NDExNSA2MS4wMjc3IDQuOTA4ODUgNjAuNzc5IDUuMDA3ODEgNjAuNTczMkM1LjEwOTM4IDYwLjM2NDkgNS4yMzQzOCA2MC4xOTU2IDUuMzgyODEgNjAuMDY1NEM1LjUzMzg1IDU5LjkzMjYgNS43MDE4MiA1OS44MzYzIDUuODg2NzIgNTkuNzc2NEM2LjA3NDIyIDU5LjcxMzkgNi4yNzIxNCA1OS42ODI2IDYuNDgwNDcgNTkuNjgyNkM2Ljc2NDMyIDU5LjY4MjYgNy4wMDkxMSA1OS43MzYgNy4yMTQ4NCA1OS44NDI4QzcuNDIwNTcgNTkuOTQ5NSA3LjU4OTg0IDYwLjA5MjggNy43MjI2NiA2MC4yNzI1QzcuODU1NDcgNjAuNDQ5NSA3Ljk1MzEyIDYwLjY1MDEgOC4wMTU2MiA2MC44NzRDOC4wODA3MyA2MS4wOTU0IDguMTEzMjggNjEuMzIzMiA4LjExMzI4IDYxLjU1NzZDOC4xMTMyOCA2MS44MjU4IDguMDc1NTIgNjIuMDc3MSA4IDYyLjMxMTVDNy45MjQ0OCA2Mi41NDU5IDcuODExMiA2Mi43NTE2IDcuNjYwMTYgNjIuOTI4N0M3LjUxMTcyIDYzLjEwNTggNy4zMjgxMiA2My4yNDM4IDcuMTA5MzggNjMuMzQyOEM2Ljg5MDYyIDYzLjQ0MTcgNi42MzY3MiA2My40OTEyIDYuMzQ3NjYgNjMuNDkxMkM2LjA0MDM2IDYzLjQ5MTIgNS43NzIxNCA2My40Mjg3IDUuNTQyOTcgNjMuMzAzN0M1LjMxMzggNjMuMTc2MSA1LjEyMzcgNjMuMDA2OCA0Ljk3MjY2IDYyLjc5NTlDNC44MjE2MSA2Mi41ODUgNC43MDgzMyA2Mi4zNTA2IDQuNjMyODEgNjIuMDkyOEM0LjU1NzI5IDYxLjgzNSA0LjUxOTUzIDYxLjU3MzIgNC41MTk1MyA2MS4zMDc2VjYwLjk2NzhDNC41MTk1MyA2MC41NjY3IDQuNTU5OSA2MC4xNzM1IDQuNjQwNjIgNTkuNzg4MUM0LjcyMTM1IDU5LjQwMjcgNC44NjA2OCA1OS4wNTM3IDUuMDU4NTkgNTguNzQxMkM1LjI1OTExIDU4LjQyODcgNS41MzY0NiA1OC4xOCA1Ljg5MDYyIDU3Ljk5NTFDNi4yNDQ3OSA1Ny44MTAyIDYuNjk2NjEgNTcuNzE3OCA3LjI0NjA5IDU3LjcxNzhaTTEyLjY3NTIgNjAuMTE2MlY2MC45ODM0QzEyLjY3NTIgNjEuNDQ5NSAxMi42MzM1IDYxLjg0MjggMTIuNTUwMiA2Mi4xNjMxQzEyLjQ2NjggNjIuNDgzNCAxMi4zNDcgNjIuNzQxMiAxMi4xOTA4IDYyLjkzNjVDMTIuMDM0NSA2My4xMzE4IDExLjg0NTcgNjMuMjczOCAxMS42MjQ0IDYzLjM2MjNDMTEuNDA1NiA2My40NDgyIDExLjE1ODIgNjMuNDkxMiAxMC44ODIyIDYzLjQ5MTJDMTAuNjYzNSA2My40OTEyIDEwLjQ2MTYgNjMuNDYzOSAxMC4yNzY3IDYzLjQwOTJDMTAuMDkxOCA2My4zNTQ1IDkuOTI1MTcgNjMuMjY3MyA5Ljc3NjczIDYzLjE0NzVDOS42MzA5IDYzLjAyNTEgOS41MDU5IDYyLjg2NjIgOS40MDE3MyA2Mi42NzA5QzkuMjk3NTcgNjIuNDc1NiA5LjIxODE0IDYyLjIzODYgOS4xNjM0NSA2MS45NkM5LjEwODc3IDYxLjY4MTMgOS4wODE0MiA2MS4zNTU4IDkuMDgxNDIgNjAuOTgzNFY2MC4xMTYyQzkuMDgxNDIgNTkuNjUwMSA5LjEyMzA5IDU5LjI1OTQgOS4yMDY0MiA1OC45NDQzQzkuMjkyMzYgNTguNjI5MiA5LjQxMzQ1IDU4LjM3NjYgOS41Njk3IDU4LjE4NjVDOS43MjU5NSA1Ny45OTM4IDkuOTEzNDUgNTcuODU1OCAxMC4xMzIyIDU3Ljc3MjVDMTAuMzUzNiA1Ny42ODkxIDEwLjYwMSA1Ny42NDc1IDEwLjg3NDQgNTcuNjQ3NUMxMS4wOTU3IDU3LjY0NzUgMTEuMjk4OSA1Ny42NzQ4IDExLjQ4MzggNTcuNzI5NUMxMS42NzEzIDU3Ljc4MTYgMTEuODM3OSA1Ny44NjYyIDExLjk4MzggNTcuOTgzNEMxMi4xMjk2IDU4LjA5OCAxMi4yNTMzIDU4LjI1MTYgMTIuMzU0OSA1OC40NDQzQzEyLjQ1OSA1OC42MzQ0IDEyLjUzODUgNTguODY3NSAxMi41OTMxIDU5LjE0MzZDMTIuNjQ3OCA1OS40MTk2IDEyLjY3NTIgNTkuNzQzOCAxMi42NzUyIDYwLjExNjJaTTExLjk0ODYgNjEuMTAwNlY1OS45OTUxQzExLjk0ODYgNTkuNzM5OSAxMS45MzMgNTkuNTE2IDExLjkwMTcgNTkuMzIzMkMxMS44NzMxIDU5LjEyNzkgMTEuODMwMSA1OC45NjEzIDExLjc3MjggNTguODIzMkMxMS43MTU1IDU4LjY4NTIgMTEuNjQyNiA1OC41NzMyIDExLjU1NDEgNTguNDg3M0MxMS40NjgxIDU4LjQwMTQgMTEuMzY3OSA1OC4zMzg5IDExLjI1MzMgNTguMjk5OEMxMS4xNDEzIDU4LjI1ODEgMTEuMDE1IDU4LjIzNzMgMTAuODc0NCA1OC4yMzczQzEwLjcwMjUgNTguMjM3MyAxMC41NTAyIDU4LjI2OTkgMTAuNDE3NCA1OC4zMzVDMTAuMjg0NSA1OC4zOTc1IDEwLjE3MjYgNTguNDk3NyAxMC4wODE0IDU4LjYzNTdDOS45OTI4OCA1OC43NzM4IDkuOTI1MTcgNTguOTU0OCA5Ljg3ODMgNTkuMTc4N0M5LjgzMTQyIDU5LjQwMjcgOS44MDc5OCA1OS42NzQ4IDkuODA3OTggNTkuOTk1MVY2MS4xMDA2QzkuODA3OTggNjEuMzU1OCA5LjgyMjMxIDYxLjU4MTEgOS44NTA5NSA2MS43NzY0QzkuODgyMiA2MS45NzE3IDkuOTI3NzggNjIuMTQxIDkuOTg3NjcgNjIuMjg0MkMxMC4wNDc2IDYyLjQyNDggMTAuMTIwNSA2Mi41NDA3IDEwLjIwNjQgNjIuNjMxOEMxMC4yOTI0IDYyLjcyMyAxMC4zOTEzIDYyLjc5MDcgMTAuNTAzMyA2Mi44MzVDMTAuNjE3OSA2Mi44NzY2IDEwLjc0NDIgNjIuODk3NSAxMC44ODIyIDYyLjg5NzVDMTEuMDU5MyA2Mi44OTc1IDExLjIxNDIgNjIuODYzNiAxMS4zNDcgNjIuNzk1OUMxMS40Nzk5IDYyLjcyODIgMTEuNTkwNSA2Mi42MjI3IDExLjY3OTEgNjIuNDc5NUMxMS43NzAyIDYyLjMzMzcgMTEuODM3OSA2Mi4xNDc1IDExLjg4MjIgNjEuOTIwOUMxMS45MjY1IDYxLjY5MTcgMTEuOTQ4NiA2MS40MTgzIDExLjk0ODYgNjEuMTAwNlpNMTMuNjc0NiA1OS4xMjAxVjU4LjgxOTNDMTMuNjc0NiA1OC42MDMyIDEzLjcyMTQgNTguNDA2NiAxMy44MTUyIDU4LjIyOTVDMTMuOTA4OSA1OC4wNTI0IDE0LjA0MzEgNTcuOTEwNSAxNC4yMTc1IDU3LjgwMzdDMTQuMzkyIDU3LjY5NjkgMTQuNTk5IDU3LjY0MzYgMTQuODM4NiA1Ny42NDM2QzE1LjA4MzQgNTcuNjQzNiAxNS4yOTE4IDU3LjY5NjkgMTUuNDYzNiA1Ny44MDM3QzE1LjYzODEgNTcuOTEwNSAxNS43NzIyIDU4LjA1MjQgMTUuODY2IDU4LjIyOTVDMTUuOTU5NyA1OC40MDY2IDE2LjAwNjYgNTguNjAzMiAxNi4wMDY2IDU4LjgxOTNWNTkuMTIwMUMxNi4wMDY2IDU5LjMzMTEgMTUuOTU5NyA1OS41MjUxIDE1Ljg2NiA1OS43MDIxQzE1Ljc3NDggNTkuODc5MiAxNS42NDIgNjAuMDIxMiAxNS40Njc1IDYwLjEyNzlDMTUuMjk1NyA2MC4yMzQ3IDE1LjA4ODYgNjAuMjg4MSAxNC44NDY0IDYwLjI4ODFDMTQuNjA0MyA2MC4yODgxIDE0LjM5NDYgNjAuMjM0NyAxNC4yMTc1IDYwLjEyNzlDMTQuMDQzMSA2MC4wMjEyIDEzLjkwODkgNTkuODc5MiAxMy44MTUyIDU5LjcwMjFDMTMuNzIxNCA1OS41MjUxIDEzLjY3NDYgNTkuMzMxMSAxMy42NzQ2IDU5LjEyMDFaTTE0LjIxNzUgNTguODE5M1Y1OS4xMjAxQzE0LjIxNzUgNTkuMjM5OSAxNC4yMzk3IDU5LjM1MzIgMTQuMjgzOSA1OS40NkMxNC4zMzA4IDU5LjU2NjcgMTQuNDAxMSA1OS42NTQgMTQuNDk0OSA1OS43MjE3QzE0LjU4ODYgNTkuNzg2OCAxNC43MDU4IDU5LjgxOTMgMTQuODQ2NCA1OS44MTkzQzE0Ljk4NzEgNTkuODE5MyAxNS4xMDI5IDU5Ljc4NjggMTUuMTk0MSA1OS43MjE3QzE1LjI4NTIgNTkuNjU0IDE1LjM1MjkgNTkuNTY2NyAxNS4zOTcyIDU5LjQ2QzE1LjQ0MTUgNTkuMzUzMiAxNS40NjM2IDU5LjIzOTkgMTUuNDYzNiA1OS4xMjAxVjU4LjgxOTNDMTUuNDYzNiA1OC42OTY5IDE1LjQ0MDIgNTguNTgyNCAxNS4zOTMzIDU4LjQ3NTZDMTUuMzQ5IDU4LjM2NjIgMTUuMjggNTguMjc5IDE1LjE4NjMgNTguMjEzOUMxNS4wOTUxIDU4LjE0NjIgMTQuOTc5MyA1OC4xMTIzIDE0LjgzODYgNTguMTEyM0MxNC43MDA2IDU4LjExMjMgMTQuNTg0NyA1OC4xNDYyIDE0LjQ5MSA1OC4yMTM5QzE0LjM5OTggNTguMjc5IDE0LjMzMDggNTguMzY2MiAxNC4yODM5IDU4LjQ3NTZDMTQuMjM5NyA1OC41ODI0IDE0LjIxNzUgNTguNjk2OSAxNC4yMTc1IDU4LjgxOTNaTTE2LjQ0NDEgNjIuMzIzMlY2Mi4wMTg2QzE2LjQ0NDEgNjEuODA1IDE2LjQ5MSA2MS42MDk3IDE2LjU4NDcgNjEuNDMyNkMxNi42Nzg1IDYxLjI1NTUgMTYuODEyNiA2MS4xMTM2IDE2Ljk4NzEgNjEuMDA2OEMxNy4xNjE1IDYwLjkwMDEgMTcuMzY4NiA2MC44NDY3IDE3LjYwODIgNjAuODQ2N0MxNy44NTI5IDYwLjg0NjcgMTguMDYxMyA2MC45MDAxIDE4LjIzMzIgNjEuMDA2OEMxOC40MDc2IDYxLjExMzYgMTguNTQxOCA2MS4yNTU1IDE4LjYzNTUgNjEuNDMyNkMxOC43MjkzIDYxLjYwOTcgMTguNzc2MSA2MS44MDUgMTguNzc2MSA2Mi4wMTg2VjYyLjMyMzJDMTguNzc2MSA2Mi41MzY4IDE4LjcyOTMgNjIuNzMyMSAxOC42MzU1IDYyLjkwOTJDMTguNTQ0NCA2My4wODYzIDE4LjQxMTUgNjMuMjI4MiAxOC4yMzcxIDYzLjMzNUMxOC4wNjUyIDYzLjQ0MTcgMTcuODU4MiA2My40OTUxIDE3LjYxNiA2My40OTUxQzE3LjM3MzggNjMuNDk1MSAxNy4xNjU0IDYzLjQ0MTcgMTYuOTkxIDYzLjMzNUMxNi44MTY1IDYzLjIyODIgMTYuNjgxMSA2My4wODYzIDE2LjU4NDcgNjIuOTA5MkMxNi40OTEgNjIuNzMyMSAxNi40NDQxIDYyLjUzNjggMTYuNDQ0MSA2Mi4zMjMyWk0xNi45ODcxIDYyLjAxODZWNjIuMzIzMkMxNi45ODcxIDYyLjQ0MyAxNy4wMDkyIDYyLjU1NzYgMTcuMDUzNSA2Mi42NjdDMTcuMTAwMyA2Mi43NzM4IDE3LjE3MDcgNjIuODYxIDE3LjI2NDQgNjIuOTI4N0MxNy4zNTgyIDYyLjk5MzggMTcuNDc1MyA2My4wMjY0IDE3LjYxNiA2My4wMjY0QzE3Ljc1NjYgNjMuMDI2NCAxNy44NzI1IDYyLjk5MzggMTcuOTYzNiA2Mi45Mjg3QzE4LjA1NzQgNjIuODYxIDE4LjEyNjQgNjIuNzczOCAxOC4xNzA3IDYyLjY2N0MxOC4yMTQ5IDYyLjU2MDIgMTguMjM3MSA2Mi40NDU2IDE4LjIzNzEgNjIuMzIzMlY2Mi4wMTg2QzE4LjIzNzEgNjEuODk2MiAxOC4yMTM2IDYxLjc4MTYgMTguMTY2OCA2MS42NzQ4QzE4LjEyMjUgNjEuNTY4IDE4LjA1MzUgNjEuNDgyMSAxNy45NTk3IDYxLjQxN0MxNy44Njg2IDYxLjM0OTMgMTcuNzUxNCA2MS4zMTU0IDE3LjYwODIgNjEuMzE1NEMxNy40NzAxIDYxLjMxNTQgMTcuMzU0MyA2MS4zNDkzIDE3LjI2MDUgNjEuNDE3QzE3LjE2OTQgNjEuNDgyMSAxNy4xMDAzIDYxLjU2OCAxNy4wNTM1IDYxLjY3NDhDMTcuMDA5MiA2MS43ODE2IDE2Ljk4NzEgNjEuODk2MiAxNi45ODcxIDYyLjAxODZaTTE3Ljc4NzggNTguNTM0MkwxNS4wMTA1IDYyLjk3OTVMMTQuNjA0MyA2Mi43MjE3TDE3LjM4MTYgNTguMjc2NEwxNy43ODc4IDU4LjUzNDJaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjU0Ii8+CjxwYXRoIGQ9Ik04LjMxNjQxIDg5LjcwNjFWOTAuMjk5OEg0LjIwNzAzVjg5Ljg3NEw2Ljc1MzkxIDg1LjkzMjZINy4zNDM3NUw2LjcxMDk0IDg3LjA3MzJMNS4wMjczNCA4OS43MDYxSDguMzE2NDFaTTcuNTIzNDQgODUuOTMyNlY5MS42MjAxSDYuODAwNzhWODUuOTMyNkg3LjUyMzQ0Wk0xMi42NzUyIDg4LjMyMzJWODkuMTkwNEMxMi42NzUyIDg5LjY1NjYgMTIuNjMzNSA5MC4wNDk4IDEyLjU1MDIgOTAuMzcwMUMxMi40NjY4IDkwLjY5MDQgMTIuMzQ3IDkwLjk0ODIgMTIuMTkwOCA5MS4xNDM2QzEyLjAzNDUgOTEuMzM4OSAxMS44NDU3IDkxLjQ4MDggMTEuNjI0NCA5MS41NjkzQzExLjQwNTYgOTEuNjU1MyAxMS4xNTgyIDkxLjY5ODIgMTAuODgyMiA5MS42OTgyQzEwLjY2MzUgOTEuNjk4MiAxMC40NjE2IDkxLjY3MDkgMTAuMjc2NyA5MS42MTYyQzEwLjA5MTggOTEuNTYxNSA5LjkyNTE3IDkxLjQ3NDMgOS43NzY3MyA5MS4zNTQ1QzkuNjMwOSA5MS4yMzIxIDkuNTA1OSA5MS4wNzMyIDkuNDAxNzMgOTAuODc3OUM5LjI5NzU3IDkwLjY4MjYgOS4yMTgxNCA5MC40NDU2IDkuMTYzNDUgOTAuMTY3QzkuMTA4NzcgODkuODg4MyA5LjA4MTQyIDg5LjU2MjggOS4wODE0MiA4OS4xOTA0Vjg4LjMyMzJDOS4wODE0MiA4Ny44NTcxIDkuMTIzMDkgODcuNDY2NSA5LjIwNjQyIDg3LjE1MTRDOS4yOTIzNiA4Ni44MzYzIDkuNDEzNDUgODYuNTgzNyA5LjU2OTcgODYuMzkzNkM5LjcyNTk1IDg2LjIwMDggOS45MTM0NSA4Ni4wNjI4IDEwLjEzMjIgODUuOTc5NUMxMC4zNTM2IDg1Ljg5NjIgMTAuNjAxIDg1Ljg1NDUgMTAuODc0NCA4NS44NTQ1QzExLjA5NTcgODUuODU0NSAxMS4yOTg5IDg1Ljg4MTggMTEuNDgzOCA4NS45MzY1QzExLjY3MTMgODUuOTg4NiAxMS44Mzc5IDg2LjA3MzIgMTEuOTgzOCA4Ni4xOTA0QzEyLjEyOTYgODYuMzA1IDEyLjI1MzMgODYuNDU4NyAxMi4zNTQ5IDg2LjY1MTRDMTIuNDU5IDg2Ljg0MTUgMTIuNTM4NSA4Ny4wNzQ1IDEyLjU5MzEgODcuMzUwNkMxMi42NDc4IDg3LjYyNjYgMTIuNjc1MiA4Ny45NTA4IDEyLjY3NTIgODguMzIzMlpNMTEuOTQ4NiA4OS4zMDc2Vjg4LjIwMjFDMTEuOTQ4NiA4Ny45NDY5IDExLjkzMyA4Ny43MjMgMTEuOTAxNyA4Ny41MzAzQzExLjg3MzEgODcuMzM1IDExLjgzMDEgODcuMTY4MyAxMS43NzI4IDg3LjAzMDNDMTEuNzE1NSA4Ni44OTIzIDExLjY0MjYgODYuNzgwMyAxMS41NTQxIDg2LjY5NDNDMTEuNDY4MSA4Ni42MDg0IDExLjM2NzkgODYuNTQ1OSAxMS4yNTMzIDg2LjUwNjhDMTEuMTQxMyA4Ni40NjUyIDExLjAxNSA4Ni40NDQzIDEwLjg3NDQgODYuNDQ0M0MxMC43MDI1IDg2LjQ0NDMgMTAuNTUwMiA4Ni40NzY5IDEwLjQxNzQgODYuNTQyQzEwLjI4NDUgODYuNjA0NSAxMC4xNzI2IDg2LjcwNDggMTAuMDgxNCA4Ni44NDI4QzkuOTkyODggODYuOTgwOCA5LjkyNTE3IDg3LjE2MTggOS44NzgzIDg3LjM4NTdDOS44MzE0MiA4Ny42MDk3IDkuODA3OTggODcuODgxOCA5LjgwNzk4IDg4LjIwMjFWODkuMzA3NkM5LjgwNzk4IDg5LjU2MjggOS44MjIzMSA4OS43ODgxIDkuODUwOTUgODkuOTgzNEM5Ljg4MjIgOTAuMTc4NyA5LjkyNzc4IDkwLjM0OCA5Ljk4NzY3IDkwLjQ5MTJDMTAuMDQ3NiA5MC42MzE4IDEwLjEyMDUgOTAuNzQ3NyAxMC4yMDY0IDkwLjgzODlDMTAuMjkyNCA5MC45MyAxMC4zOTEzIDkwLjk5NzcgMTAuNTAzMyA5MS4wNDJDMTAuNjE3OSA5MS4wODM3IDEwLjc0NDIgOTEuMTA0NSAxMC44ODIyIDkxLjEwNDVDMTEuMDU5MyA5MS4xMDQ1IDExLjIxNDIgOTEuMDcwNiAxMS4zNDcgOTEuMDAyOUMxMS40Nzk5IDkwLjkzNTIgMTEuNTkwNSA5MC44Mjk4IDExLjY3OTEgOTAuNjg2NUMxMS43NzAyIDkwLjU0MDcgMTEuODM3OSA5MC4zNTQ1IDExLjg4MjIgOTAuMTI3OUMxMS45MjY1IDg5Ljg5ODggMTEuOTQ4NiA4OS42MjUzIDExLjk0ODYgODkuMzA3NlpNMTMuNjc0NiA4Ny4zMjcxVjg3LjAyNjRDMTMuNjc0NiA4Ni44MTAyIDEzLjcyMTQgODYuNjEzNiAxMy44MTUyIDg2LjQzNjVDMTMuOTA4OSA4Ni4yNTk0IDE0LjA0MzEgODYuMTE3NSAxNC4yMTc1IDg2LjAxMDdDMTQuMzkyIDg1LjkwNCAxNC41OTkgODUuODUwNiAxNC44Mzg2IDg1Ljg1MDZDMTUuMDgzNCA4NS44NTA2IDE1LjI5MTggODUuOTA0IDE1LjQ2MzYgODYuMDEwN0MxNS42MzgxIDg2LjExNzUgMTUuNzcyMiA4Ni4yNTk0IDE1Ljg2NiA4Ni40MzY1QzE1Ljk1OTcgODYuNjEzNiAxNi4wMDY2IDg2LjgxMDIgMTYuMDA2NiA4Ny4wMjY0Vjg3LjMyNzFDMTYuMDA2NiA4Ny41MzgxIDE1Ljk1OTcgODcuNzMyMSAxNS44NjYgODcuOTA5MkMxNS43NzQ4IDg4LjA4NjMgMTUuNjQyIDg4LjIyODIgMTUuNDY3NSA4OC4zMzVDMTUuMjk1NyA4OC40NDE3IDE1LjA4ODYgODguNDk1MSAxNC44NDY0IDg4LjQ5NTFDMTQuNjA0MyA4OC40OTUxIDE0LjM5NDYgODguNDQxNyAxNC4yMTc1IDg4LjMzNUMxNC4wNDMxIDg4LjIyODIgMTMuOTA4OSA4OC4wODYzIDEzLjgxNTIgODcuOTA5MkMxMy43MjE0IDg3LjczMjEgMTMuNjc0NiA4Ny41MzgxIDEzLjY3NDYgODcuMzI3MVpNMTQuMjE3NSA4Ny4wMjY0Vjg3LjMyNzFDMTQuMjE3NSA4Ny40NDY5IDE0LjIzOTcgODcuNTYwMiAxNC4yODM5IDg3LjY2N0MxNC4zMzA4IDg3Ljc3MzggMTQuNDAxMSA4Ny44NjEgMTQuNDk0OSA4Ny45Mjg3QzE0LjU4ODYgODcuOTkzOCAxNC43MDU4IDg4LjAyNjQgMTQuODQ2NCA4OC4wMjY0QzE0Ljk4NzEgODguMDI2NCAxNS4xMDI5IDg3Ljk5MzggMTUuMTk0MSA4Ny45Mjg3QzE1LjI4NTIgODcuODYxIDE1LjM1MjkgODcuNzczOCAxNS4zOTcyIDg3LjY2N0MxNS40NDE1IDg3LjU2MDIgMTUuNDYzNiA4Ny40NDY5IDE1LjQ2MzYgODcuMzI3MVY4Ny4wMjY0QzE1LjQ2MzYgODYuOTA0IDE1LjQ0MDIgODYuNzg5NCAxNS4zOTMzIDg2LjY4MjZDMTUuMzQ5IDg2LjU3MzIgMTUuMjggODYuNDg2IDE1LjE4NjMgODYuNDIwOUMxNS4wOTUxIDg2LjM1MzIgMTQuOTc5MyA4Ni4zMTkzIDE0LjgzODYgODYuMzE5M0MxNC43MDA2IDg2LjMxOTMgMTQuNTg0NyA4Ni4zNTMyIDE0LjQ5MSA4Ni40MjA5QzE0LjM5OTggODYuNDg2IDE0LjMzMDggODYuNTczMiAxNC4yODM5IDg2LjY4MjZDMTQuMjM5NyA4Ni43ODk0IDE0LjIxNzUgODYuOTA0IDE0LjIxNzUgODcuMDI2NFpNMTYuNDQ0MSA5MC41MzAzVjkwLjIyNTZDMTYuNDQ0MSA5MC4wMTIgMTYuNDkxIDg5LjgxNjcgMTYuNTg0NyA4OS42Mzk2QzE2LjY3ODUgODkuNDYyNiAxNi44MTI2IDg5LjMyMDYgMTYuOTg3MSA4OS4yMTM5QzE3LjE2MTUgODkuMTA3MSAxNy4zNjg2IDg5LjA1MzcgMTcuNjA4MiA4OS4wNTM3QzE3Ljg1MjkgODkuMDUzNyAxOC4wNjEzIDg5LjEwNzEgMTguMjMzMiA4OS4yMTM5QzE4LjQwNzYgODkuMzIwNiAxOC41NDE4IDg5LjQ2MjYgMTguNjM1NSA4OS42Mzk2QzE4LjcyOTMgODkuODE2NyAxOC43NzYxIDkwLjAxMiAxOC43NzYxIDkwLjIyNTZWOTAuNTMwM0MxOC43NzYxIDkwLjc0MzggMTguNzI5MyA5MC45MzkxIDE4LjYzNTUgOTEuMTE2MkMxOC41NDQ0IDkxLjI5MzMgMTguNDExNSA5MS40MzUyIDE4LjIzNzEgOTEuNTQyQzE4LjA2NTIgOTEuNjQ4OCAxNy44NTgyIDkxLjcwMjEgMTcuNjE2IDkxLjcwMjFDMTcuMzczOCA5MS43MDIxIDE3LjE2NTQgOTEuNjQ4OCAxNi45OTEgOTEuNTQyQzE2LjgxNjUgOTEuNDM1MiAxNi42ODExIDkxLjI5MzMgMTYuNTg0NyA5MS4xMTYyQzE2LjQ5MSA5MC45MzkxIDE2LjQ0NDEgOTAuNzQzOCAxNi40NDQxIDkwLjUzMDNaTTE2Ljk4NzEgOTAuMjI1NlY5MC41MzAzQzE2Ljk4NzEgOTAuNjUwMSAxNy4wMDkyIDkwLjc2NDYgMTcuMDUzNSA5MC44NzRDMTcuMTAwMyA5MC45ODA4IDE3LjE3MDcgOTEuMDY4IDE3LjI2NDQgOTEuMTM1N0MxNy4zNTgyIDkxLjIwMDggMTcuNDc1MyA5MS4yMzM0IDE3LjYxNiA5MS4yMzM0QzE3Ljc1NjYgOTEuMjMzNCAxNy44NzI1IDkxLjIwMDggMTcuOTYzNiA5MS4xMzU3QzE4LjA1NzQgOTEuMDY4IDE4LjEyNjQgOTAuOTgwOCAxOC4xNzA3IDkwLjg3NEMxOC4yMTQ5IDkwLjc2NzMgMTguMjM3MSA5MC42NTI3IDE4LjIzNzEgOTAuNTMwM1Y5MC4yMjU2QzE4LjIzNzEgOTAuMTAzMiAxOC4yMTM2IDg5Ljk4ODYgMTguMTY2OCA4OS44ODE4QzE4LjEyMjUgODkuNzc1MSAxOC4wNTM1IDg5LjY4OTEgMTcuOTU5NyA4OS42MjRDMTcuODY4NiA4OS41NTYzIDE3Ljc1MTQgODkuNTIyNSAxNy42MDgyIDg5LjUyMjVDMTcuNDcwMSA4OS41MjI1IDE3LjM1NDMgODkuNTU2MyAxNy4yNjA1IDg5LjYyNEMxNy4xNjk0IDg5LjY4OTEgMTcuMTAwMyA4OS43NzUxIDE3LjA1MzUgODkuODgxOEMxNy4wMDkyIDg5Ljk4ODYgMTYuOTg3MSA5MC4xMDMyIDE2Ljk4NzEgOTAuMjI1NlpNMTcuNzg3OCA4Ni43NDEyTDE1LjAxMDUgOTEuMTg2NUwxNC42MDQzIDkwLjkyODdMMTcuMzgxNiA4Ni40ODM0TDE3Ljc4NzggODYuNzQxMloiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuNTQiLz4KPHBhdGggZD0iTTguMTk5MjIgMTE5LjIzM1YxMTkuODI3SDQuNDc2NTZWMTE5LjMwOEw2LjMzOTg0IDExNy4yMzNDNi41NjkwMSAxMTYuOTc4IDYuNzQ2MDkgMTE2Ljc2MiA2Ljg3MTA5IDExNi41ODVDNi45OTg3IDExNi40MDUgNy4wODcyNCAxMTYuMjQ1IDcuMTM2NzIgMTE2LjEwNEM3LjE4ODggMTE1Ljk2MSA3LjIxNDg0IDExNS44MTUgNy4yMTQ4NCAxMTUuNjY3QzcuMjE0ODQgMTE1LjQ3OSA3LjE3NTc4IDExNS4zMSA3LjA5NzY2IDExNS4xNTlDNy4wMjIxNCAxMTUuMDA2IDYuOTEwMTYgMTE0Ljg4MyA2Ljc2MTcyIDExNC43OTJDNi42MTMyOCAxMTQuNzAxIDYuNDMzNTkgMTE0LjY1NSA2LjIyMjY2IDExNC42NTVDNS45NzAwNSAxMTQuNjU1IDUuNzU5MTEgMTE0LjcwNSA1LjU4OTg0IDExNC44MDRDNS40MjMxOCAxMTQuOSA1LjI5ODE4IDExNS4wMzUgNS4yMTQ4NCAxMTUuMjFDNS4xMzE1MSAxMTUuMzg0IDUuMDg5ODQgMTE1LjU4NSA1LjA4OTg0IDExNS44MTJINC4zNjcxOUM0LjM2NzE5IDExNS40OTEgNC40Mzc1IDExNS4xOTggNC41NzgxMiAxMTQuOTMzQzQuNzE4NzUgMTE0LjY2NyA0LjkyNzA4IDExNC40NTYgNS4yMDMxMiAxMTQuM0M1LjQ3OTE3IDExNC4xNDEgNS44MTkwMSAxMTQuMDYyIDYuMjIyNjYgMTE0LjA2MkM2LjU4MjAzIDExNC4wNjIgNi44ODkzMiAxMTQuMTI1IDcuMTQ0NTMgMTE0LjI1M0M3LjM5OTc0IDExNC4zNzggNy41OTUwNSAxMTQuNTU1IDcuNzMwNDcgMTE0Ljc4NEM3Ljg2ODQ5IDExNS4wMTEgNy45Mzc1IDExNS4yNzYgNy45Mzc1IDExNS41ODFDNy45Mzc1IDExNS43NDggNy45MDg4NSAxMTUuOTE3IDcuODUxNTYgMTE2LjA4OUM3Ljc5Njg4IDExNi4yNTggNy43MjAwNSAxMTYuNDI3IDcuNjIxMDkgMTE2LjU5N0M3LjUyNDc0IDExNi43NjYgNy40MTE0NiAxMTYuOTMzIDcuMjgxMjUgMTE3LjA5N0M3LjE1MzY1IDExNy4yNjEgNy4wMTY5MyAxMTcuNDIyIDYuODcxMDkgMTE3LjU4MUw1LjM0NzY2IDExOS4yMzNIOC4xOTkyMlpNMTIuNjc1MiAxMTYuNTNWMTE3LjM5N0MxMi42NzUyIDExNy44NjQgMTIuNjMzNSAxMTguMjU3IDEyLjU1MDIgMTE4LjU3N0MxMi40NjY4IDExOC44OTcgMTIuMzQ3IDExOS4xNTUgMTIuMTkwOCAxMTkuMzUxQzEyLjAzNDUgMTE5LjU0NiAxMS44NDU3IDExOS42ODggMTEuNjI0NCAxMTkuNzc2QzExLjQwNTYgMTE5Ljg2MiAxMS4xNTgyIDExOS45MDUgMTAuODgyMiAxMTkuOTA1QzEwLjY2MzUgMTE5LjkwNSAxMC40NjE2IDExOS44NzggMTAuMjc2NyAxMTkuODIzQzEwLjA5MTggMTE5Ljc2OSA5LjkyNTE3IDExOS42ODEgOS43NzY3MyAxMTkuNTYyQzkuNjMwOSAxMTkuNDM5IDkuNTA1OSAxMTkuMjggOS40MDE3MyAxMTkuMDg1QzkuMjk3NTcgMTE4Ljg5IDkuMjE4MTQgMTE4LjY1MyA5LjE2MzQ1IDExOC4zNzRDOS4xMDg3NyAxMTguMDk1IDkuMDgxNDIgMTE3Ljc3IDkuMDgxNDIgMTE3LjM5N1YxMTYuNTNDOS4wODE0MiAxMTYuMDY0IDkuMTIzMDkgMTE1LjY3NCA5LjIwNjQyIDExNS4zNThDOS4yOTIzNiAxMTUuMDQzIDkuNDEzNDUgMTE0Ljc5MSA5LjU2OTcgMTE0LjYwMUM5LjcyNTk1IDExNC40MDggOS45MTM0NSAxMTQuMjcgMTAuMTMyMiAxMTQuMTg3QzEwLjM1MzYgMTE0LjEwMyAxMC42MDEgMTE0LjA2MiAxMC44NzQ0IDExNC4wNjJDMTEuMDk1NyAxMTQuMDYyIDExLjI5ODkgMTE0LjA4OSAxMS40ODM4IDExNC4xNDRDMTEuNjcxMyAxMTQuMTk2IDExLjgzNzkgMTE0LjI4IDExLjk4MzggMTE0LjM5N0MxMi4xMjk2IDExNC41MTIgMTIuMjUzMyAxMTQuNjY2IDEyLjM1NDkgMTE0Ljg1OEMxMi40NTkgMTE1LjA0OSAxMi41Mzg1IDExNS4yODIgMTIuNTkzMSAxMTUuNTU4QzEyLjY0NzggMTE1LjgzNCAxMi42NzUyIDExNi4xNTggMTIuNjc1MiAxMTYuNTNaTTExLjk0ODYgMTE3LjUxNVYxMTYuNDA5QzExLjk0ODYgMTE2LjE1NCAxMS45MzMgMTE1LjkzIDExLjkwMTcgMTE1LjczN0MxMS44NzMxIDExNS41NDIgMTEuODMwMSAxMTUuMzc1IDExLjc3MjggMTE1LjIzN0MxMS43MTU1IDExNS4wOTkgMTEuNjQyNiAxMTQuOTg3IDExLjU1NDEgMTE0LjkwMUMxMS40NjgxIDExNC44MTUgMTEuMzY3OSAxMTQuNzUzIDExLjI1MzMgMTE0LjcxNEMxMS4xNDEzIDExNC42NzIgMTEuMDE1IDExNC42NTEgMTAuODc0NCAxMTQuNjUxQzEwLjcwMjUgMTE0LjY1MSAxMC41NTAyIDExNC42ODQgMTAuNDE3NCAxMTQuNzQ5QzEwLjI4NDUgMTE0LjgxMiAxMC4xNzI2IDExNC45MTIgMTAuMDgxNCAxMTUuMDVDOS45OTI4OCAxMTUuMTg4IDkuOTI1MTcgMTE1LjM2OSA5Ljg3ODMgMTE1LjU5M0M5LjgzMTQyIDExNS44MTcgOS44MDc5OCAxMTYuMDg5IDkuODA3OTggMTE2LjQwOVYxMTcuNTE1QzkuODA3OTggMTE3Ljc3IDkuODIyMzEgMTE3Ljk5NSA5Ljg1MDk1IDExOC4xOUM5Ljg4MjIgMTE4LjM4NiA5LjkyNzc4IDExOC41NTUgOS45ODc2NyAxMTguNjk4QzEwLjA0NzYgMTE4LjgzOSAxMC4xMjA1IDExOC45NTUgMTAuMjA2NCAxMTkuMDQ2QzEwLjI5MjQgMTE5LjEzNyAxMC4zOTEzIDExOS4yMDUgMTAuNTAzMyAxMTkuMjQ5QzEwLjYxNzkgMTE5LjI5MSAxMC43NDQyIDExOS4zMTIgMTAuODgyMiAxMTkuMzEyQzExLjA1OTMgMTE5LjMxMiAxMS4yMTQyIDExOS4yNzggMTEuMzQ3IDExOS4yMUMxMS40Nzk5IDExOS4xNDIgMTEuNTkwNSAxMTkuMDM3IDExLjY3OTEgMTE4Ljg5NEMxMS43NzAyIDExOC43NDggMTEuODM3OSAxMTguNTYyIDExLjg4MjIgMTE4LjMzNUMxMS45MjY1IDExOC4xMDYgMTEuOTQ4NiAxMTcuODMyIDExLjk0ODYgMTE3LjUxNVpNMTMuNjc0NiAxMTUuNTM0VjExNS4yMzNDMTMuNjc0NiAxMTUuMDE3IDEzLjcyMTQgMTE0LjgyMSAxMy44MTUyIDExNC42NDRDMTMuOTA4OSAxMTQuNDY2IDE0LjA0MzEgMTE0LjMyNSAxNC4yMTc1IDExNC4yMThDMTQuMzkyIDExNC4xMTEgMTQuNTk5IDExNC4wNTggMTQuODM4NiAxMTQuMDU4QzE1LjA4MzQgMTE0LjA1OCAxNS4yOTE4IDExNC4xMTEgMTUuNDYzNiAxMTQuMjE4QzE1LjYzODEgMTE0LjMyNSAxNS43NzIyIDExNC40NjYgMTUuODY2IDExNC42NDRDMTUuOTU5NyAxMTQuODIxIDE2LjAwNjYgMTE1LjAxNyAxNi4wMDY2IDExNS4yMzNWMTE1LjUzNEMxNi4wMDY2IDExNS43NDUgMTUuOTU5NyAxMTUuOTM5IDE1Ljg2NiAxMTYuMTE2QzE1Ljc3NDggMTE2LjI5MyAxNS42NDIgMTE2LjQzNSAxNS40Njc1IDExNi41NDJDMTUuMjk1NyAxMTYuNjQ5IDE1LjA4ODYgMTE2LjcwMiAxNC44NDY0IDExNi43MDJDMTQuNjA0MyAxMTYuNzAyIDE0LjM5NDYgMTE2LjY0OSAxNC4yMTc1IDExNi41NDJDMTQuMDQzMSAxMTYuNDM1IDEzLjkwODkgMTE2LjI5MyAxMy44MTUyIDExNi4xMTZDMTMuNzIxNCAxMTUuOTM5IDEzLjY3NDYgMTE1Ljc0NSAxMy42NzQ2IDExNS41MzRaTTE0LjIxNzUgMTE1LjIzM1YxMTUuNTM0QzE0LjIxNzUgMTE1LjY1NCAxNC4yMzk3IDExNS43NjcgMTQuMjgzOSAxMTUuODc0QzE0LjMzMDggMTE1Ljk4MSAxNC40MDExIDExNi4wNjggMTQuNDk0OSAxMTYuMTM2QzE0LjU4ODYgMTE2LjIwMSAxNC43MDU4IDExNi4yMzMgMTQuODQ2NCAxMTYuMjMzQzE0Ljk4NzEgMTE2LjIzMyAxNS4xMDI5IDExNi4yMDEgMTUuMTk0MSAxMTYuMTM2QzE1LjI4NTIgMTE2LjA2OCAxNS4zNTI5IDExNS45ODEgMTUuMzk3MiAxMTUuODc0QzE1LjQ0MTUgMTE1Ljc2NyAxNS40NjM2IDExNS42NTQgMTUuNDYzNiAxMTUuNTM0VjExNS4yMzNDMTUuNDYzNiAxMTUuMTExIDE1LjQ0MDIgMTE0Ljk5NiAxNS4zOTMzIDExNC44OUMxNS4zNDkgMTE0Ljc4IDE1LjI4IDExNC42OTMgMTUuMTg2MyAxMTQuNjI4QzE1LjA5NTEgMTE0LjU2IDE0Ljk3OTMgMTE0LjUyNiAxNC44Mzg2IDExNC41MjZDMTQuNzAwNiAxMTQuNTI2IDE0LjU4NDcgMTE0LjU2IDE0LjQ5MSAxMTQuNjI4QzE0LjM5OTggMTE0LjY5MyAxNC4zMzA4IDExNC43OCAxNC4yODM5IDExNC44OUMxNC4yMzk3IDExNC45OTYgMTQuMjE3NSAxMTUuMTExIDE0LjIxNzUgMTE1LjIzM1pNMTYuNDQ0MSAxMTguNzM3VjExOC40MzNDMTYuNDQ0MSAxMTguMjE5IDE2LjQ5MSAxMTguMDI0IDE2LjU4NDcgMTE3Ljg0N0MxNi42Nzg1IDExNy42NyAxNi44MTI2IDExNy41MjggMTYuOTg3MSAxMTcuNDIxQzE3LjE2MTUgMTE3LjMxNCAxNy4zNjg2IDExNy4yNjEgMTcuNjA4MiAxMTcuMjYxQzE3Ljg1MjkgMTE3LjI2MSAxOC4wNjEzIDExNy4zMTQgMTguMjMzMiAxMTcuNDIxQzE4LjQwNzYgMTE3LjUyOCAxOC41NDE4IDExNy42NyAxOC42MzU1IDExNy44NDdDMTguNzI5MyAxMTguMDI0IDE4Ljc3NjEgMTE4LjIxOSAxOC43NzYxIDExOC40MzNWMTE4LjczN0MxOC43NzYxIDExOC45NTEgMTguNzI5MyAxMTkuMTQ2IDE4LjYzNTUgMTE5LjMyM0MxOC41NDQ0IDExOS41IDE4LjQxMTUgMTE5LjY0MiAxOC4yMzcxIDExOS43NDlDMTguMDY1MiAxMTkuODU2IDE3Ljg1ODIgMTE5LjkwOSAxNy42MTYgMTE5LjkwOUMxNy4zNzM4IDExOS45MDkgMTcuMTY1NCAxMTkuODU2IDE2Ljk5MSAxMTkuNzQ5QzE2LjgxNjUgMTE5LjY0MiAxNi42ODExIDExOS41IDE2LjU4NDcgMTE5LjMyM0MxNi40OTEgMTE5LjE0NiAxNi40NDQxIDExOC45NTEgMTYuNDQ0MSAxMTguNzM3Wk0xNi45ODcxIDExOC40MzNWMTE4LjczN0MxNi45ODcxIDExOC44NTcgMTcuMDA5MiAxMTguOTcyIDE3LjA1MzUgMTE5LjA4MUMxNy4xMDAzIDExOS4xODggMTcuMTcwNyAxMTkuMjc1IDE3LjI2NDQgMTE5LjM0M0MxNy4zNTgyIDExOS40MDggMTcuNDc1MyAxMTkuNDQgMTcuNjE2IDExOS40NEMxNy43NTY2IDExOS40NCAxNy44NzI1IDExOS40MDggMTcuOTYzNiAxMTkuMzQzQzE4LjA1NzQgMTE5LjI3NSAxOC4xMjY0IDExOS4xODggMTguMTcwNyAxMTkuMDgxQzE4LjIxNDkgMTE4Ljk3NCAxOC4yMzcxIDExOC44NiAxOC4yMzcxIDExOC43MzdWMTE4LjQzM0MxOC4yMzcxIDExOC4zMSAxOC4yMTM2IDExOC4xOTYgMTguMTY2OCAxMTguMDg5QzE4LjEyMjUgMTE3Ljk4MiAxOC4wNTM1IDExNy44OTYgMTcuOTU5NyAxMTcuODMxQzE3Ljg2ODYgMTE3Ljc2MyAxNy43NTE0IDExNy43MjkgMTcuNjA4MiAxMTcuNzI5QzE3LjQ3MDEgMTE3LjcyOSAxNy4zNTQzIDExNy43NjMgMTcuMjYwNSAxMTcuODMxQzE3LjE2OTQgMTE3Ljg5NiAxNy4xMDAzIDExNy45ODIgMTcuMDUzNSAxMTguMDg5QzE3LjAwOTIgMTE4LjE5NiAxNi45ODcxIDExOC4zMSAxNi45ODcxIDExOC40MzNaTTE3Ljc4NzggMTE0Ljk0OEwxNS4wMTA1IDExOS4zOTRMMTQuNjA0MyAxMTkuMTM2TDE3LjM4MTYgMTE0LjY5TDE3Ljc4NzggMTE0Ljk0OFoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuNTQiLz4KPHBhdGggZD0iTTEzLjA0MyAxNDQuNzM3VjE0NS42MDRDMTMuMDQzIDE0Ni4wNzEgMTMuMDAxMyAxNDYuNDY0IDEyLjkxOCAxNDYuNzg0QzEyLjgzNDYgMTQ3LjEwNCAxMi43MTQ4IDE0Ny4zNjIgMTIuNTU4NiAxNDcuNTU4QzEyLjQwMjMgMTQ3Ljc1MyAxMi4yMTM1IDE0Ny44OTUgMTEuOTkyMiAxNDcuOTgzQzExLjc3MzQgMTQ4LjA2OSAxMS41MjYgMTQ4LjExMiAxMS4yNSAxNDguMTEyQzExLjAzMTIgMTQ4LjExMiAxMC44Mjk0IDE0OC4wODUgMTAuNjQ0NSAxNDguMDNDMTAuNDU5NiAxNDcuOTc2IDEwLjI5MyAxNDcuODg4IDEwLjE0NDUgMTQ3Ljc2OUM5Ljk5ODcgMTQ3LjY0NiA5Ljg3MzcgMTQ3LjQ4NyA5Ljc2OTUzIDE0Ny4yOTJDOS42NjUzNiAxNDcuMDk3IDkuNTg1OTQgMTQ2Ljg2IDkuNTMxMjUgMTQ2LjU4MUM5LjQ3NjU2IDE0Ni4zMDIgOS40NDkyMiAxNDUuOTc3IDkuNDQ5MjIgMTQ1LjYwNFYxNDQuNzM3QzkuNDQ5MjIgMTQ0LjI3MSA5LjQ5MDg5IDE0My44ODEgOS41NzQyMiAxNDMuNTY1QzkuNjYwMTYgMTQzLjI1IDkuNzgxMjUgMTQyLjk5OCA5LjkzNzUgMTQyLjgwOEMxMC4wOTM4IDE0Mi42MTUgMTAuMjgxMiAxNDIuNDc3IDEwLjUgMTQyLjM5NEMxMC43MjE0IDE0Mi4zMSAxMC45Njg4IDE0Mi4yNjkgMTEuMjQyMiAxNDIuMjY5QzExLjQ2MzUgMTQyLjI2OSAxMS42NjY3IDE0Mi4yOTYgMTEuODUxNiAxNDIuMzUxQzEyLjAzOTEgMTQyLjQwMyAxMi4yMDU3IDE0Mi40ODcgMTIuMzUxNiAxNDIuNjA0QzEyLjQ5NzQgMTQyLjcxOSAxMi42MjExIDE0Mi44NzMgMTIuNzIyNyAxNDMuMDY1QzEyLjgyNjggMTQzLjI1NiAxMi45MDYyIDE0My40ODkgMTIuOTYwOSAxNDMuNzY1QzEzLjAxNTYgMTQ0LjA0MSAxMy4wNDMgMTQ0LjM2NSAxMy4wNDMgMTQ0LjczN1pNMTIuMzE2NCAxNDUuNzIyVjE0NC42MTZDMTIuMzE2NCAxNDQuMzYxIDEyLjMwMDggMTQ0LjEzNyAxMi4yNjk1IDE0My45NDRDMTIuMjQwOSAxNDMuNzQ5IDEyLjE5NzkgMTQzLjU4MiAxMi4xNDA2IDE0My40NDRDMTIuMDgzMyAxNDMuMzA2IDEyLjAxMDQgMTQzLjE5NCAxMS45MjE5IDE0My4xMDhDMTEuODM1OSAxNDMuMDIyIDExLjczNTcgMTQyLjk2IDExLjYyMTEgMTQyLjkyMUMxMS41MDkxIDE0Mi44NzkgMTEuMzgyOCAxNDIuODU4IDExLjI0MjIgMTQyLjg1OEMxMS4wNzAzIDE0Mi44NTggMTAuOTE4IDE0Mi44OTEgMTAuNzg1MiAxNDIuOTU2QzEwLjY1MjMgMTQzLjAxOSAxMC41NDA0IDE0My4xMTkgMTAuNDQ5MiAxNDMuMjU3QzEwLjM2MDcgMTQzLjM5NSAxMC4yOTMgMTQzLjU3NiAxMC4yNDYxIDE0My44QzEwLjE5OTIgMTQ0LjAyNCAxMC4xNzU4IDE0NC4yOTYgMTAuMTc1OCAxNDQuNjE2VjE0NS43MjJDMTAuMTc1OCAxNDUuOTc3IDEwLjE5MDEgMTQ2LjIwMiAxMC4yMTg4IDE0Ni4zOTdDMTAuMjUgMTQ2LjU5MyAxMC4yOTU2IDE0Ni43NjIgMTAuMzU1NSAxNDYuOTA1QzEwLjQxNTQgMTQ3LjA0NiAxMC40ODgzIDE0Ny4xNjIgMTAuNTc0MiAxNDcuMjUzQzEwLjY2MDIgMTQ3LjM0NCAxMC43NTkxIDE0Ny40MTIgMTAuODcxMSAxNDcuNDU2QzEwLjk4NTcgMTQ3LjQ5OCAxMS4xMTIgMTQ3LjUxOSAxMS4yNSAxNDcuNTE5QzExLjQyNzEgMTQ3LjUxOSAxMS41ODIgMTQ3LjQ4NSAxMS43MTQ4IDE0Ny40MTdDMTEuODQ3NyAxNDcuMzQ5IDExLjk1ODMgMTQ3LjI0NCAxMi4wNDY5IDE0Ny4xMDFDMTIuMTM4IDE0Ni45NTUgMTIuMjA1NyAxNDYuNzY5IDEyLjI1IDE0Ni41NDJDMTIuMjk0MyAxNDYuMzEzIDEyLjMxNjQgMTQ2LjAzOSAxMi4zMTY0IDE0NS43MjJaTTE0LjA0MjQgMTQzLjc0MVYxNDMuNDRDMTQuMDQyNCAxNDMuMjI0IDE0LjA4OTIgMTQzLjAyOCAxNC4xODMgMTQyLjg1MUMxNC4yNzY3IDE0Mi42NzQgMTQuNDEwOCAxNDIuNTMyIDE0LjU4NTMgMTQyLjQyNUMxNC43NTk4IDE0Mi4zMTggMTQuOTY2OCAxNDIuMjY1IDE1LjIwNjQgMTQyLjI2NUMxNS40NTEyIDE0Mi4yNjUgMTUuNjU5NSAxNDIuMzE4IDE1LjgzMTQgMTQyLjQyNUMxNi4wMDU5IDE0Mi41MzIgMTYuMTQgMTQyLjY3NCAxNi4yMzM4IDE0Mi44NTFDMTYuMzI3NSAxNDMuMDI4IDE2LjM3NDQgMTQzLjIyNCAxNi4zNzQ0IDE0My40NFYxNDMuNzQxQzE2LjM3NDQgMTQzLjk1MiAxNi4zMjc1IDE0NC4xNDYgMTYuMjMzOCAxNDQuMzIzQzE2LjE0MjYgMTQ0LjUgMTYuMDA5OCAxNDQuNjQyIDE1LjgzNTMgMTQ0Ljc0OUMxNS42NjM1IDE0NC44NTYgMTUuNDU2NCAxNDQuOTA5IDE1LjIxNDIgMTQ0LjkwOUMxNC45NzIgMTQ0LjkwOSAxNC43NjI0IDE0NC44NTYgMTQuNTg1MyAxNDQuNzQ5QzE0LjQxMDggMTQ0LjY0MiAxNC4yNzY3IDE0NC41IDE0LjE4MyAxNDQuMzIzQzE0LjA4OTIgMTQ0LjE0NiAxNC4wNDI0IDE0My45NTIgMTQuMDQyNCAxNDMuNzQxWk0xNC41ODUzIDE0My40NFYxNDMuNzQxQzE0LjU4NTMgMTQzLjg2MSAxNC42MDc1IDE0My45NzQgMTQuNjUxNyAxNDQuMDgxQzE0LjY5ODYgMTQ0LjE4OCAxNC43Njg5IDE0NC4yNzUgMTQuODYyNyAxNDQuMzQzQzE0Ljk1NjQgMTQ0LjQwOCAxNS4wNzM2IDE0NC40NCAxNS4yMTQyIDE0NC40NEMxNS4zNTQ5IDE0NC40NCAxNS40NzA3IDE0NC40MDggMTUuNTYxOSAxNDQuMzQzQzE1LjY1MyAxNDQuMjc1IDE1LjcyMDcgMTQ0LjE4OCAxNS43NjUgMTQ0LjA4MUMxNS44MDkzIDE0My45NzQgMTUuODMxNCAxNDMuODYxIDE1LjgzMTQgMTQzLjc0MVYxNDMuNDRDMTUuODMxNCAxNDMuMzE4IDE1LjgwOCAxNDMuMjAzIDE1Ljc2MTEgMTQzLjA5N0MxNS43MTY4IDE0Mi45ODcgMTUuNjQ3OCAxNDIuOSAxNS41NTQxIDE0Mi44MzVDMTUuNDYyOSAxNDIuNzY3IDE1LjM0NyAxNDIuNzMzIDE1LjIwNjQgMTQyLjczM0MxNS4wNjg0IDE0Mi43MzMgMTQuOTUyNSAxNDIuNzY3IDE0Ljg1ODggMTQyLjgzNUMxNC43Njc2IDE0Mi45IDE0LjY5ODYgMTQyLjk4NyAxNC42NTE3IDE0My4wOTdDMTQuNjA3NSAxNDMuMjAzIDE0LjU4NTMgMTQzLjMxOCAxNC41ODUzIDE0My40NFpNMTYuODExOSAxNDYuOTQ0VjE0Ni42NEMxNi44MTE5IDE0Ni40MjYgMTYuODU4OCAxNDYuMjMxIDE2Ljk1MjUgMTQ2LjA1NEMxNy4wNDYzIDE0NS44NzcgMTcuMTgwNCAxNDUuNzM1IDE3LjM1NDkgMTQ1LjYyOEMxNy41MjkzIDE0NS41MjEgMTcuNzM2NCAxNDUuNDY4IDE3Ljk3NiAxNDUuNDY4QzE4LjIyMDcgMTQ1LjQ2OCAxOC40MjkxIDE0NS41MjEgMTguNjAxIDE0NS42MjhDMTguNzc1NCAxNDUuNzM1IDE4LjkwOTUgMTQ1Ljg3NyAxOS4wMDMzIDE0Ni4wNTRDMTkuMDk3IDE0Ni4yMzEgMTkuMTQzOSAxNDYuNDI2IDE5LjE0MzkgMTQ2LjY0VjE0Ni45NDRDMTkuMTQzOSAxNDcuMTU4IDE5LjA5NyAxNDcuMzUzIDE5LjAwMzMgMTQ3LjUzQzE4LjkxMjIgMTQ3LjcwNyAxOC43NzkzIDE0Ny44NDkgMTguNjA0OSAxNDcuOTU2QzE4LjQzMyAxNDguMDYzIDE4LjIyNiAxNDguMTE2IDE3Ljk4MzggMTQ4LjExNkMxNy43NDE2IDE0OC4xMTYgMTcuNTMzMiAxNDguMDYzIDE3LjM1ODggMTQ3Ljk1NkMxNy4xODQzIDE0Ny44NDkgMTcuMDQ4OSAxNDcuNzA3IDE2Ljk1MjUgMTQ3LjUzQzE2Ljg1ODggMTQ3LjM1MyAxNi44MTE5IDE0Ny4xNTggMTYuODExOSAxNDYuOTQ0Wk0xNy4zNTQ5IDE0Ni42NFYxNDYuOTQ0QzE3LjM1NDkgMTQ3LjA2NCAxNy4zNzcgMTQ3LjE3OSAxNy40MjEzIDE0Ny4yODhDMTcuNDY4MSAxNDcuMzk1IDE3LjUzODUgMTQ3LjQ4MiAxNy42MzIyIDE0Ny41NUMxNy43MjYgMTQ3LjYxNSAxNy44NDMxIDE0Ny42NDcgMTcuOTgzOCAxNDcuNjQ3QzE4LjEyNDQgMTQ3LjY0NyAxOC4yNDAzIDE0Ny42MTUgMTguMzMxNCAxNDcuNTVDMTguNDI1MiAxNDcuNDgyIDE4LjQ5NDIgMTQ3LjM5NSAxOC41Mzg1IDE0Ny4yODhDMTguNTgyNyAxNDcuMTgxIDE4LjYwNDkgMTQ3LjA2NyAxOC42MDQ5IDE0Ni45NDRWMTQ2LjY0QzE4LjYwNDkgMTQ2LjUxNyAxOC41ODE0IDE0Ni40MDMgMTguNTM0NSAxNDYuMjk2QzE4LjQ5MDMgMTQ2LjE4OSAxOC40MjEzIDE0Ni4xMDMgMTguMzI3NSAxNDYuMDM4QzE4LjIzNjQgMTQ1Ljk3IDE4LjExOTIgMTQ1LjkzNyAxNy45NzYgMTQ1LjkzN0MxNy44Mzc5IDE0NS45MzcgMTcuNzIyIDE0NS45NyAxNy42MjgzIDE0Ni4wMzhDMTcuNTM3MiAxNDYuMTAzIDE3LjQ2ODEgMTQ2LjE4OSAxNy40MjEzIDE0Ni4yOTZDMTcuMzc3IDE0Ni40MDMgMTcuMzU0OSAxNDYuNTE3IDE3LjM1NDkgMTQ2LjY0Wk0xOC4xNTU2IDE0My4xNTVMMTUuMzc4MyAxNDcuNjAxTDE0Ljk3MiAxNDcuMzQzTDE3Ljc0OTQgMTQyLjg5N0wxOC4xNTU2IDE0My4xNTVaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjU0Ii8+CjxwYXRoIGQ9Ik0xMzggNThDMTM4IDU2Ljg5NTQgMTM4Ljg5NSA1NiAxNDAgNTZIMTU0QzE1NS4xMDUgNTYgMTU2IDU2Ljg5NTQgMTU2IDU4VjE0NkgxMzhWNThaIiBmaWxsPSIjM0ZBNzFBIi8+CjxwYXRoIGQ9Ik0yNSA0LjE2MTEzTDIwMCA0LjE2MTE2IiBzdHJva2U9ImJsYWNrIiBzdHJva2Utb3BhY2l0eT0iMC4xMiIvPgo8cGF0aCBkPSJNMjUgMzMuMTYxMUwyMDAgMzMuMTYxMiIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLW9wYWNpdHk9IjAuMTIiLz4KPHBhdGggZD0iTTI1IDYxLjE2MTFMMjAwIDYxLjE2MTIiIHN0cm9rZT0iYmxhY2siIHN0cm9rZS1vcGFjaXR5PSIwLjEyIi8+CjxwYXRoIGQ9Ik0yNSA4OS4xNjExTDIwMCA4OS4xNjEyIiBzdHJva2U9ImJsYWNrIiBzdHJva2Utb3BhY2l0eT0iMC4xMiIvPgo8cGF0aCBkPSJNMjUgMTE4LjE2MUwyMDAgMTE4LjE2MSIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLW9wYWNpdHk9IjAuMTIiLz4KPGxpbmUgeDE9IjIzLjIiIHkxPSIxNDUuOTYxIiB4Mj0iMjAyLjgiIHkyPSIxNDUuOTYxIiBzdHJva2U9ImJsYWNrIiBzdHJva2Utb3BhY2l0eT0iMC43IiBzdHJva2Utd2lkdGg9IjAuNCIgc3Ryb2tlLWxpbmVjYXA9InNxdWFyZSIvPgo8bGluZSB4MT0iNDAuNDUwMiIgeTE9IjE0OC4wNzIiIHgyPSI0MC40NTAyIiB5Mj0iMTQ3LjI1IiBzdHJva2U9ImJsYWNrIiBzdHJva2Utb3BhY2l0eT0iMC41IiBzdHJva2Utd2lkdGg9IjAuNSIgc3Ryb2tlLWxpbmVjYXA9InNxdWFyZSIvPgo8cGF0aCBkPSJNMzIuNTA5MSAxNTIuMDI1VjE1Mi44OTNDMzIuNTA5MSAxNTMuMzU5IDMyLjQ2NzQgMTUzLjc1MiAzMi4zODQxIDE1NC4wNzJDMzIuMzAwNyAxNTQuMzkzIDMyLjE4MDkgMTU0LjY1IDMyLjAyNDcgMTU0Ljg0NkMzMS44Njg0IDE1NS4wNDEgMzEuNjc5NiAxNTUuMTgzIDMxLjQ1ODMgMTU1LjI3MUMzMS4yMzk1IDE1NS4zNTcgMzAuOTkyMSAxNTUuNCAzMC43MTYxIDE1NS40QzMwLjQ5NzMgMTU1LjQgMzAuMjk1NSAxNTUuMzczIDMwLjExMDYgMTU1LjMxOEMyOS45MjU3IDE1NS4yNjQgMjkuNzU5MSAxNTUuMTc2IDI5LjYxMDYgMTU1LjA1N0MyOS40NjQ4IDE1NC45MzQgMjkuMzM5OCAxNTQuNzc1IDI5LjIzNTYgMTU0LjU4QzI5LjEzMTUgMTU0LjM4NSAyOS4wNTIgMTU0LjE0OCAyOC45OTczIDE1My44NjlDMjguOTQyNyAxNTMuNTkgMjguOTE1MyAxNTMuMjY1IDI4LjkxNTMgMTUyLjg5M1YxNTIuMDI1QzI4LjkxNTMgMTUxLjU1OSAyOC45NTcgMTUxLjE2OSAyOS4wNDAzIDE1MC44NTRDMjkuMTI2MiAxNTAuNTM4IDI5LjI0NzMgMTUwLjI4NiAyOS40MDM2IDE1MC4wOTZDMjkuNTU5OCAxNDkuOTAzIDI5Ljc0NzMgMTQ5Ljc2NSAyOS45NjYxIDE0OS42ODJDMzAuMTg3NCAxNDkuNTk4IDMwLjQzNDggMTQ5LjU1NyAzMC43MDgzIDE0OS41NTdDMzAuOTI5NiAxNDkuNTU3IDMxLjEzMjggMTQ5LjU4NCAzMS4zMTc3IDE0OS42MzlDMzEuNTA1MiAxNDkuNjkxIDMxLjY3MTggMTQ5Ljc3NSAzMS44MTc3IDE0OS44OTNDMzEuOTYzNSAxNTAuMDA3IDMyLjA4NzIgMTUwLjE2MSAzMi4xODg3IDE1MC4zNTRDMzIuMjkyOSAxNTAuNTQ0IDMyLjM3MjMgMTUwLjc3NyAzMi40MjcgMTUxLjA1M0MzMi40ODE3IDE1MS4zMjkgMzIuNTA5MSAxNTEuNjUzIDMyLjUwOTEgMTUyLjAyNVpNMzEuNzgyNSAxNTMuMDFWMTUxLjkwNEMzMS43ODI1IDE1MS42NDkgMzEuNzY2OSAxNTEuNDI1IDMxLjczNTYgMTUxLjIzMkMzMS43MDcgMTUxLjAzNyAzMS42NjQgMTUwLjg3IDMxLjYwNjcgMTUwLjczMkMzMS41NDk0IDE1MC41OTQgMzEuNDc2NSAxNTAuNDgyIDMxLjM4OCAxNTAuMzk2QzMxLjMwMiAxNTAuMzExIDMxLjIwMTggMTUwLjI0OCAzMS4wODcyIDE1MC4yMDlDMzAuOTc1MiAxNTAuMTY3IDMwLjg0ODkgMTUwLjE0NiAzMC43MDgzIDE1MC4xNDZDMzAuNTM2NCAxNTAuMTQ2IDMwLjM4NDEgMTUwLjE3OSAzMC4yNTEyIDE1MC4yNDRDMzAuMTE4NCAxNTAuMzA3IDMwLjAwNjUgMTUwLjQwNyAyOS45MTUzIDE1MC41NDVDMjkuODI2OCAxNTAuNjgzIDI5Ljc1OTEgMTUwLjg2NCAyOS43MTIyIDE1MS4wODhDMjkuNjY1MyAxNTEuMzEyIDI5LjY0MTkgMTUxLjU4NCAyOS42NDE5IDE1MS45MDRWMTUzLjAxQzI5LjY0MTkgMTUzLjI2NSAyOS42NTYyIDE1My40OSAyOS42ODQ4IDE1My42ODZDMjkuNzE2MSAxNTMuODgxIDI5Ljc2MTcgMTU0LjA1IDI5LjgyMTYgMTU0LjE5M0MyOS44ODE1IDE1NC4zMzQgMjkuOTU0NCAxNTQuNDUgMzAuMDQwMyAxNTQuNTQxQzMwLjEyNjIgMTU0LjYzMiAzMC4yMjUyIDE1NC43IDMwLjMzNzIgMTU0Ljc0NEMzMC40NTE4IDE1NC43ODYgMzAuNTc4MSAxNTQuODA3IDMwLjcxNjEgMTU0LjgwN0MzMC44OTMyIDE1NC44MDcgMzEuMDQ4MSAxNTQuNzczIDMxLjE4MDkgMTU0LjcwNUMzMS4zMTM3IDE1NC42MzcgMzEuNDI0NCAxNTQuNTMyIDMxLjUxMyAxNTQuMzg5QzMxLjYwNDEgMTU0LjI0MyAzMS42NzE4IDE1NC4wNTcgMzEuNzE2MSAxNTMuODNDMzEuNzYwNCAxNTMuNjAxIDMxLjc4MjUgMTUzLjMyNyAzMS43ODI1IDE1My4wMVpNMzUuODk2NCAxNDkuNjA0VjE1NS4zMjJIMzUuMTczN1YxNTAuNTA2TDMzLjcxNjcgMTUxLjAzN1YxNTAuMzg1TDM1Ljc4MzEgMTQ5LjYwNEgzNS44OTY0Wk00MS4xMTI0IDE0OS42MzVWMTU1LjMyMkg0MC4zNTg1VjE0OS42MzVINDEuMTEyNFpNNDMuNDk1MiAxNTIuMTkzVjE1Mi44MTFINDAuOTQ4M1YxNTIuMTkzSDQzLjQ5NTJaTTQzLjg4MTkgMTQ5LjYzNVYxNTAuMjUySDQwLjk0ODNWMTQ5LjYzNUg0My44ODE5Wk00Ni40MjE2IDE1NS40QzQ2LjEyNzMgMTU1LjQgNDUuODYwNCAxNTUuMzUxIDQ1LjYyMDggMTU1LjI1MkM0NS4zODM4IDE1NS4xNSA0NS4xNzk0IDE1NS4wMDggNDUuMDA3NSAxNTQuODI2QzQ0LjgzODMgMTU0LjY0NCA0NC43MDgxIDE1NC40MjggNDQuNjE2OSAxNTQuMTc4QzQ0LjUyNTggMTUzLjkyOCA0NC40ODAyIDE1My42NTQgNDQuNDgwMiAxNTMuMzU3VjE1My4xOTNDNDQuNDgwMiAxNTIuODUgNDQuNTMxIDE1Mi41NDQgNDQuNjMyNSAxNTIuMjc1QzQ0LjczNDEgMTUyLjAwNSA0NC44NzIxIDE1MS43NzUgNDUuMDQ2NiAxNTEuNTg4QzQ1LjIyMTEgMTUxLjQgNDUuNDE5IDE1MS4yNTggNDUuNjQwMyAxNTEuMTYyQzQ1Ljg2MTcgMTUxLjA2NiA0Ni4wOTA5IDE1MS4wMTggNDYuMzI3OCAxNTEuMDE4QzQ2LjYyOTkgMTUxLjAxOCA0Ni44OTAzIDE1MS4wNyA0Ny4xMDkxIDE1MS4xNzRDNDcuMzMwNSAxNTEuMjc4IDQ3LjUxMTQgMTUxLjQyNCA0Ny42NTIxIDE1MS42MTFDNDcuNzkyNyAxNTEuNzk2IDQ3Ljg5NjkgMTUyLjAxNSA0Ny45NjQ2IDE1Mi4yNjhDNDguMDMyMyAxNTIuNTE4IDQ4LjA2NjEgMTUyLjc5MSA0OC4wNjYxIDE1My4wODhWMTUzLjQxMkg0NC45MDk5VjE1Mi44MjJINDcuMzQzNVYxNTIuNzY4QzQ3LjMzMzEgMTUyLjU4IDQ3LjI5NCAxNTIuMzk4IDQ3LjIyNjMgMTUyLjIyMUM0Ny4xNjEyIDE1Mi4wNDQgNDcuMDU3IDE1MS44OTggNDYuOTEzOCAxNTEuNzgzQzQ2Ljc3MDYgMTUxLjY2OSA0Ni41NzUyIDE1MS42MTEgNDYuMzI3OCAxNTEuNjExQzQ2LjE2MzggMTUxLjYxMSA0Ni4wMTI3IDE1MS42NDYgNDUuODc0NyAxNTEuNzE3QzQ1LjczNjcgMTUxLjc4NSA0NS42MTgyIDE1MS44ODYgNDUuNTE5MyAxNTIuMDIxQzQ1LjQyMDMgMTUyLjE1NyA0NS4zNDM1IDE1Mi4zMjIgNDUuMjg4OCAxNTIuNTE4QzQ1LjIzNDEgMTUyLjcxMyA0NS4yMDY4IDE1Mi45MzggNDUuMjA2OCAxNTMuMTkzVjE1My4zNTdDNDUuMjA2OCAxNTMuNTU4IDQ1LjIzNDEgMTUzLjc0NyA0NS4yODg4IDE1My45MjRDNDUuMzQ2MSAxNTQuMDk4IDQ1LjQyODEgMTU0LjI1MiA0NS41MzQ5IDE1NC4zODVDNDUuNjQ0MyAxNTQuNTE4IDQ1Ljc3NTggMTU0LjYyMiA0NS45Mjk0IDE1NC42OTdDNDYuMDg1NyAxNTQuNzczIDQ2LjI2MjcgMTU0LjgxMSA0Ni40NjA3IDE1NC44MTFDNDYuNzE1OSAxNTQuODExIDQ2LjkzMiAxNTQuNzU4IDQ3LjEwOTEgMTU0LjY1NEM0Ny4yODYyIDE1NC41NSA0Ny40NDExIDE1NC40MTEgNDcuNTczOSAxNTQuMjM2TDQ4LjAxMTQgMTU0LjU4NEM0Ny45MjAzIDE1NC43MjIgNDcuODA0NCAxNTQuODU0IDQ3LjY2MzggMTU0Ljk3OUM0Ny41MjMyIDE1NS4xMDQgNDcuMzUgMTU1LjIwNSA0Ny4xNDQzIDE1NS4yODNDNDYuOTQxMSAxNTUuMzYxIDQ2LjcwMDIgMTU1LjQgNDYuNDIxNiAxNTUuNFpNNDguOTg4NiAxNDkuMzIySDQ5LjcxNTJWMTU0LjUwMkw0OS42NTI3IDE1NS4zMjJINDguOTg4NlYxNDkuMzIyWk01Mi41NzA2IDE1My4xNzRWMTUzLjI1NkM1Mi41NzA2IDE1My41NjMgNTIuNTM0MiAxNTMuODQ4IDUyLjQ2MTMgMTU0LjExMUM1Mi4zODgzIDE1NC4zNzIgNTIuMjgxNiAxNTQuNTk4IDUyLjE0MDkgMTU0Ljc5MUM1Mi4wMDAzIDE1NC45ODQgNTEuODI4NCAxNTUuMTMzIDUxLjYyNTMgMTU1LjI0QzUxLjQyMjIgMTU1LjM0NyA1MS4xODkxIDE1NS40IDUwLjkyNjEgMTU1LjRDNTAuNjU3OSAxNTUuNCA1MC40MjIyIDE1NS4zNTUgNTAuMjE5MSAxNTUuMjY0QzUwLjAxODUgMTU1LjE3IDQ5Ljg0OTMgMTU1LjAzNiA0OS43MTEzIDE1NC44NjFDNDkuNTczMiAxNTQuNjg3IDQ5LjQ2MjYgMTU0LjQ3NiA0OS4zNzkyIDE1NC4yMjlDNDkuMjk4NSAxNTMuOTgxIDQ5LjI0MjUgMTUzLjcwMiA0OS4yMTEzIDE1My4zOTNWMTUzLjAzM0M0OS4yNDI1IDE1Mi43MjEgNDkuMjk4NSAxNTIuNDQxIDQ5LjM3OTIgMTUyLjE5M0M0OS40NjI2IDE1MS45NDYgNDkuNTczMiAxNTEuNzM1IDQ5LjcxMTMgMTUxLjU2MUM0OS44NDkzIDE1MS4zODMgNTAuMDE4NSAxNTEuMjQ5IDUwLjIxOTEgMTUxLjE1OEM1MC40MTk2IDE1MS4wNjQgNTAuNjUyNyAxNTEuMDE4IDUwLjkxODMgMTUxLjAxOEM1MS4xODM5IDE1MS4wMTggNTEuNDE5NiAxNTEuMDcgNTEuNjI1MyAxNTEuMTc0QzUxLjgzMSAxNTEuMjc1IDUyLjAwMjkgMTUxLjQyMSA1Mi4xNDA5IDE1MS42MTFDNTIuMjgxNiAxNTEuODAxIDUyLjM4ODMgMTUyLjAyOSA1Mi40NjEzIDE1Mi4yOTVDNTIuNTM0MiAxNTIuNTU4IDUyLjU3MDYgMTUyLjg1MSA1Mi41NzA2IDE1My4xNzRaTTUxLjg0NDEgMTUzLjI1NlYxNTMuMTc0QzUxLjg0NDEgMTUyLjk2MyA1MS44MjQ1IDE1Mi43NjUgNTEuNzg1NSAxNTIuNThDNTEuNzQ2NCAxNTIuMzkzIDUxLjY4MzkgMTUyLjIyOSA1MS41OTggMTUyLjA4OEM1MS41MTIgMTUxLjk0NSA1MS4zOTg4IDE1MS44MzMgNTEuMjU4MSAxNTEuNzUyQzUxLjExNzUgMTUxLjY2OSA1MC45NDQzIDE1MS42MjcgNTAuNzM4NiAxNTEuNjI3QzUwLjU1NjMgMTUxLjYyNyA1MC4zOTc1IDE1MS42NTggNTAuMjYyIDE1MS43MjFDNTAuMTI5MiAxNTEuNzgzIDUwLjAxNTkgMTUxLjg2OCA0OS45MjIyIDE1MS45NzVDNDkuODI4NCAxNTIuMDc5IDQ5Ljc1MTYgMTUyLjE5OSA0OS42OTE3IDE1Mi4zMzRDNDkuNjM0NCAxNTIuNDY3IDQ5LjU5MTUgMTUyLjYwNSA0OS41NjI4IDE1Mi43NDhWMTUzLjY4OUM0OS42MDQ1IDE1My44NzIgNDkuNjcyMiAxNTQuMDQ4IDQ5Ljc2NTkgMTU0LjIxN0M0OS44NjIzIDE1NC4zODMgNDkuOTg5OSAxNTQuNTIgNTAuMTQ4OCAxNTQuNjI3QzUwLjMxMDIgMTU0LjczNCA1MC41MDk0IDE1NC43ODcgNTAuNzQ2NCAxNTQuNzg3QzUwLjk0MTcgMTU0Ljc4NyA1MS4xMDg0IDE1NC43NDggNTEuMjQ2NCAxNTQuNjdDNTEuMzg3IDE1NC41ODkgNTEuNTAwMyAxNTQuNDc5IDUxLjU4NjMgMTU0LjMzOEM1MS42NzQ4IDE1NC4xOTcgNTEuNzM5OSAxNTQuMDM1IDUxLjc4MTYgMTUzLjg1QzUxLjgyMzIgMTUzLjY2NSA1MS44NDQxIDE1My40NjcgNTEuODQ0MSAxNTMuMjU2WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC41NCIvPgo8ZyBjbGlwLXBhdGg9InVybCgjY2xpcDFfNDA1M18xODUxMTMpIj4KPGxpbmUgeDE9Ijc1Ljg1MDEiIHkxPSIxNDcuMDcyIiB4Mj0iNzUuODUwMSIgeTI9IjE0Ni4yNSIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLW9wYWNpdHk9IjAuNSIgc3Ryb2tlLXdpZHRoPSIwLjUiIHN0cm9rZS1saW5lY2FwPSJzcXVhcmUiLz4KPHBhdGggZD0iTTY3LjkwOSAxNTIuMDI1VjE1Mi44OTNDNjcuOTA5IDE1My4zNTkgNjcuODY3MyAxNTMuNzUyIDY3Ljc4NCAxNTQuMDcyQzY3LjcwMDYgMTU0LjM5MyA2Ny41ODA4IDE1NC42NSA2Ny40MjQ2IDE1NC44NDZDNjcuMjY4MyAxNTUuMDQxIDY3LjA3OTUgMTU1LjE4MyA2Ni44NTgyIDE1NS4yNzFDNjYuNjM5NCAxNTUuMzU3IDY2LjM5MiAxNTUuNCA2Ni4xMTYgMTU1LjRDNjUuODk3MiAxNTUuNCA2NS42OTU0IDE1NS4zNzMgNjUuNTEwNSAxNTUuMzE4QzY1LjMyNTYgMTU1LjI2NCA2NS4xNTkgMTU1LjE3NiA2NS4wMTA1IDE1NS4wNTdDNjQuODY0NyAxNTQuOTM0IDY0LjczOTcgMTU0Ljc3NSA2NC42MzU1IDE1NC41OEM2NC41MzE0IDE1NC4zODUgNjQuNDUxOSAxNTQuMTQ4IDY0LjM5NzIgMTUzLjg2OUM2NC4zNDI2IDE1My41OSA2NC4zMTUyIDE1My4yNjUgNjQuMzE1MiAxNTIuODkzVjE1Mi4wMjVDNjQuMzE1MiAxNTEuNTU5IDY0LjM1NjkgMTUxLjE2OSA2NC40NDAyIDE1MC44NTRDNjQuNTI2MSAxNTAuNTM4IDY0LjY0NzIgMTUwLjI4NiA2NC44MDM1IDE1MC4wOTZDNjQuOTU5NyAxNDkuOTAzIDY1LjE0NzIgMTQ5Ljc2NSA2NS4zNjYgMTQ5LjY4MkM2NS41ODczIDE0OS41OTggNjUuODM0NyAxNDkuNTU3IDY2LjEwODIgMTQ5LjU1N0M2Ni4zMjk1IDE0OS41NTcgNjYuNTMyNyAxNDkuNTg0IDY2LjcxNzYgMTQ5LjYzOUM2Ni45MDUxIDE0OS42OTEgNjcuMDcxNyAxNDkuNzc1IDY3LjIxNzYgMTQ5Ljg5M0M2Ny4zNjM0IDE1MC4wMDcgNjcuNDg3MSAxNTAuMTYxIDY3LjU4ODYgMTUwLjM1NEM2Ny42OTI4IDE1MC41NDQgNjcuNzcyMiAxNTAuNzc3IDY3LjgyNjkgMTUxLjA1M0M2Ny44ODE2IDE1MS4zMjkgNjcuOTA5IDE1MS42NTMgNjcuOTA5IDE1Mi4wMjVaTTY3LjE4MjQgMTUzLjAxVjE1MS45MDRDNjcuMTgyNCAxNTEuNjQ5IDY3LjE2NjggMTUxLjQyNSA2Ny4xMzU1IDE1MS4yMzJDNjcuMTA2OSAxNTEuMDM3IDY3LjA2MzkgMTUwLjg3IDY3LjAwNjYgMTUwLjczMkM2Ni45NDkzIDE1MC41OTQgNjYuODc2NCAxNTAuNDgyIDY2Ljc4NzkgMTUwLjM5NkM2Ni43MDE5IDE1MC4zMTEgNjYuNjAxNyAxNTAuMjQ4IDY2LjQ4NzEgMTUwLjIwOUM2Ni4zNzUxIDE1MC4xNjcgNjYuMjQ4OCAxNTAuMTQ2IDY2LjEwODIgMTUwLjE0NkM2NS45MzYzIDE1MC4xNDYgNjUuNzg0IDE1MC4xNzkgNjUuNjUxMSAxNTAuMjQ0QzY1LjUxODMgMTUwLjMwNyA2NS40MDY0IDE1MC40MDcgNjUuMzE1MiAxNTAuNTQ1QzY1LjIyNjcgMTUwLjY4MyA2NS4xNTkgMTUwLjg2NCA2NS4xMTIxIDE1MS4wODhDNjUuMDY1MiAxNTEuMzEyIDY1LjA0MTggMTUxLjU4NCA2NS4wNDE4IDE1MS45MDRWMTUzLjAxQzY1LjA0MTggMTUzLjI2NSA2NS4wNTYxIDE1My40OSA2NS4wODQ3IDE1My42ODZDNjUuMTE2IDE1My44ODEgNjUuMTYxNiAxNTQuMDUgNjUuMjIxNSAxNTQuMTkzQzY1LjI4MTQgMTU0LjMzNCA2NS4zNTQzIDE1NC40NSA2NS40NDAyIDE1NC41NDFDNjUuNTI2MSAxNTQuNjMyIDY1LjYyNTEgMTU0LjcgNjUuNzM3MSAxNTQuNzQ0QzY1Ljg1MTcgMTU0Ljc4NiA2NS45NzggMTU0LjgwNyA2Ni4xMTYgMTU0LjgwN0M2Ni4yOTMxIDE1NC44MDcgNjYuNDQ4IDE1NC43NzMgNjYuNTgwOCAxNTQuNzA1QzY2LjcxMzYgMTU0LjYzNyA2Ni44MjQzIDE1NC41MzIgNjYuOTEyOSAxNTQuMzg5QzY3LjAwNCAxNTQuMjQzIDY3LjA3MTcgMTU0LjA1NyA2Ny4xMTYgMTUzLjgzQzY3LjE2MDMgMTUzLjYwMSA2Ny4xODI0IDE1My4zMjcgNjcuMTgyNCAxNTMuMDFaTTcyLjY0NzggMTU0LjcyOVYxNTUuMzIySDY4LjkyNTJWMTU0LjgwM0w3MC43ODg1IDE1Mi43MjlDNzEuMDE3NiAxNTIuNDczIDcxLjE5NDcgMTUyLjI1NyA3MS4zMTk3IDE1Mi4wOEM3MS40NDczIDE1MS45IDcxLjUzNTkgMTUxLjc0IDcxLjU4NTMgMTUxLjZDNzEuNjM3NCAxNTEuNDU2IDcxLjY2MzUgMTUxLjMxMSA3MS42NjM1IDE1MS4xNjJDNzEuNjYzNSAxNTAuOTc1IDcxLjYyNDQgMTUwLjgwNSA3MS41NDYzIDE1MC42NTRDNzEuNDcwOCAxNTAuNTAxIDcxLjM1ODggMTUwLjM3OCA3MS4yMTAzIDE1MC4yODdDNzEuMDYxOSAxNTAuMTk2IDcwLjg4MjIgMTUwLjE1IDcwLjY3MTMgMTUwLjE1QzcwLjQxODcgMTUwLjE1IDcwLjIwNzcgMTUwLjIgNzAuMDM4NSAxNTAuMjk5QzY5Ljg3MTggMTUwLjM5NSA2OS43NDY4IDE1MC41MzEgNjkuNjYzNSAxNTAuNzA1QzY5LjU4MDEgMTUwLjg4IDY5LjUzODUgMTUxLjA4IDY5LjUzODUgMTUxLjMwN0g2OC44MTU4QzY4LjgxNTggMTUwLjk4NiA2OC44ODYxIDE1MC42OTMgNjkuMDI2NyAxNTAuNDI4QzY5LjE2NzQgMTUwLjE2MiA2OS4zNzU3IDE0OS45NTEgNjkuNjUxNyAxNDkuNzk1QzY5LjkyNzggMTQ5LjYzNiA3MC4yNjc2IDE0OS41NTcgNzAuNjcxMyAxNDkuNTU3QzcxLjAzMDcgMTQ5LjU1NyA3MS4zMzc5IDE0OS42MiA3MS41OTMyIDE0OS43NDhDNzEuODQ4NCAxNDkuODczIDcyLjA0MzcgMTUwLjA1IDcyLjE3OTEgMTUwLjI3OUM3Mi4zMTcxIDE1MC41MDYgNzIuMzg2MSAxNTAuNzcxIDcyLjM4NjEgMTUxLjA3NkM3Mi4zODYxIDE1MS4yNDMgNzIuMzU3NSAxNTEuNDEyIDcyLjMwMDIgMTUxLjU4NEM3Mi4yNDU1IDE1MS43NTMgNzIuMTY4NyAxNTEuOTIzIDcyLjA2OTcgMTUyLjA5MkM3MS45NzM0IDE1Mi4yNjEgNzEuODYwMSAxNTIuNDI4IDcxLjcyOTkgMTUyLjU5MkM3MS42MDIzIDE1Mi43NTYgNzEuNDY1NSAxNTIuOTE3IDcxLjMxOTcgMTUzLjA3Nkw2OS43OTYzIDE1NC43MjlINzIuNjQ3OFpNNzYuNTEyMyAxNDkuNjM1VjE1NS4zMjJINzUuNzU4NFYxNDkuNjM1SDc2LjUxMjNaTTc4Ljg5NTEgMTUyLjE5M1YxNTIuODExSDc2LjM0ODJWMTUyLjE5M0g3OC44OTUxWk03OS4yODE4IDE0OS42MzVWMTUwLjI1Mkg3Ni4zNDgyVjE0OS42MzVINzkuMjgxOFpNODEuODIxNSAxNTUuNEM4MS41MjcyIDE1NS40IDgxLjI2MDMgMTU1LjM1MSA4MS4wMjA3IDE1NS4yNTJDODAuNzgzNyAxNTUuMTUgODAuNTc5MyAxNTUuMDA4IDgwLjQwNzQgMTU0LjgyNkM4MC4yMzgyIDE1NC42NDQgODAuMTA4IDE1NC40MjggODAuMDE2OCAxNTQuMTc4Qzc5LjkyNTcgMTUzLjkyOCA3OS44ODAxIDE1My42NTQgNzkuODgwMSAxNTMuMzU3VjE1My4xOTNDNzkuODgwMSAxNTIuODUgNzkuOTMwOSAxNTIuNTQ0IDgwLjAzMjQgMTUyLjI3NUM4MC4xMzQgMTUyLjAwNSA4MC4yNzIgMTUxLjc3NSA4MC40NDY1IDE1MS41ODhDODAuNjIxIDE1MS40IDgwLjgxODkgMTUxLjI1OCA4MS4wNDAyIDE1MS4xNjJDODEuMjYxNiAxNTEuMDY2IDgxLjQ5MDggMTUxLjAxOCA4MS43Mjc3IDE1MS4wMThDODIuMDI5OCAxNTEuMDE4IDgyLjI5MDIgMTUxLjA3IDgyLjUwOSAxNTEuMTc0QzgyLjczMDQgMTUxLjI3OCA4Mi45MTEzIDE1MS40MjQgODMuMDUyIDE1MS42MTFDODMuMTkyNiAxNTEuNzk2IDgzLjI5NjggMTUyLjAxNSA4My4zNjQ1IDE1Mi4yNjhDODMuNDMyMiAxNTIuNTE4IDgzLjQ2NiAxNTIuNzkxIDgzLjQ2NiAxNTMuMDg4VjE1My40MTJIODAuMzA5OFYxNTIuODIySDgyLjc0MzRWMTUyLjc2OEM4Mi43MzMgMTUyLjU4IDgyLjY5MzkgMTUyLjM5OCA4Mi42MjYyIDE1Mi4yMjFDODIuNTYxMSAxNTIuMDQ0IDgyLjQ1NjkgMTUxLjg5OCA4Mi4zMTM3IDE1MS43ODNDODIuMTcwNSAxNTEuNjY5IDgxLjk3NTEgMTUxLjYxMSA4MS43Mjc3IDE1MS42MTFDODEuNTYzNyAxNTEuNjExIDgxLjQxMjYgMTUxLjY0NiA4MS4yNzQ2IDE1MS43MTdDODEuMTM2NiAxNTEuNzg1IDgxLjAxODEgMTUxLjg4NiA4MC45MTkyIDE1Mi4wMjFDODAuODIwMiAxNTIuMTU3IDgwLjc0MzQgMTUyLjMyMiA4MC42ODg3IDE1Mi41MThDODAuNjM0IDE1Mi43MTMgODAuNjA2NyAxNTIuOTM4IDgwLjYwNjcgMTUzLjE5M1YxNTMuMzU3QzgwLjYwNjcgMTUzLjU1OCA4MC42MzQgMTUzLjc0NyA4MC42ODg3IDE1My45MjRDODAuNzQ2IDE1NC4wOTggODAuODI4IDE1NC4yNTIgODAuOTM0OCAxNTQuMzg1QzgxLjA0NDIgMTU0LjUxOCA4MS4xNzU3IDE1NC42MjIgODEuMzI5MyAxNTQuNjk3QzgxLjQ4NTYgMTU0Ljc3MyA4MS42NjI2IDE1NC44MTEgODEuODYwNiAxNTQuODExQzgyLjExNTggMTU0LjgxMSA4Mi4zMzE5IDE1NC43NTggODIuNTA5IDE1NC42NTRDODIuNjg2MSAxNTQuNTUgODIuODQxIDE1NC40MTEgODIuOTczOCAxNTQuMjM2TDgzLjQxMTMgMTU0LjU4NEM4My4zMjAyIDE1NC43MjIgODMuMjA0MyAxNTQuODU0IDgzLjA2MzcgMTU0Ljk3OUM4Mi45MjMxIDE1NS4xMDQgODIuNzQ5OSAxNTUuMjA1IDgyLjU0NDIgMTU1LjI4M0M4Mi4zNDEgMTU1LjM2MSA4Mi4xMDAxIDE1NS40IDgxLjgyMTUgMTU1LjRaTTg0LjM4ODUgMTQ5LjMyMkg4NS4xMTUxVjE1NC41MDJMODUuMDUyNiAxNTUuMzIySDg0LjM4ODVWMTQ5LjMyMlpNODcuOTcwNSAxNTMuMTc0VjE1My4yNTZDODcuOTcwNSAxNTMuNTYzIDg3LjkzNDEgMTUzLjg0OCA4Ny44NjEyIDE1NC4xMTFDODcuNzg4MiAxNTQuMzcyIDg3LjY4MTUgMTU0LjU5OCA4Ny41NDA4IDE1NC43OTFDODcuNDAwMiAxNTQuOTg0IDg3LjIyODMgMTU1LjEzMyA4Ny4wMjUyIDE1NS4yNEM4Ni44MjIxIDE1NS4zNDcgODYuNTg5IDE1NS40IDg2LjMyNiAxNTUuNEM4Ni4wNTc4IDE1NS40IDg1LjgyMjEgMTU1LjM1NSA4NS42MTkgMTU1LjI2NEM4NS40MTg1IDE1NS4xNyA4NS4yNDkyIDE1NS4wMzYgODUuMTExMiAxNTQuODYxQzg0Ljk3MzEgMTU0LjY4NyA4NC44NjI1IDE1NC40NzYgODQuNzc5MSAxNTQuMjI5Qzg0LjY5ODQgMTUzLjk4MSA4NC42NDI0IDE1My43MDIgODQuNjExMiAxNTMuMzkzVjE1My4wMzNDODQuNjQyNCAxNTIuNzIxIDg0LjY5ODQgMTUyLjQ0MSA4NC43NzkxIDE1Mi4xOTNDODQuODYyNSAxNTEuOTQ2IDg0Ljk3MzEgMTUxLjczNSA4NS4xMTEyIDE1MS41NjFDODUuMjQ5MiAxNTEuMzgzIDg1LjQxODUgMTUxLjI0OSA4NS42MTkgMTUxLjE1OEM4NS44MTk1IDE1MS4wNjQgODYuMDUyNiAxNTEuMDE4IDg2LjMxODIgMTUxLjAxOEM4Ni41ODM4IDE1MS4wMTggODYuODE5NSAxNTEuMDcgODcuMDI1MiAxNTEuMTc0Qzg3LjIzMSAxNTEuMjc1IDg3LjQwMjggMTUxLjQyMSA4Ny41NDA4IDE1MS42MTFDODcuNjgxNSAxNTEuODAxIDg3Ljc4ODIgMTUyLjAyOSA4Ny44NjEyIDE1Mi4yOTVDODcuOTM0MSAxNTIuNTU4IDg3Ljk3MDUgMTUyLjg1MSA4Ny45NzA1IDE1My4xNzRaTTg3LjI0NCAxNTMuMjU2VjE1My4xNzRDODcuMjQ0IDE1Mi45NjMgODcuMjI0NCAxNTIuNzY1IDg3LjE4NTQgMTUyLjU4Qzg3LjE0NjMgMTUyLjM5MyA4Ny4wODM4IDE1Mi4yMjkgODYuOTk3OSAxNTIuMDg4Qzg2LjkxMTkgMTUxLjk0NSA4Ni43OTg3IDE1MS44MzMgODYuNjU4IDE1MS43NTJDODYuNTE3NCAxNTEuNjY5IDg2LjM0NDIgMTUxLjYyNyA4Ni4xMzg1IDE1MS42MjdDODUuOTU2MiAxNTEuNjI3IDg1Ljc5NzQgMTUxLjY1OCA4NS42NjE5IDE1MS43MjFDODUuNTI5MSAxNTEuNzgzIDg1LjQxNTggMTUxLjg2OCA4NS4zMjIxIDE1MS45NzVDODUuMjI4MyAxNTIuMDc5IDg1LjE1MTUgMTUyLjE5OSA4NS4wOTE2IDE1Mi4zMzRDODUuMDM0MyAxNTIuNDY3IDg0Ljk5MTQgMTUyLjYwNSA4NC45NjI3IDE1Mi43NDhWMTUzLjY4OUM4NS4wMDQ0IDE1My44NzIgODUuMDcyMSAxNTQuMDQ4IDg1LjE2NTggMTU0LjIxN0M4NS4yNjIyIDE1NC4zODMgODUuMzg5OCAxNTQuNTIgODUuNTQ4NyAxNTQuNjI3Qzg1LjcxMDEgMTU0LjczNCA4NS45MDkzIDE1NC43ODcgODYuMTQ2MyAxNTQuNzg3Qzg2LjM0MTYgMTU0Ljc4NyA4Ni41MDgzIDE1NC43NDggODYuNjQ2MyAxNTQuNjdDODYuNzg2OSAxNTQuNTg5IDg2LjkwMDIgMTU0LjQ3OSA4Ni45ODYyIDE1NC4zMzhDODcuMDc0NyAxNTQuMTk3IDg3LjEzOTggMTU0LjAzNSA4Ny4xODE1IDE1My44NUM4Ny4yMjMxIDE1My42NjUgODcuMjQ0IDE1My40NjcgODcuMjQ0IDE1My4yNTZaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjU0Ii8+CjwvZz4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAyXzQwNTNfMTg1MTEzKSI+CjxsaW5lIHgxPSIxMTEuMjUiIHkxPSIxNDcuMDcyIiB4Mj0iMTExLjI1IiB5Mj0iMTQ2LjI1IiBzdHJva2U9ImJsYWNrIiBzdHJva2Utb3BhY2l0eT0iMC41IiBzdHJva2Utd2lkdGg9IjAuNSIgc3Ryb2tlLWxpbmVjYXA9InNxdWFyZSIvPgo8cGF0aCBkPSJNMTAzLjMwOSAxNTIuMDI1VjE1Mi44OTNDMTAzLjMwOSAxNTMuMzU5IDEwMy4yNjcgMTUzLjc1MiAxMDMuMTg0IDE1NC4wNzJDMTAzLjEwMSAxNTQuMzkzIDEwMi45ODEgMTU0LjY1IDEwMi44MjQgMTU0Ljg0NkMxMDIuNjY4IDE1NS4wNDEgMTAyLjQ3OSAxNTUuMTgzIDEwMi4yNTggMTU1LjI3MUMxMDIuMDM5IDE1NS4zNTcgMTAxLjc5MiAxNTUuNCAxMDEuNTE2IDE1NS40QzEwMS4yOTcgMTU1LjQgMTAxLjA5NSAxNTUuMzczIDEwMC45MSAxNTUuMzE4QzEwMC43MjYgMTU1LjI2NCAxMDAuNTU5IDE1NS4xNzYgMTAwLjQxIDE1NS4wNTdDMTAwLjI2NSAxNTQuOTM0IDEwMC4xNCAxNTQuNzc1IDEwMC4wMzUgMTU0LjU4Qzk5LjkzMTMgMTU0LjM4NSA5OS44NTE4IDE1NC4xNDggOTkuNzk3MSAxNTMuODY5Qzk5Ljc0MjUgMTUzLjU5IDk5LjcxNTEgMTUzLjI2NSA5OS43MTUxIDE1Mi44OTNWMTUyLjAyNUM5OS43MTUxIDE1MS41NTkgOTkuNzU2OCAxNTEuMTY5IDk5Ljg0MDEgMTUwLjg1NEM5OS45MjYxIDE1MC41MzggMTAwLjA0NyAxNTAuMjg2IDEwMC4yMDMgMTUwLjA5NkMxMDAuMzYgMTQ5LjkwMyAxMDAuNTQ3IDE0OS43NjUgMTAwLjc2NiAxNDkuNjgyQzEwMC45ODcgMTQ5LjU5OCAxMDEuMjM1IDE0OS41NTcgMTAxLjUwOCAxNDkuNTU3QzEwMS43MjkgMTQ5LjU1NyAxMDEuOTMzIDE0OS41ODQgMTAyLjExNyAxNDkuNjM5QzEwMi4zMDUgMTQ5LjY5MSAxMDIuNDcyIDE0OS43NzUgMTAyLjYxNyAxNDkuODkzQzEwMi43NjMgMTUwLjAwNyAxMDIuODg3IDE1MC4xNjEgMTAyLjk4OSAxNTAuMzU0QzEwMy4wOTMgMTUwLjU0NCAxMDMuMTcyIDE1MC43NzcgMTAzLjIyNyAxNTEuMDUzQzEwMy4yODIgMTUxLjMyOSAxMDMuMzA5IDE1MS42NTMgMTAzLjMwOSAxNTIuMDI1Wk0xMDIuNTgyIDE1My4wMVYxNTEuOTA0QzEwMi41ODIgMTUxLjY0OSAxMDIuNTY3IDE1MS40MjUgMTAyLjUzNSAxNTEuMjMyQzEwMi41MDcgMTUxLjAzNyAxMDIuNDY0IDE1MC44NyAxMDIuNDA3IDE1MC43MzJDMTAyLjM0OSAxNTAuNTk0IDEwMi4yNzYgMTUwLjQ4MiAxMDIuMTg4IDE1MC4zOTZDMTAyLjEwMiAxNTAuMzExIDEwMi4wMDIgMTUwLjI0OCAxMDEuODg3IDE1MC4yMDlDMTAxLjc3NSAxNTAuMTY3IDEwMS42NDkgMTUwLjE0NiAxMDEuNTA4IDE1MC4xNDZDMTAxLjMzNiAxNTAuMTQ2IDEwMS4xODQgMTUwLjE3OSAxMDEuMDUxIDE1MC4yNDRDMTAwLjkxOCAxNTAuMzA3IDEwMC44MDYgMTUwLjQwNyAxMDAuNzE1IDE1MC41NDVDMTAwLjYyNyAxNTAuNjgzIDEwMC41NTkgMTUwLjg2NCAxMDAuNTEyIDE1MS4wODhDMTAwLjQ2NSAxNTEuMzEyIDEwMC40NDIgMTUxLjU4NCAxMDAuNDQyIDE1MS45MDRWMTUzLjAxQzEwMC40NDIgMTUzLjI2NSAxMDAuNDU2IDE1My40OSAxMDAuNDg1IDE1My42ODZDMTAwLjUxNiAxNTMuODgxIDEwMC41NjEgMTU0LjA1IDEwMC42MjEgMTU0LjE5M0MxMDAuNjgxIDE1NC4zMzQgMTAwLjc1NCAxNTQuNDUgMTAwLjg0IDE1NC41NDFDMTAwLjkyNiAxNTQuNjMyIDEwMS4wMjUgMTU0LjcgMTAxLjEzNyAxNTQuNzQ0QzEwMS4yNTIgMTU0Ljc4NiAxMDEuMzc4IDE1NC44MDcgMTAxLjUxNiAxNTQuODA3QzEwMS42OTMgMTU0LjgwNyAxMDEuODQ4IDE1NC43NzMgMTAxLjk4MSAxNTQuNzA1QzEwMi4xMTQgMTU0LjYzNyAxMDIuMjI0IDE1NC41MzIgMTAyLjMxMyAxNTQuMzg5QzEwMi40MDQgMTU0LjI0MyAxMDIuNDcyIDE1NC4wNTcgMTAyLjUxNiAxNTMuODNDMTAyLjU2IDE1My42MDEgMTAyLjU4MiAxNTMuMzI3IDEwMi41ODIgMTUzLjAxWk0xMDUuMzc2IDE1Mi4xMjNIMTA1Ljg5MUMxMDYuMTQ0IDE1Mi4xMjMgMTA2LjM1MiAxNTIuMDgxIDEwNi41MTYgMTUxLjk5OEMxMDYuNjgzIDE1MS45MTIgMTA2LjgwNyAxNTEuNzk2IDEwNi44ODggMTUxLjY1QzEwNi45NzEgMTUxLjUwMiAxMDcuMDEzIDE1MS4zMzUgMTA3LjAxMyAxNTEuMTVDMTA3LjAxMyAxNTAuOTMyIDEwNi45NzYgMTUwLjc0OCAxMDYuOTAzIDE1MC42QzEwNi44MyAxNTAuNDUxIDEwNi43MjEgMTUwLjMzOSAxMDYuNTc1IDE1MC4yNjRDMTA2LjQyOSAxNTAuMTg4IDEwNi4yNDQgMTUwLjE1IDEwNi4wMiAxNTAuMTVDMTA1LjgxNyAxNTAuMTUgMTA1LjYzOCAxNTAuMTkxIDEwNS40ODEgMTUwLjI3MUMxMDUuMzI4IDE1MC4zNSAxMDUuMjA3IDE1MC40NjIgMTA1LjExOCAxNTAuNjA3QzEwNS4wMzIgMTUwLjc1MyAxMDQuOTg5IDE1MC45MjUgMTA0Ljk4OSAxNTEuMTIzSDEwNC4yNjZDMTA0LjI2NiAxNTAuODM0IDEwNC4zMzkgMTUwLjU3MSAxMDQuNDg1IDE1MC4zMzRDMTA0LjYzMSAxNTAuMDk3IDEwNC44MzYgMTQ5LjkwOCAxMDUuMDk5IDE0OS43NjhDMTA1LjM2NCAxNDkuNjI3IDEwNS42NzEgMTQ5LjU1NyAxMDYuMDIgMTQ5LjU1N0MxMDYuMzY0IDE0OS41NTcgMTA2LjY2NSAxNDkuNjE4IDEwNi45MjMgMTQ5Ljc0QzEwNy4xODEgMTQ5Ljg2IDEwNy4zODEgMTUwLjA0IDEwNy41MjQgMTUwLjI3OUMxMDcuNjY4IDE1MC41MTYgMTA3LjczOSAxNTAuODEyIDEwNy43MzkgMTUxLjE2NkMxMDcuNzM5IDE1MS4zMDkgMTA3LjcwNSAxNTEuNDYzIDEwNy42MzggMTUxLjYyN0MxMDcuNTcyIDE1MS43ODggMTA3LjQ3IDE1MS45MzkgMTA3LjMyOSAxNTIuMDhDMTA3LjE5MSAxNTIuMjIxIDEwNy4wMTEgMTUyLjMzNyAxMDYuNzkgMTUyLjQyOEMxMDYuNTY5IDE1Mi41MTYgMTA2LjMwMyAxNTIuNTYxIDEwNS45OTMgMTUyLjU2MUgxMDUuMzc2VjE1Mi4xMjNaTTEwNS4zNzYgMTUyLjcxN1YxNTIuMjgzSDEwNS45OTNDMTA2LjM1NSAxNTIuMjgzIDEwNi42NTUgMTUyLjMyNiAxMDYuODkxIDE1Mi40MTJDMTA3LjEyOCAxNTIuNDk4IDEwNy4zMTUgMTUyLjYxMyAxMDcuNDUgMTUyLjc1NkMxMDcuNTg4IDE1Mi44OTkgMTA3LjY4NCAxNTMuMDU3IDEwNy43MzkgMTUzLjIyOUMxMDcuNzk2IDE1My4zOTggMTA3LjgyNSAxNTMuNTY3IDEwNy44MjUgMTUzLjczNkMxMDcuODI1IDE1NC4wMDIgMTA3Ljc4IDE1NC4yMzggMTA3LjY4OCAxNTQuNDQzQzEwNy42IDE1NC42NDkgMTA3LjQ3NCAxNTQuODI0IDEwNy4zMDkgMTU0Ljk2N0MxMDcuMTQ4IDE1NS4xMSAxMDYuOTU4IDE1NS4yMTggMTA2LjczOSAxNTUuMjkxQzEwNi41MiAxNTUuMzY0IDEwNi4yODIgMTU1LjQgMTA2LjAyNCAxNTUuNEMxMDUuNzc3IDE1NS40IDEwNS41NDQgMTU1LjM2NSAxMDUuMzI1IDE1NS4yOTVDMTA1LjEwOSAxNTUuMjI1IDEwNC45MTggMTU1LjEyMyAxMDQuNzUxIDE1NC45OUMxMDQuNTg0IDE1NC44NTUgMTA0LjQ1NCAxNTQuNjg5IDEwNC4zNiAxNTQuNDk0QzEwNC4yNjYgMTU0LjI5NiAxMDQuMjIgMTU0LjA3MSAxMDQuMjIgMTUzLjgxOEgxMDQuOTQyQzEwNC45NDIgMTU0LjAxNiAxMDQuOTg1IDE1NC4xODkgMTA1LjA3MSAxNTQuMzM4QzEwNS4xNiAxNTQuNDg2IDEwNS4yODUgMTU0LjYwMiAxMDUuNDQ2IDE1NC42ODZDMTA1LjYxIDE1NC43NjYgMTA1LjgwMyAxNTQuODA3IDEwNi4wMjQgMTU0LjgwN0MxMDYuMjQ2IDE1NC44MDcgMTA2LjQzNiAxNTQuNzY5IDEwNi41OTUgMTU0LjY5M0MxMDYuNzU2IDE1NC42MTUgMTA2Ljg4IDE1NC40OTggMTA2Ljk2NiAxNTQuMzQyQzEwNy4wNTQgMTU0LjE4NiAxMDcuMDk5IDE1My45ODkgMTA3LjA5OSAxNTMuNzUyQzEwNy4wOTkgMTUzLjUxNSAxMDcuMDQ5IDE1My4zMjEgMTA2Ljk1IDE1My4xN0MxMDYuODUxIDE1My4wMTYgMTA2LjcxMSAxNTIuOTAzIDEwNi41MjggMTUyLjgzQzEwNi4zNDkgMTUyLjc1NSAxMDYuMTM2IDE1Mi43MTcgMTA1Ljg5MSAxNTIuNzE3SDEwNS4zNzZaTTExMS45MTIgMTQ5LjYzNVYxNTUuMzIySDExMS4xNThWMTQ5LjYzNUgxMTEuOTEyWk0xMTQuMjk1IDE1Mi4xOTNWMTUyLjgxMUgxMTEuNzQ4VjE1Mi4xOTNIMTE0LjI5NVpNMTE0LjY4MiAxNDkuNjM1VjE1MC4yNTJIMTExLjc0OFYxNDkuNjM1SDExNC42ODJaTTExNy4yMjEgMTU1LjRDMTE2LjkyNyAxNTUuNCAxMTYuNjYgMTU1LjM1MSAxMTYuNDIxIDE1NS4yNTJDMTE2LjE4NCAxNTUuMTUgMTE1Ljk3OSAxNTUuMDA4IDExNS44MDcgMTU0LjgyNkMxMTUuNjM4IDE1NC42NDQgMTE1LjUwOCAxNTQuNDI4IDExNS40MTcgMTU0LjE3OEMxMTUuMzI2IDE1My45MjggMTE1LjI4IDE1My42NTQgMTE1LjI4IDE1My4zNTdWMTUzLjE5M0MxMTUuMjggMTUyLjg1IDExNS4zMzEgMTUyLjU0NCAxMTUuNDMyIDE1Mi4yNzVDMTE1LjUzNCAxNTIuMDA1IDExNS42NzIgMTUxLjc3NSAxMTUuODQ2IDE1MS41ODhDMTE2LjAyMSAxNTEuNCAxMTYuMjE5IDE1MS4yNTggMTE2LjQ0IDE1MS4xNjJDMTE2LjY2MiAxNTEuMDY2IDExNi44OTEgMTUxLjAxOCAxMTcuMTI4IDE1MS4wMThDMTE3LjQzIDE1MS4wMTggMTE3LjY5IDE1MS4wNyAxMTcuOTA5IDE1MS4xNzRDMTE4LjEzIDE1MS4yNzggMTE4LjMxMSAxNTEuNDI0IDExOC40NTIgMTUxLjYxMUMxMTguNTkyIDE1MS43OTYgMTE4LjY5NyAxNTIuMDE1IDExOC43NjQgMTUyLjI2OEMxMTguODMyIDE1Mi41MTggMTE4Ljg2NiAxNTIuNzkxIDExOC44NjYgMTUzLjA4OFYxNTMuNDEySDExNS43MVYxNTIuODIySDExOC4xNDNWMTUyLjc2OEMxMTguMTMzIDE1Mi41OCAxMTguMDk0IDE1Mi4zOTggMTE4LjAyNiAxNTIuMjIxQzExNy45NjEgMTUyLjA0NCAxMTcuODU3IDE1MS44OTggMTE3LjcxNCAxNTEuNzgzQzExNy41NyAxNTEuNjY5IDExNy4zNzUgMTUxLjYxMSAxMTcuMTI4IDE1MS42MTFDMTE2Ljk2NCAxNTEuNjExIDExNi44MTMgMTUxLjY0NiAxMTYuNjc1IDE1MS43MTdDMTE2LjUzNyAxNTEuNzg1IDExNi40MTggMTUxLjg4NiAxMTYuMzE5IDE1Mi4wMjFDMTE2LjIyIDE1Mi4xNTcgMTE2LjE0MyAxNTIuMzIyIDExNi4wODkgMTUyLjUxOEMxMTYuMDM0IDE1Mi43MTMgMTE2LjAwNyAxNTIuOTM4IDExNi4wMDcgMTUzLjE5M1YxNTMuMzU3QzExNi4wMDcgMTUzLjU1OCAxMTYuMDM0IDE1My43NDcgMTE2LjA4OSAxNTMuOTI0QzExNi4xNDYgMTU0LjA5OCAxMTYuMjI4IDE1NC4yNTIgMTE2LjMzNSAxNTQuMzg1QzExNi40NDQgMTU0LjUxOCAxMTYuNTc2IDE1NC42MjIgMTE2LjcyOSAxNTQuNjk3QzExNi44ODUgMTU0Ljc3MyAxMTcuMDYzIDE1NC44MTEgMTE3LjI2IDE1NC44MTFDMTE3LjUxNiAxNTQuODExIDExNy43MzIgMTU0Ljc1OCAxMTcuOTA5IDE1NC42NTRDMTE4LjA4NiAxNTQuNTUgMTE4LjI0MSAxNTQuNDExIDExOC4zNzQgMTU0LjIzNkwxMTguODExIDE1NC41ODRDMTE4LjcyIDE1NC43MjIgMTE4LjYwNCAxNTQuODU0IDExOC40NjQgMTU0Ljk3OUMxMTguMzIzIDE1NS4xMDQgMTE4LjE1IDE1NS4yMDUgMTE3Ljk0NCAxNTUuMjgzQzExNy43NDEgMTU1LjM2MSAxMTcuNSAxNTUuNCAxMTcuMjIxIDE1NS40Wk0xMTkuNzg4IDE0OS4zMjJIMTIwLjUxNVYxNTQuNTAyTDEyMC40NTIgMTU1LjMyMkgxMTkuNzg4VjE0OS4zMjJaTTEyMy4zNyAxNTMuMTc0VjE1My4yNTZDMTIzLjM3IDE1My41NjMgMTIzLjMzNCAxNTMuODQ4IDEyMy4yNjEgMTU0LjExMUMxMjMuMTg4IDE1NC4zNzIgMTIzLjA4MSAxNTQuNTk4IDEyMi45NDEgMTU0Ljc5MUMxMjIuOCAxNTQuOTg0IDEyMi42MjggMTU1LjEzMyAxMjIuNDI1IDE1NS4yNEMxMjIuMjIyIDE1NS4zNDcgMTIxLjk4OSAxNTUuNCAxMjEuNzI2IDE1NS40QzEyMS40NTggMTU1LjQgMTIxLjIyMiAxNTUuMzU1IDEyMS4wMTkgMTU1LjI2NEMxMjAuODE4IDE1NS4xNyAxMjAuNjQ5IDE1NS4wMzYgMTIwLjUxMSAxNTQuODYxQzEyMC4zNzMgMTU0LjY4NyAxMjAuMjYyIDE1NC40NzYgMTIwLjE3OSAxNTQuMjI5QzEyMC4wOTggMTUzLjk4MSAxMjAuMDQyIDE1My43MDIgMTIwLjAxMSAxNTMuMzkzVjE1My4wMzNDMTIwLjA0MiAxNTIuNzIxIDEyMC4wOTggMTUyLjQ0MSAxMjAuMTc5IDE1Mi4xOTNDMTIwLjI2MiAxNTEuOTQ2IDEyMC4zNzMgMTUxLjczNSAxMjAuNTExIDE1MS41NjFDMTIwLjY0OSAxNTEuMzgzIDEyMC44MTggMTUxLjI0OSAxMjEuMDE5IDE1MS4xNThDMTIxLjIxOSAxNTEuMDY0IDEyMS40NTIgMTUxLjAxOCAxMjEuNzE4IDE1MS4wMThDMTIxLjk4NCAxNTEuMDE4IDEyMi4yMTkgMTUxLjA3IDEyMi40MjUgMTUxLjE3NEMxMjIuNjMxIDE1MS4yNzUgMTIyLjgwMyAxNTEuNDIxIDEyMi45NDEgMTUxLjYxMUMxMjMuMDgxIDE1MS44MDEgMTIzLjE4OCAxNTIuMDI5IDEyMy4yNjEgMTUyLjI5NUMxMjMuMzM0IDE1Mi41NTggMTIzLjM3IDE1Mi44NTEgMTIzLjM3IDE1My4xNzRaTTEyMi42NDQgMTUzLjI1NlYxNTMuMTc0QzEyMi42NDQgMTUyLjk2MyAxMjIuNjI0IDE1Mi43NjUgMTIyLjU4NSAxNTIuNThDMTIyLjU0NiAxNTIuMzkzIDEyMi40ODQgMTUyLjIyOSAxMjIuMzk4IDE1Mi4wODhDMTIyLjMxMiAxNTEuOTQ1IDEyMi4xOTkgMTUxLjgzMyAxMjIuMDU4IDE1MS43NTJDMTIxLjkxNyAxNTEuNjY5IDEyMS43NDQgMTUxLjYyNyAxMjEuNTM4IDE1MS42MjdDMTIxLjM1NiAxNTEuNjI3IDEyMS4xOTcgMTUxLjY1OCAxMjEuMDYyIDE1MS43MjFDMTIwLjkyOSAxNTEuNzgzIDEyMC44MTYgMTUxLjg2OCAxMjAuNzIyIDE1MS45NzVDMTIwLjYyOCAxNTIuMDc5IDEyMC41NTEgMTUyLjE5OSAxMjAuNDkyIDE1Mi4zMzRDMTIwLjQzNCAxNTIuNDY3IDEyMC4zOTEgMTUyLjYwNSAxMjAuMzYzIDE1Mi43NDhWMTUzLjY4OUMxMjAuNDA0IDE1My44NzIgMTIwLjQ3MiAxNTQuMDQ4IDEyMC41NjYgMTU0LjIxN0MxMjAuNjYyIDE1NC4zODMgMTIwLjc5IDE1NC41MiAxMjAuOTQ5IDE1NC42MjdDMTIxLjExIDE1NC43MzQgMTIxLjMwOSAxNTQuNzg3IDEyMS41NDYgMTU0Ljc4N0MxMjEuNzQyIDE1NC43ODcgMTIxLjkwOCAxNTQuNzQ4IDEyMi4wNDYgMTU0LjY3QzEyMi4xODcgMTU0LjU4OSAxMjIuMyAxNTQuNDc5IDEyMi4zODYgMTU0LjMzOEMxMjIuNDc1IDE1NC4xOTcgMTIyLjU0IDE1NC4wMzUgMTIyLjU4MSAxNTMuODVDMTIyLjYyMyAxNTMuNjY1IDEyMi42NDQgMTUzLjQ2NyAxMjIuNjQ0IDE1My4yNTZaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjU0Ii8+CjwvZz4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAzXzQwNTNfMTg1MTEzKSI+CjxsaW5lIHgxPSIxNDYuNjUiIHkxPSIxNDcuMDcyIiB4Mj0iMTQ2LjY1IiB5Mj0iMTQ2LjI1IiBzdHJva2U9ImJsYWNrIiBzdHJva2Utb3BhY2l0eT0iMC41IiBzdHJva2Utd2lkdGg9IjAuNSIgc3Ryb2tlLWxpbmVjYXA9InNxdWFyZSIvPgo8cGF0aCBkPSJNMTM4LjcwOSAxNTIuMDI1VjE1Mi44OTNDMTM4LjcwOSAxNTMuMzU5IDEzOC42NjggMTUzLjc1MiAxMzguNTg0IDE1NC4wNzJDMTM4LjUwMSAxNTQuMzkzIDEzOC4zODEgMTU0LjY1IDEzOC4yMjUgMTU0Ljg0NkMxMzguMDY5IDE1NS4wNDEgMTM3Ljg4IDE1NS4xODMgMTM3LjY1OCAxNTUuMjcxQzEzNy40NCAxNTUuMzU3IDEzNy4xOTIgMTU1LjQgMTM2LjkxNiAxNTUuNEMxMzYuNjk4IDE1NS40IDEzNi40OTYgMTU1LjM3MyAxMzYuMzExIDE1NS4zMThDMTM2LjEyNiAxNTUuMjY0IDEzNS45NTkgMTU1LjE3NiAxMzUuODExIDE1NS4wNTdDMTM1LjY2NSAxNTQuOTM0IDEzNS41NCAxNTQuNzc1IDEzNS40MzYgMTU0LjU4QzEzNS4zMzIgMTU0LjM4NSAxMzUuMjUyIDE1NC4xNDggMTM1LjE5OCAxNTMuODY5QzEzNS4xNDMgMTUzLjU5IDEzNS4xMTYgMTUzLjI2NSAxMzUuMTE2IDE1Mi44OTNWMTUyLjAyNUMxMzUuMTE2IDE1MS41NTkgMTM1LjE1NyAxNTEuMTY5IDEzNS4yNDEgMTUwLjg1NEMxMzUuMzI2IDE1MC41MzggMTM1LjQ0OCAxNTAuMjg2IDEzNS42MDQgMTUwLjA5NkMxMzUuNzYgMTQ5LjkwMyAxMzUuOTQ4IDE0OS43NjUgMTM2LjE2NiAxNDkuNjgyQzEzNi4zODggMTQ5LjU5OCAxMzYuNjM1IDE0OS41NTcgMTM2LjkwOCAxNDkuNTU3QzEzNy4xMyAxNDkuNTU3IDEzNy4zMzMgMTQ5LjU4NCAxMzcuNTE4IDE0OS42MzlDMTM3LjcwNSAxNDkuNjkxIDEzNy44NzIgMTQ5Ljc3NSAxMzguMDE4IDE0OS44OTNDMTM4LjE2NCAxNTAuMDA3IDEzOC4yODcgMTUwLjE2MSAxMzguMzg5IDE1MC4zNTRDMTM4LjQ5MyAxNTAuNTQ0IDEzOC41NzMgMTUwLjc3NyAxMzguNjI3IDE1MS4wNTNDMTM4LjY4MiAxNTEuMzI5IDEzOC43MDkgMTUxLjY1MyAxMzguNzA5IDE1Mi4wMjVaTTEzNy45ODMgMTUzLjAxVjE1MS45MDRDMTM3Ljk4MyAxNTEuNjQ5IDEzNy45NjcgMTUxLjQyNSAxMzcuOTM2IDE1MS4yMzJDMTM3LjkwNyAxNTEuMDM3IDEzNy44NjQgMTUwLjg3IDEzNy44MDcgMTUwLjczMkMxMzcuNzUgMTUwLjU5NCAxMzcuNjc3IDE1MC40ODIgMTM3LjU4OCAxNTAuMzk2QzEzNy41MDIgMTUwLjMxMSAxMzcuNDAyIDE1MC4yNDggMTM3LjI4NyAxNTAuMjA5QzEzNy4xNzUgMTUwLjE2NyAxMzcuMDQ5IDE1MC4xNDYgMTM2LjkwOCAxNTAuMTQ2QzEzNi43MzcgMTUwLjE0NiAxMzYuNTg0IDE1MC4xNzkgMTM2LjQ1MSAxNTAuMjQ0QzEzNi4zMTkgMTUwLjMwNyAxMzYuMjA3IDE1MC40MDcgMTM2LjExNiAxNTAuNTQ1QzEzNi4wMjcgMTUwLjY4MyAxMzUuOTU5IDE1MC44NjQgMTM1LjkxMiAxNTEuMDg4QzEzNS44NjYgMTUxLjMxMiAxMzUuODQyIDE1MS41ODQgMTM1Ljg0MiAxNTEuOTA0VjE1My4wMUMxMzUuODQyIDE1My4yNjUgMTM1Ljg1NiAxNTMuNDkgMTM1Ljg4NSAxNTMuNjg2QzEzNS45MTYgMTUzLjg4MSAxMzUuOTYyIDE1NC4wNSAxMzYuMDIyIDE1NC4xOTNDMTM2LjA4MiAxNTQuMzM0IDEzNi4xNTUgMTU0LjQ1IDEzNi4yNDEgMTU0LjU0MUMxMzYuMzI2IDE1NC42MzIgMTM2LjQyNSAxNTQuNyAxMzYuNTM3IDE1NC43NDRDMTM2LjY1MiAxNTQuNzg2IDEzNi43NzggMTU0LjgwNyAxMzYuOTE2IDE1NC44MDdDMTM3LjA5MyAxNTQuODA3IDEzNy4yNDggMTU0Ljc3MyAxMzcuMzgxIDE1NC43MDVDMTM3LjUxNCAxNTQuNjM3IDEzNy42MjUgMTU0LjUzMiAxMzcuNzEzIDE1NC4zODlDMTM3LjgwNCAxNTQuMjQzIDEzNy44NzIgMTU0LjA1NyAxMzcuOTE2IDE1My44M0MxMzcuOTYxIDE1My42MDEgMTM3Ljk4MyAxNTMuMzI3IDEzNy45ODMgMTUzLjAxWk0xNDMuNTY1IDE1My40MDhWMTU0LjAwMkgxMzkuNDU2VjE1My41NzZMMTQyLjAwMyAxNDkuNjM1SDE0Mi41OTNMMTQxLjk2IDE1MC43NzVMMTQwLjI3NiAxNTMuNDA4SDE0My41NjVaTTE0Mi43NzIgMTQ5LjYzNVYxNTUuMzIySDE0Mi4wNVYxNDkuNjM1SDE0Mi43NzJaTTE0Ny4zMTMgMTQ5LjYzNVYxNTUuMzIySDE0Ni41NTlWMTQ5LjYzNUgxNDcuMzEzWk0xNDkuNjk1IDE1Mi4xOTNWMTUyLjgxMUgxNDcuMTQ5VjE1Mi4xOTNIMTQ5LjY5NVpNMTUwLjA4MiAxNDkuNjM1VjE1MC4yNTJIMTQ3LjE0OVYxNDkuNjM1SDE1MC4wODJaTTE1Mi42MjIgMTU1LjRDMTUyLjMyOCAxNTUuNCAxNTIuMDYxIDE1NS4zNTEgMTUxLjgyMSAxNTUuMjUyQzE1MS41ODQgMTU1LjE1IDE1MS4zOCAxNTUuMDA4IDE1MS4yMDggMTU0LjgyNkMxNTEuMDM4IDE1NC42NDQgMTUwLjkwOCAxNTQuNDI4IDE1MC44MTcgMTU0LjE3OEMxNTAuNzI2IDE1My45MjggMTUwLjY4IDE1My42NTQgMTUwLjY4IDE1My4zNTdWMTUzLjE5M0MxNTAuNjggMTUyLjg1IDE1MC43MzEgMTUyLjU0NCAxNTAuODMzIDE1Mi4yNzVDMTUwLjkzNCAxNTIuMDA1IDE1MS4wNzIgMTUxLjc3NSAxNTEuMjQ3IDE1MS41ODhDMTUxLjQyMSAxNTEuNCAxNTEuNjE5IDE1MS4yNTggMTUxLjg0MSAxNTEuMTYyQzE1Mi4wNjIgMTUxLjA2NiAxNTIuMjkxIDE1MS4wMTggMTUyLjUyOCAxNTEuMDE4QzE1Mi44MyAxNTEuMDE4IDE1My4wOTEgMTUxLjA3IDE1My4zMDkgMTUxLjE3NEMxNTMuNTMxIDE1MS4yNzggMTUzLjcxMiAxNTEuNDI0IDE1My44NTIgMTUxLjYxMUMxNTMuOTkzIDE1MS43OTYgMTU0LjA5NyAxNTIuMDE1IDE1NC4xNjUgMTUyLjI2OEMxNTQuMjMyIDE1Mi41MTggMTU0LjI2NiAxNTIuNzkxIDE1NC4yNjYgMTUzLjA4OFYxNTMuNDEySDE1MS4xMVYxNTIuODIySDE1My41NDRWMTUyLjc2OEMxNTMuNTMzIDE1Mi41OCAxNTMuNDk0IDE1Mi4zOTggMTUzLjQyNiAxNTIuMjIxQzE1My4zNjEgMTUyLjA0NCAxNTMuMjU3IDE1MS44OTggMTUzLjExNCAxNTEuNzgzQzE1Mi45NzEgMTUxLjY2OSAxNTIuNzc1IDE1MS42MTEgMTUyLjUyOCAxNTEuNjExQzE1Mi4zNjQgMTUxLjYxMSAxNTIuMjEzIDE1MS42NDYgMTUyLjA3NSAxNTEuNzE3QzE1MS45MzcgMTUxLjc4NSAxNTEuODE4IDE1MS44ODYgMTUxLjcxOSAxNTIuMDIxQzE1MS42MiAxNTIuMTU3IDE1MS41NDQgMTUyLjMyMiAxNTEuNDg5IDE1Mi41MThDMTUxLjQzNCAxNTIuNzEzIDE1MS40MDcgMTUyLjkzOCAxNTEuNDA3IDE1My4xOTNWMTUzLjM1N0MxNTEuNDA3IDE1My41NTggMTUxLjQzNCAxNTMuNzQ3IDE1MS40ODkgMTUzLjkyNEMxNTEuNTQ2IDE1NC4wOTggMTUxLjYyOCAxNTQuMjUyIDE1MS43MzUgMTU0LjM4NUMxNTEuODQ0IDE1NC41MTggMTUxLjk3NiAxNTQuNjIyIDE1Mi4xMyAxNTQuNjk3QzE1Mi4yODYgMTU0Ljc3MyAxNTIuNDYzIDE1NC44MTEgMTUyLjY2MSAxNTQuODExQzE1Mi45MTYgMTU0LjgxMSAxNTMuMTMyIDE1NC43NTggMTUzLjMwOSAxNTQuNjU0QzE1My40ODYgMTU0LjU1IDE1My42NDEgMTU0LjQxMSAxNTMuNzc0IDE1NC4yMzZMMTU0LjIxMiAxNTQuNTg0QzE1NC4xMiAxNTQuNzIyIDE1NC4wMDUgMTU0Ljg1NCAxNTMuODY0IDE1NC45NzlDMTUzLjcyMyAxNTUuMTA0IDE1My41NSAxNTUuMjA1IDE1My4zNDQgMTU1LjI4M0MxNTMuMTQxIDE1NS4zNjEgMTUyLjkgMTU1LjQgMTUyLjYyMiAxNTUuNFpNMTU1LjE4OSAxNDkuMzIySDE1NS45MTVWMTU0LjUwMkwxNTUuODUzIDE1NS4zMjJIMTU1LjE4OVYxNDkuMzIyWk0xNTguNzcxIDE1My4xNzRWMTUzLjI1NkMxNTguNzcxIDE1My41NjMgMTU4LjczNCAxNTMuODQ4IDE1OC42NjEgMTU0LjExMUMxNTguNTg5IDE1NC4zNzIgMTU4LjQ4MiAxNTQuNTk4IDE1OC4zNDEgMTU0Ljc5MUMxNTguMjAxIDE1NC45ODQgMTU4LjAyOSAxNTUuMTMzIDE1Ny44MjYgMTU1LjI0QzE1Ny42MjIgMTU1LjM0NyAxNTcuMzg5IDE1NS40IDE1Ny4xMjYgMTU1LjRDMTU2Ljg1OCAxNTUuNCAxNTYuNjIyIDE1NS4zNTUgMTU2LjQxOSAxNTUuMjY0QzE1Ni4yMTkgMTU1LjE3IDE1Ni4wNDkgMTU1LjAzNiAxNTUuOTExIDE1NC44NjFDMTU1Ljc3MyAxNTQuNjg3IDE1NS42NjMgMTU0LjQ3NiAxNTUuNTc5IDE1NC4yMjlDMTU1LjQ5OSAxNTMuOTgxIDE1NS40NDMgMTUzLjcwMiAxNTUuNDExIDE1My4zOTNWMTUzLjAzM0MxNTUuNDQzIDE1Mi43MjEgMTU1LjQ5OSAxNTIuNDQxIDE1NS41NzkgMTUyLjE5M0MxNTUuNjYzIDE1MS45NDYgMTU1Ljc3MyAxNTEuNzM1IDE1NS45MTEgMTUxLjU2MUMxNTYuMDQ5IDE1MS4zODMgMTU2LjIxOSAxNTEuMjQ5IDE1Ni40MTkgMTUxLjE1OEMxNTYuNjIgMTUxLjA2NCAxNTYuODUzIDE1MS4wMTggMTU3LjExOCAxNTEuMDE4QzE1Ny4zODQgMTUxLjAxOCAxNTcuNjIgMTUxLjA3IDE1Ny44MjYgMTUxLjE3NEMxNTguMDMxIDE1MS4yNzUgMTU4LjIwMyAxNTEuNDIxIDE1OC4zNDEgMTUxLjYxMUMxNTguNDgyIDE1MS44MDEgMTU4LjU4OSAxNTIuMDI5IDE1OC42NjEgMTUyLjI5NUMxNTguNzM0IDE1Mi41NTggMTU4Ljc3MSAxNTIuODUxIDE1OC43NzEgMTUzLjE3NFpNMTU4LjA0NCAxNTMuMjU2VjE1My4xNzRDMTU4LjA0NCAxNTIuOTYzIDE1OC4wMjUgMTUyLjc2NSAxNTcuOTg2IDE1Mi41OEMxNTcuOTQ3IDE1Mi4zOTMgMTU3Ljg4NCAxNTIuMjI5IDE1Ny43OTggMTUyLjA4OEMxNTcuNzEyIDE1MS45NDUgMTU3LjU5OSAxNTEuODMzIDE1Ny40NTggMTUxLjc1MkMxNTcuMzE4IDE1MS42NjkgMTU3LjE0NSAxNTEuNjI3IDE1Ni45MzkgMTUxLjYyN0MxNTYuNzU3IDE1MS42MjcgMTU2LjU5OCAxNTEuNjU4IDE1Ni40NjIgMTUxLjcyMUMxNTYuMzI5IDE1MS43ODMgMTU2LjIxNiAxNTEuODY4IDE1Ni4xMjIgMTUxLjk3NUMxNTYuMDI5IDE1Mi4wNzkgMTU1Ljk1MiAxNTIuMTk5IDE1NS44OTIgMTUyLjMzNEMxNTUuODM1IDE1Mi40NjcgMTU1Ljc5MiAxNTIuNjA1IDE1NS43NjMgMTUyLjc0OFYxNTMuNjg5QzE1NS44MDUgMTUzLjg3MiAxNTUuODcyIDE1NC4wNDggMTU1Ljk2NiAxNTQuMjE3QzE1Ni4wNjIgMTU0LjM4MyAxNTYuMTkgMTU0LjUyIDE1Ni4zNDkgMTU0LjYyN0MxNTYuNTEgMTU0LjczNCAxNTYuNzEgMTU0Ljc4NyAxNTYuOTQ3IDE1NC43ODdDMTU3LjE0MiAxNTQuNzg3IDE1Ny4zMDkgMTU0Ljc0OCAxNTcuNDQ3IDE1NC42N0MxNTcuNTg3IDE1NC41ODkgMTU3LjcwMSAxNTQuNDc5IDE1Ny43ODYgMTU0LjMzOEMxNTcuODc1IDE1NC4xOTcgMTU3Ljk0IDE1NC4wMzUgMTU3Ljk4MiAxNTMuODVDMTU4LjAyMyAxNTMuNjY1IDE1OC4wNDQgMTUzLjQ2NyAxNTguMDQ0IDE1My4yNTZaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjU0Ii8+CjwvZz4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXA0XzQwNTNfMTg1MTEzKSI+CjxsaW5lIHgxPSIxODIuMDUiIHkxPSIxNDcuMDcyIiB4Mj0iMTgyLjA1IiB5Mj0iMTQ2LjI1IiBzdHJva2U9ImJsYWNrIiBzdHJva2Utb3BhY2l0eT0iMC41IiBzdHJva2Utd2lkdGg9IjAuNSIgc3Ryb2tlLWxpbmVjYXA9InNxdWFyZSIvPgo8cGF0aCBkPSJNMTc0LjEwOSAxNTIuMDI1VjE1Mi44OTNDMTc0LjEwOSAxNTMuMzU5IDE3NC4wNjcgMTUzLjc1MiAxNzMuOTg0IDE1NC4wNzJDMTczLjkwMSAxNTQuMzkzIDE3My43ODEgMTU0LjY1IDE3My42MjUgMTU0Ljg0NkMxNzMuNDY5IDE1NS4wNDEgMTczLjI4IDE1NS4xODMgMTczLjA1OCAxNTUuMjcxQzE3Mi44NCAxNTUuMzU3IDE3Mi41OTIgMTU1LjQgMTcyLjMxNiAxNTUuNEMxNzIuMDk3IDE1NS40IDE3MS44OTYgMTU1LjM3MyAxNzEuNzExIDE1NS4zMThDMTcxLjUyNiAxNTUuMjY0IDE3MS4zNTkgMTU1LjE3NiAxNzEuMjExIDE1NS4wNTdDMTcxLjA2NSAxNTQuOTM0IDE3MC45NCAxNTQuNzc1IDE3MC44MzYgMTU0LjU4QzE3MC43MzIgMTU0LjM4NSAxNzAuNjUyIDE1NC4xNDggMTcwLjU5NyAxNTMuODY5QzE3MC41NDMgMTUzLjU5IDE3MC41MTUgMTUzLjI2NSAxNzAuNTE1IDE1Mi44OTNWMTUyLjAyNUMxNzAuNTE1IDE1MS41NTkgMTcwLjU1NyAxNTEuMTY5IDE3MC42NCAxNTAuODU0QzE3MC43MjYgMTUwLjUzOCAxNzAuODQ3IDE1MC4yODYgMTcxLjAwNCAxNTAuMDk2QzE3MS4xNiAxNDkuOTAzIDE3MS4zNDcgMTQ5Ljc2NSAxNzEuNTY2IDE0OS42ODJDMTcxLjc4OCAxNDkuNTk4IDE3Mi4wMzUgMTQ5LjU1NyAxNzIuMzA4IDE0OS41NTdDMTcyLjUzIDE0OS41NTcgMTcyLjczMyAxNDkuNTg0IDE3Mi45MTggMTQ5LjYzOUMxNzMuMTA1IDE0OS42OTEgMTczLjI3MiAxNDkuNzc1IDE3My40MTggMTQ5Ljg5M0MxNzMuNTY0IDE1MC4wMDcgMTczLjY4NyAxNTAuMTYxIDE3My43ODkgMTUwLjM1NEMxNzMuODkzIDE1MC41NDQgMTczLjk3MiAxNTAuNzc3IDE3NC4wMjcgMTUxLjA1M0MxNzQuMDgyIDE1MS4zMjkgMTc0LjEwOSAxNTEuNjUzIDE3NC4xMDkgMTUyLjAyNVpNMTczLjM4MyAxNTMuMDFWMTUxLjkwNEMxNzMuMzgzIDE1MS42NDkgMTczLjM2NyAxNTEuNDI1IDE3My4zMzYgMTUxLjIzMkMxNzMuMzA3IDE1MS4wMzcgMTczLjI2NCAxNTAuODcgMTczLjIwNyAxNTAuNzMyQzE3My4xNSAxNTAuNTk0IDE3My4wNzcgMTUwLjQ4MiAxNzIuOTg4IDE1MC4zOTZDMTcyLjkwMiAxNTAuMzExIDE3Mi44MDIgMTUwLjI0OCAxNzIuNjg3IDE1MC4yMDlDMTcyLjU3NSAxNTAuMTY3IDE3Mi40NDkgMTUwLjE0NiAxNzIuMzA4IDE1MC4xNDZDMTcyLjEzNiAxNTAuMTQ2IDE3MS45ODQgMTUwLjE3OSAxNzEuODUxIDE1MC4yNDRDMTcxLjcxOSAxNTAuMzA3IDE3MS42MDcgMTUwLjQwNyAxNzEuNTE1IDE1MC41NDVDMTcxLjQyNyAxNTAuNjgzIDE3MS4zNTkgMTUwLjg2NCAxNzEuMzEyIDE1MS4wODhDMTcxLjI2NSAxNTEuMzEyIDE3MS4yNDIgMTUxLjU4NCAxNzEuMjQyIDE1MS45MDRWMTUzLjAxQzE3MS4yNDIgMTUzLjI2NSAxNzEuMjU2IDE1My40OSAxNzEuMjg1IDE1My42ODZDMTcxLjMxNiAxNTMuODgxIDE3MS4zNjIgMTU0LjA1IDE3MS40MjIgMTU0LjE5M0MxNzEuNDgyIDE1NC4zMzQgMTcxLjU1NCAxNTQuNDUgMTcxLjY0IDE1NC41NDFDMTcxLjcyNiAxNTQuNjMyIDE3MS44MjUgMTU0LjcgMTcxLjkzNyAxNTQuNzQ0QzE3Mi4wNTIgMTU0Ljc4NiAxNzIuMTc4IDE1NC44MDcgMTcyLjMxNiAxNTQuODA3QzE3Mi40OTMgMTU0LjgwNyAxNzIuNjQ4IDE1NC43NzMgMTcyLjc4MSAxNTQuNzA1QzE3Mi45MTQgMTU0LjYzNyAxNzMuMDI1IDE1NC41MzIgMTczLjExMyAxNTQuMzg5QzE3My4yMDQgMTU0LjI0MyAxNzMuMjcyIDE1NC4wNTcgMTczLjMxNiAxNTMuODNDMTczLjM2IDE1My42MDEgMTczLjM4MyAxNTMuMzI3IDE3My4zODMgMTUzLjAxWk0xNzYuMDM2IDE1Mi42MTVMMTc1LjQ1NyAxNTIuNDY3TDE3NS43NDMgMTQ5LjYzNUgxNzguNjYxVjE1MC4zMDNIMTc2LjM1NkwxNzYuMTg0IDE1MS44NUMxNzYuMjg4IDE1MS43OSAxNzYuNDIgMTUxLjczNCAxNzYuNTc5IDE1MS42ODJDMTc2Ljc0IDE1MS42MyAxNzYuOTI1IDE1MS42MDQgMTc3LjEzMyAxNTEuNjA0QzE3Ny4zOTYgMTUxLjYwNCAxNzcuNjMyIDE1MS42NDkgMTc3Ljg0IDE1MS43NEMxNzguMDQ5IDE1MS44MjkgMTc4LjIyNiAxNTEuOTU2IDE3OC4zNzEgMTUyLjEyM0MxNzguNTIgMTUyLjI5IDE3OC42MzMgMTUyLjQ5IDE3OC43MTEgMTUyLjcyNUMxNzguNzg5IDE1Mi45NTkgMTc4LjgyOSAxNTMuMjIxIDE3OC44MjkgMTUzLjUxQzE3OC44MjkgMTUzLjc4MyAxNzguNzkxIDE1NC4wMzUgMTc4LjcxNSAxNTQuMjY0QzE3OC42NDIgMTU0LjQ5MyAxNzguNTMyIDE1NC42OTMgMTc4LjM4MyAxNTQuODY1QzE3OC4yMzUgMTU1LjAzNSAxNzguMDQ3IDE1NS4xNjYgMTc3LjgyMSAxNTUuMjZDMTc3LjU5NyAxNTUuMzU0IDE3Ny4zMzIgMTU1LjQgMTc3LjAyOCAxNTUuNEMxNzYuNzk5IDE1NS40IDE3Ni41ODEgMTU1LjM2OSAxNzYuMzc1IDE1NS4zMDdDMTc2LjE3MiAxNTUuMjQyIDE3NS45OSAxNTUuMTQ0IDE3NS44MjkgMTU1LjAxNEMxNzUuNjcgMTU0Ljg4MSAxNzUuNTM5IDE1NC43MTcgMTc1LjQzOCAxNTQuNTIxQzE3NS4zMzkgMTU0LjMyNCAxNzUuMjc2IDE1NC4wOTIgMTc1LjI1IDE1My44MjZIMTc1LjkzOEMxNzUuOTY5IDE1NC4wNCAxNzYuMDMyIDE1NC4yMTkgMTc2LjEyNSAxNTQuMzY1QzE3Ni4yMTkgMTU0LjUxMSAxNzYuMzQyIDE1NC42MjIgMTc2LjQ5MyAxNTQuNjk3QzE3Ni42NDYgMTU0Ljc3IDE3Ni44MjUgMTU0LjgwNyAxNzcuMDI4IDE1NC44MDdDMTc3LjIgMTU0LjgwNyAxNzcuMzUyIDE1NC43NzcgMTc3LjQ4NSAxNTQuNzE3QzE3Ny42MTggMTU0LjY1NyAxNzcuNzMgMTU0LjU3MSAxNzcuODIxIDE1NC40NTlDMTc3LjkxMiAxNTQuMzQ3IDE3Ny45ODEgMTU0LjIxMiAxNzguMDI4IDE1NC4wNTNDMTc4LjA3NyAxNTMuODk0IDE3OC4xMDIgMTUzLjcxNSAxNzguMTAyIDE1My41MThDMTc4LjEwMiAxNTMuMzM4IDE3OC4wNzcgMTUzLjE3MSAxNzguMDI4IDE1My4wMThDMTc3Ljk3OCAxNTIuODY0IDE3Ny45MDQgMTUyLjczIDE3Ny44MDUgMTUyLjYxNUMxNzcuNzA5IDE1Mi41MDEgMTc3LjU5IDE1Mi40MTIgMTc3LjQ1IDE1Mi4zNUMxNzcuMzA5IDE1Mi4yODUgMTc3LjE0OCAxNTIuMjUyIDE3Ni45NjUgMTUyLjI1MkMxNzYuNzIzIDE1Mi4yNTIgMTc2LjUzOSAxNTIuMjg1IDE3Ni40MTQgMTUyLjM1QzE3Ni4yOTIgMTUyLjQxNSAxNzYuMTY2IDE1Mi41MDMgMTc2LjAzNiAxNTIuNjE1Wk0xODIuNzEzIDE0OS42MzVWMTU1LjMyMkgxODEuOTU5VjE0OS42MzVIMTgyLjcxM1pNMTg1LjA5NSAxNTIuMTkzVjE1Mi44MTFIMTgyLjU0OFYxNTIuMTkzSDE4NS4wOTVaTTE4NS40ODIgMTQ5LjYzNVYxNTAuMjUySDE4Mi41NDhWMTQ5LjYzNUgxODUuNDgyWk0xODguMDIyIDE1NS40QzE4Ny43MjcgMTU1LjQgMTg3LjQ2IDE1NS4zNTEgMTg3LjIyMSAxNTUuMjUyQzE4Ni45ODQgMTU1LjE1IDE4Ni43OCAxNTUuMDA4IDE4Ni42MDggMTU0LjgyNkMxODYuNDM4IDE1NC42NDQgMTg2LjMwOCAxNTQuNDI4IDE4Ni4yMTcgMTU0LjE3OEMxODYuMTI2IDE1My45MjggMTg2LjA4IDE1My42NTQgMTg2LjA4IDE1My4zNTdWMTUzLjE5M0MxODYuMDggMTUyLjg1IDE4Ni4xMzEgMTUyLjU0NCAxODYuMjMzIDE1Mi4yNzVDMTg2LjMzNCAxNTIuMDA1IDE4Ni40NzIgMTUxLjc3NSAxODYuNjQ3IDE1MS41ODhDMTg2LjgyMSAxNTEuNCAxODcuMDE5IDE1MS4yNTggMTg3LjI0IDE1MS4xNjJDMTg3LjQ2MiAxNTEuMDY2IDE4Ny42OTEgMTUxLjAxOCAxODcuOTI4IDE1MS4wMThDMTg4LjIzIDE1MS4wMTggMTg4LjQ5IDE1MS4wNyAxODguNzA5IDE1MS4xNzRDMTg4LjkzMSAxNTEuMjc4IDE4OS4xMTIgMTUxLjQyNCAxODkuMjUyIDE1MS42MTFDMTg5LjM5MyAxNTEuNzk2IDE4OS40OTcgMTUyLjAxNSAxODkuNTY1IDE1Mi4yNjhDMTg5LjYzMiAxNTIuNTE4IDE4OS42NjYgMTUyLjc5MSAxODkuNjY2IDE1My4wODhWMTUzLjQxMkgxODYuNTFWMTUyLjgyMkgxODguOTQ0VjE1Mi43NjhDMTg4LjkzMyAxNTIuNTggMTg4Ljg5NCAxNTIuMzk4IDE4OC44MjYgMTUyLjIyMUMxODguNzYxIDE1Mi4wNDQgMTg4LjY1NyAxNTEuODk4IDE4OC41MTQgMTUxLjc4M0MxODguMzcxIDE1MS42NjkgMTg4LjE3NSAxNTEuNjExIDE4Ny45MjggMTUxLjYxMUMxODcuNzY0IDE1MS42MTEgMTg3LjYxMyAxNTEuNjQ2IDE4Ny40NzUgMTUxLjcxN0MxODcuMzM3IDE1MS43ODUgMTg3LjIxOCAxNTEuODg2IDE4Ny4xMTkgMTUyLjAyMUMxODcuMDIgMTUyLjE1NyAxODYuOTQ0IDE1Mi4zMjIgMTg2Ljg4OSAxNTIuNTE4QzE4Ni44MzQgMTUyLjcxMyAxODYuODA3IDE1Mi45MzggMTg2LjgwNyAxNTMuMTkzVjE1My4zNTdDMTg2LjgwNyAxNTMuNTU4IDE4Ni44MzQgMTUzLjc0NyAxODYuODg5IDE1My45MjRDMTg2Ljk0NiAxNTQuMDk4IDE4Ny4wMjggMTU0LjI1MiAxODcuMTM1IDE1NC4zODVDMTg3LjI0NCAxNTQuNTE4IDE4Ny4zNzYgMTU0LjYyMiAxODcuNTMgMTU0LjY5N0MxODcuNjg2IDE1NC43NzMgMTg3Ljg2MyAxNTQuODExIDE4OC4wNjEgMTU0LjgxMUMxODguMzE2IDE1NC44MTEgMTg4LjUzMiAxNTQuNzU4IDE4OC43MDkgMTU0LjY1NEMxODguODg2IDE1NC41NSAxODkuMDQxIDE1NC40MTEgMTg5LjE3NCAxNTQuMjM2TDE4OS42MTIgMTU0LjU4NEMxODkuNTIgMTU0LjcyMiAxODkuNDA1IDE1NC44NTQgMTg5LjI2NCAxNTQuOTc5QzE4OS4xMjMgMTU1LjEwNCAxODguOTUgMTU1LjIwNSAxODguNzQ0IDE1NS4yODNDMTg4LjU0MSAxNTUuMzYxIDE4OC4zIDE1NS40IDE4OC4wMjIgMTU1LjRaTTE5MC41ODkgMTQ5LjMyMkgxOTEuMzE1VjE1NC41MDJMMTkxLjI1MyAxNTUuMzIySDE5MC41ODlWMTQ5LjMyMlpNMTk0LjE3MSAxNTMuMTc0VjE1My4yNTZDMTk0LjE3MSAxNTMuNTYzIDE5NC4xMzQgMTUzLjg0OCAxOTQuMDYxIDE1NC4xMTFDMTkzLjk4OCAxNTQuMzcyIDE5My44ODIgMTU0LjU5OCAxOTMuNzQxIDE1NC43OTFDMTkzLjYgMTU0Ljk4NCAxOTMuNDI5IDE1NS4xMzMgMTkzLjIyNSAxNTUuMjRDMTkzLjAyMiAxNTUuMzQ3IDE5Mi43ODkgMTU1LjQgMTkyLjUyNiAxNTUuNEMxOTIuMjU4IDE1NS40IDE5Mi4wMjIgMTU1LjM1NSAxOTEuODE5IDE1NS4yNjRDMTkxLjYxOSAxNTUuMTcgMTkxLjQ0OSAxNTUuMDM2IDE5MS4zMTEgMTU0Ljg2MUMxOTEuMTczIDE1NC42ODcgMTkxLjA2MyAxNTQuNDc2IDE5MC45NzkgMTU0LjIyOUMxOTAuODk5IDE1My45ODEgMTkwLjg0MyAxNTMuNzAyIDE5MC44MTEgMTUzLjM5M1YxNTMuMDMzQzE5MC44NDMgMTUyLjcyMSAxOTAuODk5IDE1Mi40NDEgMTkwLjk3OSAxNTIuMTkzQzE5MS4wNjMgMTUxLjk0NiAxOTEuMTczIDE1MS43MzUgMTkxLjMxMSAxNTEuNTYxQzE5MS40NDkgMTUxLjM4MyAxOTEuNjE5IDE1MS4yNDkgMTkxLjgxOSAxNTEuMTU4QzE5Mi4wMiAxNTEuMDY0IDE5Mi4yNTMgMTUxLjAxOCAxOTIuNTE4IDE1MS4wMThDMTkyLjc4NCAxNTEuMDE4IDE5My4wMiAxNTEuMDcgMTkzLjIyNSAxNTEuMTc0QzE5My40MzEgMTUxLjI3NSAxOTMuNjAzIDE1MS40MjEgMTkzLjc0MSAxNTEuNjExQzE5My44ODIgMTUxLjgwMSAxOTMuOTg4IDE1Mi4wMjkgMTk0LjA2MSAxNTIuMjk1QzE5NC4xMzQgMTUyLjU1OCAxOTQuMTcxIDE1Mi44NTEgMTk0LjE3MSAxNTMuMTc0Wk0xOTMuNDQ0IDE1My4yNTZWMTUzLjE3NEMxOTMuNDQ0IDE1Mi45NjMgMTkzLjQyNSAxNTIuNzY1IDE5My4zODYgMTUyLjU4QzE5My4zNDcgMTUyLjM5MyAxOTMuMjg0IDE1Mi4yMjkgMTkzLjE5OCAxNTIuMDg4QzE5My4xMTIgMTUxLjk0NSAxOTIuOTk5IDE1MS44MzMgMTkyLjg1OCAxNTEuNzUyQzE5Mi43MTggMTUxLjY2OSAxOTIuNTQ0IDE1MS42MjcgMTkyLjMzOSAxNTEuNjI3QzE5Mi4xNTYgMTUxLjYyNyAxOTEuOTk4IDE1MS42NTggMTkxLjg2MiAxNTEuNzIxQzE5MS43MjkgMTUxLjc4MyAxOTEuNjE2IDE1MS44NjggMTkxLjUyMiAxNTEuOTc1QzE5MS40MjkgMTUyLjA3OSAxOTEuMzUyIDE1Mi4xOTkgMTkxLjI5MiAxNTIuMzM0QzE5MS4yMzUgMTUyLjQ2NyAxOTEuMTkyIDE1Mi42MDUgMTkxLjE2MyAxNTIuNzQ4VjE1My42ODlDMTkxLjIwNSAxNTMuODcyIDE5MS4yNzIgMTU0LjA0OCAxOTEuMzY2IDE1NC4yMTdDMTkxLjQ2MiAxNTQuMzgzIDE5MS41OSAxNTQuNTIgMTkxLjc0OSAxNTQuNjI3QzE5MS45MSAxNTQuNzM0IDE5Mi4xMSAxNTQuNzg3IDE5Mi4zNDcgMTU0Ljc4N0MxOTIuNTQyIDE1NC43ODcgMTkyLjcwOCAxNTQuNzQ4IDE5Mi44NDcgMTU0LjY3QzE5Mi45ODcgMTU0LjU4OSAxOTMuMSAxNTQuNDc5IDE5My4xODYgMTU0LjMzOEMxOTMuMjc1IDE1NC4xOTcgMTkzLjM0IDE1NC4wMzUgMTkzLjM4MiAxNTMuODVDMTkzLjQyMyAxNTMuNjY1IDE5My40NDQgMTUzLjQ2NyAxOTMuNDQ0IDE1My4yNTZaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjU0Ii8+CjwvZz4KPHBhdGggZD0iTTI3IDEwNy40NDVMNDAgODguMDk2Mkw3NS44NjUgNDUuMDgwMkM3Ni4yMTY2IDQ0LjY1ODYgNzYuODQyMyA0NC41OTkyIDc3LjI2NjkgNDQuOTQ3MUwxMTEuMTM1IDcyLjcwMDZDMTExLjM2NiA3Mi44OTAyIDExMS42NyA3Mi45NjYyIDExMS45NjMgNzIuOTA4TDE0Ni43OTQgNjUuOTkxQzE0Ni45MyA2NS45NjQgMTQ3LjA1OSA2NS45MDg5IDE0Ny4xNzIgNjUuODI5MkwxODQuMjY3IDM5Ljg0NjhDMTg0LjQxOSAzOS43NCAxODQuNTM5IDM5LjU5MjcgMTg0LjYxMiAzOS40MjE1TDE5OC41IDciIHN0cm9rZT0iI0ZCREIwRiIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4KPGNpcmNsZSBjeD0iNzYiIGN5PSI0NS4xNjExIiByPSIxLjUiIGZpbGw9IndoaXRlIiBzdHJva2U9IiNGQkRCMEYiLz4KPGNpcmNsZSBjeD0iMTEyIiBjeT0iNzMuMTYxMSIgcj0iMS41IiBmaWxsPSJ3aGl0ZSIgc3Ryb2tlPSIjRkJEQjBGIi8+CjxjaXJjbGUgY3g9IjE0NyIgY3k9IjY2LjE2MTEiIHI9IjEuNSIgZmlsbD0id2hpdGUiIHN0cm9rZT0iI0ZCREIwRiIvPgo8Y2lyY2xlIGN4PSIxODUiIGN5PSIzOS4xNjExIiByPSIxLjUiIGZpbGw9IndoaXRlIiBzdHJva2U9IiNGQkRCMEYiLz4KPGNpcmNsZSBjeD0iMTk4IiBjeT0iNyIgcj0iMS41IiBmaWxsPSJ3aGl0ZSIgc3Ryb2tlPSIjRkJEQjBGIi8+CjxwYXRoIGQ9Ik0yOC41IDEwNy4xMTRDMjguNSAxMDcuOTcyIDI3LjgxOTcgMTA4LjY1MSAyNyAxMDguNjUxQzI2LjE4MDMgMTA4LjY1MSAyNS41IDEwNy45NzIgMjUuNSAxMDcuMTE0QzI1LjUgMTA2LjI1NiAyNi4xODAzIDEwNS41NzYgMjcgMTA1LjU3NkMyNy44MTk3IDEwNS41NzYgMjguNSAxMDYuMjU2IDI4LjUgMTA3LjExNFoiIGZpbGw9IndoaXRlIiBzdHJva2U9IiNGQkRCMEYiLz4KPHBhdGggZD0iTTQxLjUgODcuNzc2OEM0MS41IDg4LjYzNDcgNDAuODE5NyA4OS4zMTQzIDQwIDg5LjMxNDNDMzkuMTgwMyA4OS4zMTQzIDM4LjUgODguNjM0NyAzOC41IDg3Ljc3NjhDMzguNSA4Ni45MTg4IDM5LjE4MDMgODYuMjM5MyA0MCA4Ni4yMzkzQzQwLjgxOTcgODYuMjM5MyA0MS41IDg2LjkxODggNDEuNSA4Ny43NzY4WiIgZmlsbD0id2hpdGUiIHN0cm9rZT0iI0ZCREIwRiIvPgo8cGF0aCBkPSJNMzIgMTIyQzMyIDEyMC44OTUgMzIuODk1NCAxMjAgMzQgMTIwSDQ4QzQ5LjEwNDYgMTIwIDUwIDEyMC44OTUgNTAgMTIyVjE0NkgzMlYxMjJaIiBmaWxsPSIjM0ZBNzFBIi8+CjxwYXRoIGQ9Ik02NyA3OEM2NyA3Ni44OTU0IDY3Ljg5NTQgNzYgNjkgNzZIODNDODQuMTA0NiA3NiA4NSA3Ni44OTU0IDg1IDc4VjE0Nkg2N1Y3OFoiIGZpbGw9IiMzRkE3MUEiLz4KPHBhdGggZD0iTTEwMiA5MkMxMDIgOTAuODk1NCAxMDIuODk1IDkwIDEwNCA5MEgxMThDMTE5LjEwNSA5MCAxMjAgOTAuODk1NCAxMjAgOTJWMTQ2SDEwMlY5MloiIGZpbGw9IiMzRkE3MUEiLz4KPHBhdGggZD0iTTE3MyAxMDVDMTczIDEwMy44OTUgMTczLjg5NSAxMDMgMTc1IDEwM0gxODlDMTkwLjEwNSAxMDMgMTkxIDEwMy44OTUgMTkxIDEwNVYxNDZIMTczVjEwNVoiIGZpbGw9IiMzRkE3MUEiLz4KPHBhdGggZD0iTTI3IDExOEw0MCA3MS43NzkyTDc4LjUgODRMMTE5IDMyTDE0OCAzNi41TDE4MiA3MS43NzkyTDE5NS4zMTEgMTAwLjIxNkwxOTcgMTA3IiBzdHJva2U9IiM0QjcwREQiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+CjxjaXJjbGUgY3g9Ijc4IiBjeT0iODQuMTYxMSIgcj0iMS41IiBmaWxsPSJ3aGl0ZSIgc3Ryb2tlPSIjNEI3MEREIi8+CjxjaXJjbGUgY3g9IjQwIiBjeT0iNzIuMTYxMSIgcj0iMS41IiBmaWxsPSJ3aGl0ZSIgc3Ryb2tlPSIjNEI3MEREIi8+CjxjaXJjbGUgY3g9IjExOSIgY3k9IjMyIiByPSIxLjUiIGZpbGw9IndoaXRlIiBzdHJva2U9IiM0QjcwREQiLz4KPGNpcmNsZSBjeD0iMTgyIiBjeT0iNzIuMTYxMSIgcj0iMS41IiBmaWxsPSJ3aGl0ZSIgc3Ryb2tlPSIjNEI3MEREIi8+CjxjaXJjbGUgY3g9IjE5NyIgY3k9IjEwNyIgcj0iMS41IiBmaWxsPSJ3aGl0ZSIgc3Ryb2tlPSIjNEI3MEREIi8+CjxjaXJjbGUgY3g9IjI3IiBjeT0iMTE3LjE2MSIgcj0iMS41IiBmaWxsPSJ3aGl0ZSIgc3Ryb2tlPSIjNEI3MEREIi8+CjxjaXJjbGUgY3g9IjE0NyIgY3k9IjM3LjE2MTEiIHI9IjEuNSIgZmlsbD0id2hpdGUiIHN0cm9rZT0iIzRCNzBERCIvPgo8L2c+CjxkZWZzPgo8Y2xpcFBhdGggaWQ9ImNsaXAwXzQwNTNfMTg1MTEzIj4KPHJlY3Qgd2lkdGg9IjIwMCIgaGVpZ2h0PSIxNjAiIGZpbGw9IndoaXRlIi8+CjwvY2xpcFBhdGg+CjxjbGlwUGF0aCBpZD0iY2xpcDFfNDA1M18xODUxMTMiPgo8cmVjdCB3aWR0aD0iMzUuNCIgaGVpZ2h0PSIxMC4zMjIiIGZpbGw9IndoaXRlIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg1OC4zOTk5IDE0NikiLz4KPC9jbGlwUGF0aD4KPGNsaXBQYXRoIGlkPSJjbGlwMl80MDUzXzE4NTExMyI+CjxyZWN0IHdpZHRoPSIzNS40IiBoZWlnaHQ9IjEwLjMyMiIgZmlsbD0id2hpdGUiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDkzLjc5OTggMTQ2KSIvPgo8L2NsaXBQYXRoPgo8Y2xpcFBhdGggaWQ9ImNsaXAzXzQwNTNfMTg1MTEzIj4KPHJlY3Qgd2lkdGg9IjM1LjQiIGhlaWdodD0iMTAuMzIyIiBmaWxsPSJ3aGl0ZSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTI5LjIgMTQ2KSIvPgo8L2NsaXBQYXRoPgo8Y2xpcFBhdGggaWQ9ImNsaXA0XzQwNTNfMTg1MTEzIj4KPHJlY3Qgd2lkdGg9IjM1LjQiIGhlaWdodD0iMTAuMzIyIiBmaWxsPSJ3aGl0ZSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTY0LjYgMTQ2KSIvPgo8L2NsaXBQYXRoPgo8L2RlZnM+Cjwvc3ZnPgo=", + "description": "Displays changes to time-series data over time—for example, temperature or humidity readings.", + "descriptor": { + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, + "resources": [], + "templateHtml": "\n", + "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.timeSeriesChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.timeSeriesChartWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.timeSeriesChartWidget.onLatestDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '80%',\n embedTitlePanel: true,\n hasAdditionalLatestDataKeys: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'temperature', label: 'Temperature', type: 'timeseries', units: '°C', decimals: 0 }];\n }\n };\n}\n", + "settingsSchema": "{}", + "dataKeySettingsSchema": "{}", + "latestDataKeySettingsSchema": "{}", + "settingsDirective": "", + "dataKeySettingsDirective": "", + "latestDataKeySettingsDirective": "", + "hasBasicMode": false, + "basicModeDirective": "", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#FFC107\",\"settings\":{},\"_hash\":0.5534217244004682,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]},\"latestDataKeys\":null}],\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":0,\"realtime\":{\"realtimeType\":0,\"timewindowMs\":60000,\"quickInterval\":\"CURRENT_DAY\",\"interval\":1000},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000},\"timezone\":null},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"showLegend\":true,\"legendConfig\":{\"position\":\"top\",\"sortDataKeys\":false,\"showMin\":false,\"showMax\":false,\"showAvg\":true,\"showTotal\":false,\"showLatest\":false}},\"title\":\"Time series chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"thermostat\",\"iconColor\":\"#1F6BDD\",\"useDashboardTimewindow\":false,\"displayTimewindow\":true,\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"units\":\"\",\"decimals\":null,\"noDataDisplayMessage\":\"\",\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":null,\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true},\"margin\":\"0px\",\"borderRadius\":\"0px\",\"iconSize\":\"0px\"}" + }, + "tags": [ + "chart", + "time series", + "time-series", + "line", + "line chart", + "bar", + "bar chart" + ] +} \ No newline at end of file diff --git a/ui-ngx/patches/echarts+5.4.3.patch b/ui-ngx/patches/echarts+5.4.3.patch new file mode 100644 index 0000000000..bca89c616c --- /dev/null +++ b/ui-ngx/patches/echarts+5.4.3.patch @@ -0,0 +1,208 @@ +diff --git a/node_modules/echarts/lib/component/dataZoom/DataZoomModel.js b/node_modules/echarts/lib/component/dataZoom/DataZoomModel.js +index 112ebe3..9afc9b7 100644 +--- a/node_modules/echarts/lib/component/dataZoom/DataZoomModel.js ++++ b/node_modules/echarts/lib/component/dataZoom/DataZoomModel.js +@@ -422,7 +422,10 @@ function (_super) { + return axisProxy.getDataValueWindow(); + } + } else { +- return this.getAxisProxy(axisDim, axisIndex).getDataValueWindow(); ++ var axisProxy = this.getAxisProxy(axisDim, axisIndex); ++ if (axisProxy) { ++ return axisProxy.getDataValueWindow(); ++ } + } + }; + /** +@@ -449,11 +452,11 @@ function (_super) { + for (var j = 0; j < axisInfo.indexList.length; j++) { + var proxy = this.getAxisProxy(axisDim, axisInfo.indexList[j]); + +- if (proxy.hostedBy(this)) { ++ if (proxy && proxy.hostedBy(this)) { + return proxy; + } + +- if (!firstProxy) { ++ if (proxy && !firstProxy) { + firstProxy = proxy; + } + } +diff --git a/node_modules/echarts/lib/component/dataZoom/InsideZoomView.js b/node_modules/echarts/lib/component/dataZoom/InsideZoomView.js +index 7163279..c37f9c2 100644 +--- a/node_modules/echarts/lib/component/dataZoom/InsideZoomView.js ++++ b/node_modules/echarts/lib/component/dataZoom/InsideZoomView.js +@@ -112,12 +112,15 @@ var getRangeHandlers = { + range[0] = (range[0] - percentPoint) * scale + percentPoint; + range[1] = (range[1] - percentPoint) * scale + percentPoint; // Restrict range. + +- var minMaxSpan = this.dataZoomModel.findRepresentativeAxisProxy().getMinMaxSpan(); +- sliderMove(0, range, [0, 100], 0, minMaxSpan.minSpan, minMaxSpan.maxSpan); +- this.range = range; +- +- if (lastRange[0] !== range[0] || lastRange[1] !== range[1]) { +- return range; ++ var proxy = this.dataZoomModel.findRepresentativeAxisProxy(); ++ if (proxy) { ++ var minMaxSpan = proxy.getMinMaxSpan(); ++ sliderMove(0, range, [0, 100], 0, minMaxSpan.minSpan, minMaxSpan.maxSpan); ++ this.range = range; ++ ++ if (lastRange[0] !== range[0] || lastRange[1] !== range[1]) { ++ return range; ++ } + } + }, + pan: makeMover(function (range, axisModel, coordSysInfo, coordSysMainType, controller, e) { +diff --git a/node_modules/echarts/lib/component/dataZoom/SliderZoomView.js b/node_modules/echarts/lib/component/dataZoom/SliderZoomView.js +index 590ef51..aff8a0a 100644 +--- a/node_modules/echarts/lib/component/dataZoom/SliderZoomView.js ++++ b/node_modules/echarts/lib/component/dataZoom/SliderZoomView.js +@@ -64,7 +64,7 @@ var DEFAULT_MOVE_HANDLE_SIZE = 7; + var HORIZONTAL = 'horizontal'; + var VERTICAL = 'vertical'; + var LABEL_GAP = 5; +-var SHOW_DATA_SHADOW_SERIES_TYPE = ['line', 'bar', 'candlestick', 'scatter']; ++var SHOW_DATA_SHADOW_SERIES_TYPE = ['line', 'bar', 'candlestick', 'scatter', 'custom']; + var REALTIME_ANIMATION_CONFIG = { + easing: 'cubicOut', + duration: 100, +@@ -406,34 +406,37 @@ function (_super) { + var result; + var ecModel = this.ecModel; + dataZoomModel.eachTargetAxis(function (axisDim, axisIndex) { +- var seriesModels = dataZoomModel.getAxisProxy(axisDim, axisIndex).getTargetSeriesModels(); +- each(seriesModels, function (seriesModel) { +- if (result) { +- return; +- } +- +- if (showDataShadow !== true && indexOf(SHOW_DATA_SHADOW_SERIES_TYPE, seriesModel.get('type')) < 0) { +- return; +- } +- +- var thisAxis = ecModel.getComponent(getAxisMainType(axisDim), axisIndex).axis; +- var otherDim = getOtherDim(axisDim); +- var otherAxisInverse; +- var coordSys = seriesModel.coordinateSystem; +- +- if (otherDim != null && coordSys.getOtherAxis) { +- otherAxisInverse = coordSys.getOtherAxis(thisAxis).inverse; +- } +- +- otherDim = seriesModel.getData().mapDimension(otherDim); +- result = { +- thisAxis: thisAxis, +- series: seriesModel, +- thisDim: axisDim, +- otherDim: otherDim, +- otherAxisInverse: otherAxisInverse +- }; +- }, this); ++ var axisProxy = dataZoomModel.getAxisProxy(axisDim, axisIndex); ++ if (axisProxy) { ++ var seriesModels = axisProxy.getTargetSeriesModels(); ++ each(seriesModels, function (seriesModel) { ++ if (result) { ++ return; ++ } ++ ++ if (showDataShadow !== true && indexOf(SHOW_DATA_SHADOW_SERIES_TYPE, seriesModel.get('type')) < 0) { ++ return; ++ } ++ ++ var thisAxis = ecModel.getComponent(getAxisMainType(axisDim), axisIndex).axis; ++ var otherDim = getOtherDim(axisDim); ++ var otherAxisInverse; ++ var coordSys = seriesModel.coordinateSystem; ++ ++ if (otherDim != null && coordSys.getOtherAxis) { ++ otherAxisInverse = coordSys.getOtherAxis(thisAxis).inverse; ++ } ++ ++ otherDim = seriesModel.getData().mapDimension(otherDim); ++ result = { ++ thisAxis: thisAxis, ++ series: seriesModel, ++ thisDim: axisDim, ++ otherDim: otherDim, ++ otherAxisInverse: otherAxisInverse ++ }; ++ }, this); ++ } + }, this); + return result; + }; +@@ -595,12 +598,17 @@ function (_super) { + + var viewExtend = this._getViewExtent(); + +- var minMaxSpan = dataZoomModel.findRepresentativeAxisProxy().getMinMaxSpan(); +- var percentExtent = [0, 100]; +- sliderMove(delta, handleEnds, viewExtend, dataZoomModel.get('zoomLock') ? 'all' : handleIndex, minMaxSpan.minSpan != null ? linearMap(minMaxSpan.minSpan, percentExtent, viewExtend, true) : null, minMaxSpan.maxSpan != null ? linearMap(minMaxSpan.maxSpan, percentExtent, viewExtend, true) : null); +- var lastRange = this._range; +- var range = this._range = asc([linearMap(handleEnds[0], viewExtend, percentExtent, true), linearMap(handleEnds[1], viewExtend, percentExtent, true)]); +- return !lastRange || lastRange[0] !== range[0] || lastRange[1] !== range[1]; ++ var proxy = dataZoomModel.findRepresentativeAxisProxy(); ++ if (proxy) { ++ var minMaxSpan = proxy.getMinMaxSpan(); ++ var percentExtent = [0, 100]; ++ sliderMove(delta, handleEnds, viewExtend, dataZoomModel.get('zoomLock') ? 'all' : handleIndex, minMaxSpan.minSpan != null ? linearMap(minMaxSpan.minSpan, percentExtent, viewExtend, true) : null, minMaxSpan.maxSpan != null ? linearMap(minMaxSpan.maxSpan, percentExtent, viewExtend, true) : null); ++ var lastRange = this._range; ++ var range = this._range = asc([linearMap(handleEnds[0], viewExtend, percentExtent, true), linearMap(handleEnds[1], viewExtend, percentExtent, true)]); ++ return !lastRange || lastRange[0] !== range[0] || lastRange[1] !== range[1]; ++ } else { ++ return false; ++ } + }; + + SliderZoomView.prototype._updateView = function (nonRealtime) { +diff --git a/node_modules/echarts/lib/component/dataZoom/dataZoomProcessor.js b/node_modules/echarts/lib/component/dataZoom/dataZoomProcessor.js +index 3871784..9bab428 100644 +--- a/node_modules/echarts/lib/component/dataZoom/dataZoomProcessor.js ++++ b/node_modules/echarts/lib/component/dataZoom/dataZoomProcessor.js +@@ -91,7 +91,10 @@ var dataZoomProcessor = { + // init stage and not after action dispatch handler, because + // reset should be called after seriesData.restoreData. + dataZoomModel.eachTargetAxis(function (axisDim, axisIndex) { +- dataZoomModel.getAxisProxy(axisDim, axisIndex).reset(dataZoomModel); ++ var axisProxy = dataZoomModel.getAxisProxy(axisDim, axisIndex); ++ if (axisProxy) { ++ axisProxy.reset(dataZoomModel); ++ } + }); // Caution: data zoom filtering is order sensitive when using + // percent range and no min/max/scale set on axis. + // For example, we have dataZoom definition: +@@ -108,7 +111,10 @@ var dataZoomProcessor = { + // and then reset y-axis and filter y-axis. + + dataZoomModel.eachTargetAxis(function (axisDim, axisIndex) { +- dataZoomModel.getAxisProxy(axisDim, axisIndex).filterData(dataZoomModel, api); ++ var axisProxy = dataZoomModel.getAxisProxy(axisDim, axisIndex); ++ if (axisProxy) { ++ axisProxy.filterData(dataZoomModel, api); ++ } + }); + }); + ecModel.eachComponent('dataZoom', function (dataZoomModel) { +diff --git a/node_modules/echarts/lib/component/toolbox/feature/DataZoom.js b/node_modules/echarts/lib/component/toolbox/feature/DataZoom.js +index c5ee405..957ffd2 100644 +--- a/node_modules/echarts/lib/component/toolbox/feature/DataZoom.js ++++ b/node_modules/echarts/lib/component/toolbox/feature/DataZoom.js +@@ -129,10 +129,13 @@ function (_super) { + var axisModel = axis.model; + var dataZoomModel = findDataZoom(dimName, axisModel, ecModel); // Restrict range. + +- var minMaxSpan = dataZoomModel.findRepresentativeAxisProxy(axisModel).getMinMaxSpan(); ++ var proxy = dataZoomModel.findRepresentativeAxisProxy(axisModel); ++ if (proxy) { ++ var minMaxSpan = proxy.getMinMaxSpan(); + +- if (minMaxSpan.minValueSpan != null || minMaxSpan.maxValueSpan != null) { +- minMax = sliderMove(0, minMax.slice(), axis.scale.getExtent(), 0, minMaxSpan.minValueSpan, minMaxSpan.maxValueSpan); ++ if (minMaxSpan.minValueSpan != null || minMaxSpan.maxValueSpan != null) { ++ minMax = sliderMove(0, minMax.slice(), axis.scale.getExtent(), 0, minMaxSpan.minValueSpan, minMaxSpan.maxValueSpan); ++ } + } + + dataZoomModel && (snapshot[dataZoomModel.id] = { diff --git a/ui-ngx/src/app/core/api/widget-subscription.ts b/ui-ngx/src/app/core/api/widget-subscription.ts index ff16af7740..933d836ffd 100644 --- a/ui-ngx/src/app/core/api/widget-subscription.ts +++ b/ui-ngx/src/app/core/api/widget-subscription.ts @@ -61,9 +61,8 @@ import { deepClone, flatFormattedData, formattedDataFormDatasourceData, - isDefined, isDefinedAndNotNull, - isEqual, + isEqual, isUndefined, parseHttpErrorMessage } from '@core/utils'; import { EntityId } from '@app/shared/models/id/entity-id'; @@ -355,7 +354,7 @@ export class WidgetSubscription implements IWidgetSubscription { } this.units = options.units || ''; - this.decimals = isDefined(options.decimals) ? options.decimals : 2; + this.decimals = isDefinedAndNotNull(options.decimals) ? options.decimals : 2; this.loadingData = false; @@ -1491,7 +1490,8 @@ export class WidgetSubscription implements IWidgetSubscription { let datasourceDataArray: Array = []; datasourceDataArray = datasourceDataArray.concat(datasource.dataKeys.map((dataKey, keyIndex) => { dataKey.hidden = !!dataKey.settings.hideDataByDefault; - dataKey.inLegend = !dataKey.settings.removeFromLegend; + dataKey.inLegend = dataKey.settings.showInLegend || + (isUndefined(dataKey.settings.showInLegend) && !dataKey.settings.removeFromLegend); dataKey.label = this.ctx.utils.customTranslation(dataKey.label, dataKey.label); const datasourceData: DatasourceData = { datasource, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.scss index 65be3c924f..8304400818 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.scss @@ -92,6 +92,7 @@ .tb-aggregated-value-card-chart-container { position: relative; flex: 1; + min-width: 0; margin-top: 8px; margin-bottom: 8px; .tb-aggregated-value-card-chart-element { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.ts index 25be87451e..ae7c3c5eee 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.ts @@ -43,16 +43,21 @@ import { getDataKey, getLatestSingleTsValue, overlayStyle, + simpleDateFormat, textStyle } from '@shared/models/widget-settings.models'; -import { TbFlot } from '@home/components/widget/lib/flot-widget'; -import { TbFlotKeySettings, TbFlotSettings } from '@home/components/widget/lib/flot-widget.models'; import { DataKey } from '@shared/models/widget.models'; import { formatNumberValue, formatValue, isDefined, isDefinedAndNotNull, isNumeric } from '@core/utils'; import { map } from 'rxjs/operators'; import { ResizeObserver } from '@juggle/resize-observer'; import { ImagePipe } from '@shared/pipe/image.pipe'; import { DomSanitizer } from '@angular/platform-browser'; +import { TbTimeSeriesChart } from '@home/components/widget/lib/chart/time-series-chart'; +import { + TimeSeriesChartKeySettings, + TimeSeriesChartSeriesType, + TimeSeriesChartSettings +} from '@home/components/widget/lib/chart/time-series-chart.models'; const valuesLayoutHeight = 66; const valuesLayoutVerticalPadding = 16; @@ -101,8 +106,8 @@ export class AggregatedValueCardWidgetComponent implements OnInit, AfterViewInit backgroundStyle$: Observable; overlayStyle: ComponentStyle = {}; - private flot: TbFlot; - private flotDataKey: DataKey; + private lineChart: TbTimeSeriesChart; + private lineChartDataKey: DataKey; private lastUpdateTs: number; @@ -141,12 +146,17 @@ export class AggregatedValueCardWidgetComponent implements OnInit, AfterViewInit this.showChart = this.settings.showChart; if (this.showChart) { if (this.ctx.defaultSubscription.firstDatasource?.dataKeys?.length) { - this.flotDataKey = this.ctx.defaultSubscription.firstDatasource?.dataKeys[0]; - this.flotDataKey.settings = { - fillLines: false, - showLines: true, - lineWidth: 2 - } as TbFlotKeySettings; + this.lineChartDataKey = this.ctx.defaultSubscription.firstDatasource?.dataKeys[0]; + this.lineChartDataKey.settings = { + showPointLabel: false, + type: TimeSeriesChartSeriesType.line, + lineSettings: { + smooth: false, + showLine: true, + lineWidth: 2, + showPoints: false + } + } as TimeSeriesChartKeySettings; } } @@ -161,33 +171,34 @@ export class AggregatedValueCardWidgetComponent implements OnInit, AfterViewInit ngAfterViewInit(): void { if (this.showChart && this.ctx.datasources?.length) { - const settings = { - shadowSize: 0, - enableSelection: false, - smoothLines: false, - grid: { - tickColor: 'rgba(0,0,0,0.12)', - horizontalLines: true, - verticalLines: false, - outlineWidth: 0, - minBorderMargin: 0, - margin: 0 - }, - yaxis: { - showLabels: false, - tickGenerator: 'return [(axis.max + axis.min) / 2];' - }, - xaxis: { - showLabels: false - } - } as TbFlotSettings; - this.flot = new TbFlot(this.ctx, 'line', $(this.chartElement.nativeElement), settings); - this.tickMin$ = this.flot.yMin$.pipe( - map((value) => formatValue(value, (this.flotDataKey?.decimals || this.ctx.decimals)) + const settings: TimeSeriesChartSettings = { + dataZoom: false, + xAxis: { + show: false + }, + yAxis: { + show: true, + showLine: false, + showTicks: false, + showTickLabels: false, + showSplitLines: true, + min: 'dataMin', + max: 'dataMax', + intervalCalculator: + 'var scale = axis.scale; return !scale.isBlank() ? ((scale.getExtent()[1] - scale.getExtent()[0]) / 2) : undefined;' + }, + tooltipDateInterval: false, + tooltipDateFormat: simpleDateFormat('dd MMM yyyy HH:mm:ss') + } as TimeSeriesChartSettings; + + this.lineChart = new TbTimeSeriesChart(this.ctx, settings, this.chartElement.nativeElement, this.renderer, true); + + this.tickMin$ =this.lineChart.yMin$.pipe( + map((value) => formatValue(value, (this.lineChartDataKey?.decimals || this.ctx.decimals)) + )); + this.tickMax$ = this.lineChart.yMax$.pipe( + map((value) => formatValue(value, (this.lineChartDataKey?.decimals || this.ctx.decimals)) )); - this.tickMax$ = this.flot.yMax$.pipe( - map((value) => formatValue(value, (this.flotDataKey?.decimals || this.ctx.decimals)) - )); } if (this.settings.autoScale && this.showValues) { this.renderer.setStyle(this.valueCardValueContainer.nativeElement, 'height', valuesLayoutHeight + 'px'); @@ -221,7 +232,7 @@ export class AggregatedValueCardWidgetComponent implements OnInit, AfterViewInit } if (this.showChart) { - this.flot.update(); + this.lineChart.update(); } this.updateLastUpdateTs(ts); @@ -256,20 +267,14 @@ export class AggregatedValueCardWidgetComponent implements OnInit, AfterViewInit } public onResize() { - if (this.showChart) { - this.flot.resize(); - } } public onEditModeChanged() { - if (this.showChart) { - this.flot.checkMouseEvents(); - } } public onDestroy() { if (this.showChart) { - this.flot.destroy(); + this.lineChart.destroy(); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.html index d945380a10..ba538d8104 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.html @@ -18,8 +18,10 @@
-
-
{{ valueText }}
+
+
+
{{ valueText }}
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.scss index f958bb6354..22e52e05dd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.scss @@ -52,7 +52,18 @@ } .tb-value-chart-card-chart { flex: 1; + min-width: 0; height: 100%; } + &.auto-scale { + .tb-value-chart-card-value-container { + width: 35%; + display: flex; + justify-content: flex-start; + } + .tb-value-chart-card-chart { + width: 65%; + } + } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.ts index d4b8404275..b59098de17 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.ts @@ -34,7 +34,9 @@ import { ColorProcessor, ComponentStyle, getDataKey, - overlayStyle, resolveCssSize, + overlayStyle, + resolveCssSize, + simpleDateFormat, textStyle } from '@shared/models/widget-settings.models'; import { WidgetComponent } from '@home/components/widget/widget.component'; @@ -44,16 +46,20 @@ import { ValueChartCardLayout, ValueChartCardWidgetSettings } from '@home/components/widget/lib/cards/value-chart-card-widget.models'; -import { TbFlot } from '@home/components/widget/lib/flot-widget'; import { DataKey } from '@shared/models/widget.models'; -import { TbFlotKeySettings, TbFlotSettings } from '@home/components/widget/lib/flot-widget.models'; import { getTsValueByLatestDataKey } from '@home/components/widget/lib/cards/aggregated-value-card.models'; import { Observable } from 'rxjs'; import { ImagePipe } from '@shared/pipe/image.pipe'; import { DomSanitizer } from '@angular/platform-browser'; +import { TbTimeSeriesChart } from '@home/components/widget/lib/chart/time-series-chart'; +import { + TimeSeriesChartKeySettings, + TimeSeriesChartSeriesType, + TimeSeriesChartSettings +} from '@home/components/widget/lib/chart/time-series-chart.models'; const layoutHeight = 56; -const valueRelativeMaxWidth = 0.5; +const valueRelativeWidth = 0.35; @Component({ selector: 'tb-value-chart-card-widget', @@ -81,6 +87,7 @@ export class ValueChartCardWidgetComponent implements OnInit, AfterViewInit, OnD widgetTitlePanel: TemplateRef; layout: ValueChartCardLayout; + autoScale: boolean; showValue = true; valueText = 'N/A'; @@ -90,8 +97,8 @@ export class ValueChartCardWidgetComponent implements OnInit, AfterViewInit, OnD backgroundStyle$: Observable; overlayStyle: ComponentStyle = {}; - private flot: TbFlot; - private flotDataKey: DataKey; + private lineChart: TbTimeSeriesChart; + private lineChartDataKey: DataKey; private valueKey: DataKey; private contentResize$: ResizeObserver; @@ -129,6 +136,7 @@ export class ValueChartCardWidgetComponent implements OnInit, AfterViewInit, OnD } this.layout = this.settings.layout; + this.autoScale = this.settings.autoScale; this.showValue = this.settings.showValue; this.valueStyle = textStyle(this.settings.valueFont); @@ -138,37 +146,34 @@ export class ValueChartCardWidgetComponent implements OnInit, AfterViewInit, OnD this.overlayStyle = overlayStyle(this.settings.background.overlay); if (this.ctx.defaultSubscription.firstDatasource?.dataKeys?.length) { - this.flotDataKey = this.ctx.defaultSubscription.firstDatasource?.dataKeys[0]; - this.flotDataKey.settings = { - fillLines: false, - showLines: true, - lineWidth: 2 - } as TbFlotKeySettings; + this.lineChartDataKey = this.ctx.defaultSubscription.firstDatasource?.dataKeys[0]; + this.lineChartDataKey.settings = { + showPointLabel: false, + type: TimeSeriesChartSeriesType.line, + lineSettings: { + smooth: true, + showLine: true, + lineWidth: 2, + showPoints: false + } + } as TimeSeriesChartKeySettings; } } public ngAfterViewInit() { - const settings = { - shadowSize: 0, - enableSelection: false, - smoothLines: true, - grid: { - tickColor: 'rgba(0,0,0,0.12)', - horizontalLines: false, - verticalLines: false, - outlineWidth: 0, - minBorderMargin: 0, - margin: 0 + const settings: TimeSeriesChartSettings = { + dataZoom: false, + xAxis: { + show: false }, - yaxis: { - showLabels: false, - tickGenerator: 'return [(axis.max + axis.min) / 2];' + yAxis: { + show: false }, - xaxis: { - showLabels: false - } - } as TbFlotSettings; - this.flot = new TbFlot(this.ctx, 'line', $(this.chartElement.nativeElement), settings); + tooltipDateInterval: false, + tooltipDateFormat: simpleDateFormat('dd MMM yyyy HH:mm:ss') + } as TimeSeriesChartSettings; + + this.lineChart = new TbTimeSeriesChart(this.ctx, settings, this.chartElement.nativeElement, this.renderer, false); this.contentResize$ = new ResizeObserver(() => { this.onResize(); @@ -190,7 +195,9 @@ export class ValueChartCardWidgetComponent implements OnInit, AfterViewInit, OnD } public onDataUpdated() { - this.flot.update(); + if (this.lineChart) { + this.lineChart.update(); + } } public onLatestDataUpdated() { @@ -206,20 +213,21 @@ export class ValueChartCardWidgetComponent implements OnInit, AfterViewInit, OnD this.valueColor.update(value); this.cd.detectChanges(); setTimeout(() => { - this.onResize(false); + this.onResize(); }, 0); } } public onEditModeChanged() { - this.flot.checkMouseEvents(); } public onDestroy() { - this.flot.destroy(); + if (this.lineChart) { + this.lineChart.destroy(); + } } - private onResize(fitTargetHeight = true) { + private onResize(fitTargetWidth = true) { if (this.settings.autoScale && this.showValue) { const contentWidth = this.valueChartCardContent.nativeElement.getBoundingClientRect().width; const contentHeight = this.valueChartCardContent.nativeElement.getBoundingClientRect().height; @@ -228,26 +236,26 @@ export class ValueChartCardWidgetComponent implements OnInit, AfterViewInit, OnD this.valueFontSize = resolveCssSize(fontSize)[0]; } const valueRelativeHeight = Math.min(this.valueFontSize / layoutHeight, 1); - const targetValueHeight = contentHeight * valueRelativeHeight; - const maxValueWidth = contentWidth * valueRelativeMaxWidth; - this.setValueFontSize(targetValueHeight, maxValueWidth, fitTargetHeight); + const targetValueWidth = contentWidth * valueRelativeWidth; + const maxValueHeight = contentHeight * valueRelativeHeight; + this.setValueFontSize(targetValueWidth, maxValueHeight, fitTargetWidth); } - this.flot.resize(); + this.lineChart.resize(); } - private setValueFontSize(targetHeight: number, maxWidth: number, fitTargetHeight = true) { + private setValueFontSize(targetWidth: number, maxHeight: number, fitTargetWidth = true) { const fontSize = getComputedStyle(this.valueChartCardValue.nativeElement).fontSize; let valueFontSize = resolveCssSize(fontSize)[0]; this.renderer.setStyle(this.valueChartCardValue.nativeElement, 'fontSize', valueFontSize + 'px'); this.renderer.setStyle(this.valueChartCardValue.nativeElement, 'lineHeight', '1'); - let valueHeight = this.valueChartCardValue.nativeElement.getBoundingClientRect().height; - while (fitTargetHeight && valueHeight < targetHeight) { + let valueWidth = this.valueChartCardValue.nativeElement.getBoundingClientRect().width; + while (fitTargetWidth && valueWidth < targetWidth) { valueFontSize++; this.renderer.setStyle(this.valueChartCardValue.nativeElement, 'fontSize', valueFontSize + 'px'); - valueHeight = this.valueChartCardValue.nativeElement.getBoundingClientRect().height; + valueWidth = this.valueChartCardValue.nativeElement.getBoundingClientRect().width; } - let valueWidth = this.valueChartCardValue.nativeElement.getBoundingClientRect().width; - while ((valueHeight > targetHeight || valueWidth > maxWidth) && valueFontSize > 6) { + let valueHeight = this.valueChartCardValue.nativeElement.getBoundingClientRect().height; + while ((valueWidth > targetWidth || valueHeight > maxHeight) && valueFontSize > 6) { valueFontSize--; this.renderer.setStyle(this.valueChartCardValue.nativeElement, 'fontSize', valueFontSize + 'px'); valueWidth = this.valueChartCardValue.nativeElement.getBoundingClientRect().width; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts index 2e2faf35e8..c523bf7927 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts @@ -53,19 +53,14 @@ import { CallbackDataParams, CustomSeriesRenderItem, LabelLayoutOptionCallback } import { ECharts, echartsModule, - EChartsOption, + EChartsOption, EChartsSeriesItem, echartsTooltipFormatter, NamedDataSet, toNamedData } from '@home/components/widget/lib/chart/echarts-widget.models'; import { IntervalMath } from '@shared/models/time/time.models'; -interface BarChartDataItem { - id: string; - dataKey: DataKey; - data: NamedDataSet; - enabled: boolean; -} +type BarChartDataItem = EChartsSeriesItem; interface BarChartLegendItem { id: string; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.models.ts index d0b54fed53..99ea088c92 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.models.ts @@ -75,6 +75,7 @@ export const barChartWithLabelsDefaultSettings: BarChartWithLabelsWidgetSettings }, tooltipValueColor: 'rgba(0, 0, 0, 0.76)', tooltipShowDate: true, + tooltipDateInterval: true, tooltipDateFormat: customDateFormat('MMMM y'), tooltipDateFont: { family: 'Roboto', diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts index 515e47559d..7564bd9cab 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/echarts-widget.models.ts @@ -17,35 +17,45 @@ import * as echarts from 'echarts/core'; import { Axis } from 'echarts'; import AxisModel from 'echarts/types/src/coord/cartesian/AxisModel'; -import { formatValue, isDefinedAndNotNull, isNumber, isString } from '@core/utils'; +import { estimateLabelUnionRect } from 'echarts/lib/coord/axisHelper'; +import { formatValue, isDefinedAndNotNull } from '@core/utils'; import TimeScale from 'echarts/types/src/scale/Time'; import { - DataZoomComponent, DataZoomComponentOption, - GridComponent, GridComponentOption, - MarkLineComponent, MarkLineComponentOption, - TooltipComponent, TooltipComponentOption, - VisualMapComponent, VisualMapComponentOption + DataZoomComponent, + DataZoomComponentOption, + GridComponent, + GridComponentOption, + MarkLineComponent, + MarkLineComponentOption, + TooltipComponent, + TooltipComponentOption, + VisualMapComponent, + VisualMapComponentOption } from 'echarts/components'; import { BarChart, - LineChart, + BarSeriesOption, CustomChart, CustomSeriesOption, + LineChart, LineSeriesOption, - BarSeriesOption, PieSeriesOption, PieChart + PieChart, + PieSeriesOption } from 'echarts/charts'; import { LabelLayout } from 'echarts/features'; import { CanvasRenderer, SVGRenderer } from 'echarts/renderers'; -import { DataEntry, DataSet } from '@shared/models/widget.models'; +import { DataEntry, DataKey, DataSet } from '@shared/models/widget.models'; import { calculateAggIntervalWithWidgetTimeWindow, - Interval, IntervalMath, WidgetTimewindow } from '@shared/models/time/time.models'; import { CallbackDataParams } from 'echarts/types/dist/shared'; import { Renderer2 } from '@angular/core'; import { DateFormatProcessor, DateFormatSettings, Font } from '@shared/models/widget-settings.models'; +import GlobalModel from 'echarts/types/src/model/Global'; +import Axis2D from 'echarts/types/src/coord/cartesian/Axis2D'; +import SeriesModel from 'echarts/types/src/model/Series'; class EChartsModule { private initialized = false; @@ -139,6 +149,133 @@ export type EChartsDataItem = [number, any, number, number]; export type NamedDataSet = {name: string; value: EChartsDataItem}[]; +export type EChartsSeriesItem = { + id: string; + dataKey: DataKey; + data: NamedDataSet; + enabled: boolean; + units?: string; + decimals?: number; +}; + +export const getXAxis = (chart: ECharts): Axis2D => { + const model: GlobalModel = (chart as any).getModel(); + const models = model.queryComponents({mainType: 'xAxis'}); + if (models?.length) { + const axisModel = models[0] as AxisModel; + return axisModel.axis; + } + return null; +}; + +export const getYAxis = (chart: ECharts, axisId: string): Axis2D => { + const model: GlobalModel = (chart as any).getModel(); + const models = model.queryComponents({mainType: 'yAxis', id: axisId}); + if (models?.length) { + const axisModel = models[0] as AxisModel; + return axisModel.axis; + } + return null; +}; + +export const calculateYAxisWidth = (chart: ECharts, axisId: string): number => { + const axis = getYAxis(chart, axisId); + return calculateAxisSize(axis); +}; + +export const calculateXAxisHeight = (chart: ECharts): number => { + const axis = getXAxis(chart); + return calculateAxisSize(axis); +}; + +const calculateAxisSize = (axis: Axis2D): number => { + let size = 0; + if (axis && axis.model.option.show) { + const labelUnionRect = estimateLabelUnionRect(axis); + if (labelUnionRect) { + const margin = axis.model.get(['axisLabel', 'margin']); + const dimension = axis.isHorizontal() ? 'height' : 'width'; + size += labelUnionRect[dimension] + margin; + } + if (!axis.scale.isBlank() && axis.model.get(['axisTick', 'show'])) { + const tickLength = axis.model.get(['axisTick', 'length']); + size += tickLength; + } + } + return size; +}; + +export const measureYAxisNameWidth = (chart: ECharts, axisId: string, name: string): number => { + const axis = getYAxis(chart, axisId); + if (axis) { + return axis.model.getModel('nameTextStyle').getTextRect(name).height; + } + return 0; +}; + +export const measureXAxisNameHeight = (chart: ECharts, name: string): number => { + const axis = getXAxis(chart); + if (axis) { + return axis.model.getModel('nameTextStyle').getTextRect(name).height; + } + return 0; +}; + +export const measureThresholdLabelOffset = (chart: ECharts, axisId: string, thresholdId: string, value: any): [number, number] => { + const axis = getYAxis(chart, axisId); + if (axis && !axis.scale.isBlank()) { + const extent = axis.scale.getExtent(); + const model: GlobalModel = (chart as any).getModel(); + const models = model.queryComponents({mainType: 'series', id: thresholdId}); + if (models?.length) { + const lineSeriesModel = models[0] as SeriesModel; + const markLineModel = lineSeriesModel.getModel('markLine'); + const labelPosition = markLineModel.get(['label', 'position']); + if (labelPosition === 'start' || labelPosition === 'end') { + const labelModel = markLineModel.getModel('label'); + const formatter = markLineModel.get(['label', 'formatter']); + let textWidth = 0; + if (Array.isArray(value)) { + for (const val of value) { + if (val >= extent[0] && val <= extent[1]) { + const textVal = typeof formatter === 'string' ? formatter : formatter({value: val} as CallbackDataParams); + textWidth = Math.max(textWidth, labelModel.getTextRect(textVal).width); + } + } + } else { + if (value >= extent[0] && value <= extent[1]) { + const textVal = typeof formatter === 'string' ? formatter : formatter({value} as CallbackDataParams); + textWidth = labelModel.getTextRect(textVal).width; + } + } + if (!textWidth) { + return [0,0]; + } + const distanceOpt = markLineModel.get(['label', 'distance']); + let distance = 5; + if (distanceOpt) { + distance = typeof distanceOpt === 'number' ? distanceOpt : distanceOpt[0]; + } + const offset = distance + textWidth; + if (labelPosition === 'start') { + return [offset, 0]; + } else { + return [0, offset]; + } + } + } + } + return [0,0]; +}; + +export const getAxisExtent = (chart: ECharts, axisId: string): [number, number] => { + const axis = getYAxis(chart, axisId); + if (axis) { + return axis.scale.getExtent(); + } + return [0,0]; +}; + export const toNamedData = (data: DataSet): NamedDataSet => { if (!data?.length) { return []; @@ -162,11 +299,18 @@ const toEChartsDataItem = (entry: DataEntry): EChartsDataItem => { return item; }; +export enum EChartsTooltipTrigger { + point = 'point', + axis = 'axis' +} + export interface EChartsTooltipWidgetSettings { showTooltip: boolean; + tooltipTrigger?: EChartsTooltipTrigger; tooltipValueFont: Font; tooltipValueColor: string; tooltipShowDate: boolean; + tooltipDateInterval?: boolean; tooltipDateFormat: DateFormatSettings; tooltipDateFont: Font; tooltipDateColor: string; @@ -177,13 +321,15 @@ export interface EChartsTooltipWidgetSettings { export const echartsTooltipFormatter = (renderer: Renderer2, tooltipDateFormat: DateFormatProcessor, settings: EChartsTooltipWidgetSettings, - params: CallbackDataParams[], + params: CallbackDataParams[] | CallbackDataParams, decimals: number, units: string, - focusedSeriesIndex: number): null | HTMLElement => { - if (!params.length || !params[0]) { + focusedSeriesIndex: number, + series?: EChartsSeriesItem[]): null | HTMLElement => { + if (!params || Array.isArray(params) && !params[0]) { return null; } + const firstParam = Array.isArray(params) ? params[0] : params; const tooltipElement: HTMLElement = renderer.createElement('div'); renderer.setStyle(tooltipElement, 'display', 'flex'); renderer.setStyle(tooltipElement, 'flex-direction', 'column'); @@ -192,9 +338,9 @@ export const echartsTooltipFormatter = (renderer: Renderer2, if (settings.tooltipShowDate) { const dateElement: HTMLElement = renderer.createElement('div'); let dateText: string; - const startTs = params[0].value[2]; - const endTs = params[0].value[3]; - if (startTs && endTs && (endTs - 1) > startTs) { + const startTs = firstParam.value[2]; + const endTs = firstParam.value[3]; + if (settings.tooltipDateInterval && startTs && endTs && (endTs - 1) > startTs) { const startDateText = tooltipDateFormat.update(startTs); const endDateText = tooltipDateFormat.update(endTs - 1); if (startDateText === endDateText) { @@ -203,7 +349,7 @@ export const echartsTooltipFormatter = (renderer: Renderer2, dateText = startDateText + ' - ' + endDateText; } } else { - const ts = params[0].value[0]; + const ts = firstParam.value[0]; dateText = tooltipDateFormat.update(ts); } renderer.appendChild(dateElement, renderer.createText(dateText)); @@ -216,14 +362,16 @@ export const echartsTooltipFormatter = (renderer: Renderer2, renderer.appendChild(tooltipElement, dateElement); } let seriesParams: CallbackDataParams = null; - if (focusedSeriesIndex > -1) { + if (Array.isArray(params) && focusedSeriesIndex > -1) { seriesParams = params.find(param => param.seriesIndex === focusedSeriesIndex); + } else if (!Array.isArray(params)) { + seriesParams = params; } if (seriesParams) { - renderer.appendChild(tooltipElement, constructEchartsTooltipSeriesElement(renderer, settings, seriesParams, decimals, units)); - } else { + renderer.appendChild(tooltipElement, constructEchartsTooltipSeriesElement(renderer, settings, seriesParams, decimals, units, series)); + } else if (Array.isArray(params)) { for (seriesParams of params) { - renderer.appendChild(tooltipElement, constructEchartsTooltipSeriesElement(renderer, settings, seriesParams, decimals, units)); + renderer.appendChild(tooltipElement, constructEchartsTooltipSeriesElement(renderer, settings, seriesParams, decimals, units, series)); } } return tooltipElement; @@ -233,7 +381,8 @@ const constructEchartsTooltipSeriesElement = (renderer: Renderer2, settings: EChartsTooltipWidgetSettings, seriesParams: CallbackDataParams, decimals: number, - units: string): HTMLElement => { + units: string, + series?: EChartsSeriesItem[]): HTMLElement => { const labelValueElement: HTMLElement = renderer.createElement('div'); renderer.setStyle(labelValueElement, 'display', 'flex'); renderer.setStyle(labelValueElement, 'flex-direction', 'row'); @@ -262,7 +411,16 @@ const constructEchartsTooltipSeriesElement = (renderer: Renderer2, renderer.setStyle(labelTextElement, 'color', 'rgba(0, 0, 0, 0.76)'); renderer.appendChild(labelElement, labelTextElement); const valueElement: HTMLElement = renderer.createElement('div'); - const value = formatValue(seriesParams.value[1], decimals, units, false); + let formatDecimals = decimals; + let formatUnits = units; + if (series) { + const item = series.find(s => s.id === seriesParams.seriesId); + if (item) { + formatDecimals = item.decimals; + formatUnits = item.units; + } + } + const value = formatValue(seriesParams.value[1], formatDecimals, formatUnits, false); renderer.appendChild(valueElement, renderer.createText(value)); renderer.setStyle(valueElement, 'flex', '1'); renderer.setStyle(valueElement, 'text-align', 'end'); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts index c50813f7b6..31905e70ba 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts @@ -71,6 +71,7 @@ export const rangeChartDefaultSettings: RangeChartWidgetSettings = { }, tooltipValueColor: 'rgba(0, 0, 0, 0.76)', tooltipShowDate: true, + tooltipDateInterval: true, tooltipDateFormat: simpleDateFormat('dd MMM yyyy HH:mm'), tooltipDateFont: { family: 'Roboto', diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-bar.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-bar.models.ts new file mode 100644 index 0000000000..37ce7a9c7f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-bar.models.ts @@ -0,0 +1,134 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// 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. +/// + +import { LinearGradientObject } from 'zrender/lib/graphic/LinearGradient'; +import { Interval, IntervalMath } from '@shared/models/time/time.models'; +import { LabelFormatterCallback, SeriesLabelOption } from 'echarts/types/src/util/types'; +import { TimeSeriesChartDataItem } from '@home/components/widget/lib/chart/time-series-chart.models'; +import { CustomSeriesRenderItemParams } from 'echarts'; +import { CustomSeriesRenderItemAPI, CustomSeriesRenderItemReturn } from 'echarts/types/dist/shared'; +import { isNumeric } from '@core/utils'; +import * as echarts from 'echarts/core'; + +export interface BarVisualSettings { + color: string | LinearGradientObject; + borderColor: string; + borderWidth: number; + borderRadius: number; +} + +export interface BarRenderContext { + barsCount?: number; + barIndex?: number; + timeInterval?: Interval; + visualSettings?: BarVisualSettings; + labelOption?: SeriesLabelOption; + barStackIndex?: number; + currentStackItems?: TimeSeriesChartDataItem[]; +} + +export const renderTimeSeriesBar = (params: CustomSeriesRenderItemParams, api: CustomSeriesRenderItemAPI, + renderCtx: BarRenderContext): CustomSeriesRenderItemReturn => { + const time = api.value(0) as number; + let start = api.value(2) as number; + const end = api.value(3) as number; + let interval = end - start; + const ts = start ? start : time; + if (!start || !end || !interval) { + interval = IntervalMath.numberValue(renderCtx.timeInterval); + start = time - interval / 2; + } + const gap = 0.3; + const barInterval = interval / (renderCtx.barsCount + gap * (renderCtx.barsCount + 3)); + const intervalGap = barInterval * gap * 2; + const barGap = barInterval * gap; + const value = api.value(1); + const startTime = start + intervalGap + (barInterval + barGap) * renderCtx.barIndex; + const delta = barInterval; + let offset = 0; + if (renderCtx.currentStackItems?.length) { + for (let i = 0; i < renderCtx.barStackIndex; i++) { + const stackItem = renderCtx.currentStackItems[i]; + const dataName = ts + ''; + const data = stackItem.data.find(d => d.name === dataName); + if (data) { + const val = data.value[1]; + if (isNumeric(val)) { + offset += Number(val); + } + } + } + } + let lowerLeft: number[]; + if (offset !== 0 && isNumeric(value)) { + lowerLeft = api.coord([startTime, value >= 0 ? Number(value) + offset : offset]); + } else { + lowerLeft = api.coord([startTime, value >= 0 ? value : 0]); + } + const size = api.size([delta, value]); + const width = size[0]; + const height = size[1]; + + const coordSys: {x: number; y: number; width: number; height: number} = params.coordSys as any; + + const rectShape = echarts.graphic.clipRectByRect({ + x: lowerLeft[0], + y: lowerLeft[1], + width, + height + }, { + x: coordSys.x, + y: coordSys.y, + width: coordSys.width, + height: coordSys.height + }); + + const zeroPos = api.coord([0, offset]); + + const style: any = { + fill: renderCtx.visualSettings.color, + stroke: renderCtx.visualSettings.borderColor, + lineWidth: renderCtx.visualSettings.borderWidth + }; + + if (renderCtx.labelOption.show) { + let position = renderCtx.labelOption.position; + if (value < 0) { + if (position === 'top') { + position = 'bottom'; + } else if (position === 'bottom') { + position = 'top'; + } + } + style.text = (renderCtx.labelOption.formatter as LabelFormatterCallback)({value: [null, value]} as any); + style.textDistance = 5; + style.textPosition = position; + style.rich = renderCtx.labelOption.rich; + } + + return rectShape && { + type: 'rect', + id: time + '', + shape: {...rectShape, r: renderCtx.visualSettings.borderRadius}, + style, + focus: 'series', + transition: 'all', + enterFrom: { + style: { opacity: 0 }, + shape: { height: 0, y: zeroPos[1] } + } + }; +}; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html new file mode 100644 index 0000000000..1b96aac669 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.html @@ -0,0 +1,122 @@ + +
+
+ +
+
+
+
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + +
{{ 'legend.Min' | translate }}{{ 'legend.Max' | translate }}{{ 'legend.Avg' | translate }}{{ 'legend.Total' | translate }}{{ 'legend.Latest' | translate }}
+ + + {{ legendData.data[legendKey.dataIndex].min }} + + {{ legendData.data[legendKey.dataIndex].max }} + + {{ legendData.data[legendKey.dataIndex].avg }} + + {{ legendData.data[legendKey.dataIndex].total }} + + {{ legendData.data[legendKey.dataIndex].latest }} +
+
+ + + + + + +
+
+
+
{{ legendKey.dataKey.label }}
+
+
+
+ + + {{ label | translate }} + + {{ legendData.data[legendKey.dataIndex][type] }} + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.scss new file mode 100644 index 0000000000..9d990c50a7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.scss @@ -0,0 +1,181 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * 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. + */ + +$maxLegendWidth: 25%; +$maxLegendHeight: 35%; + +.tb-time-series-chart-panel { + width: 100%; + height: 100%; + position: relative; + display: flex; + flex-direction: column; + gap: 16px; + padding: 20px 24px 24px 24px; + > div:not(.tb-time-series-chart-overlay) { + z-index: 1; + } + .tb-time-series-chart-overlay { + position: absolute; + top: 12px; + left: 12px; + bottom: 12px; + right: 12px; + } + div.tb-widget-title { + padding: 0; + } + .tb-time-series-chart-content { + flex: 1; + min-width: 0; + min-height: 0; + display: flex; + flex-direction: column; + gap: 16px; + &.legend-top { + flex-direction: column-reverse; + } + &.legend-right { + flex-direction: row; + } + &.legend-left { + flex-direction: row-reverse; + } + .tb-time-series-chart-shape { + flex: 1; + min-width: 0; + min-height: 0; + display: flex; + align-items: center; + justify-content: center; + } + &.legend-right, &.legend-left { + .tb-time-series-chart-legend { + display: inline-grid; + grid-auto-flow: column; + grid-template-rows: repeat(auto-fit, minmax(16px, min-content)); + max-width: calc($maxLegendWidth - 8px); + height: fit-content; + max-height: 100%; + } + } + &.legend-top, &.legend-bottom { + .tb-time-series-chart-legend { + align-self: center; + &.tb-simple-legend { + justify-content: center; + } + &:not(.tb-simple-legend) { + width: 100%; + } + } + } + .tb-time-series-chart-legend { + display: flex; + align-items: flex-start; + align-self: stretch; + column-gap: 16px; + row-gap: 8px; + flex-wrap: wrap; + overflow: auto; + width: fit-content; + max-width: 100%; + max-height: calc($maxLegendHeight - 8px); + .tb-time-series-chart-legend-table { + border-spacing: 0; + table-layout: fixed; + &.vertical { + width: 100%; + table-layout: auto; + } + + thead th, tbody th { + position: sticky; + z-index: 1; + backdrop-filter: blur(5000px); + } + thead th { + top: 0; + &:first-child { + left: 0; + z-index: 2; + } + } + tbody th { + left: 0; + } + th, td { + &:not(:last-child) { + padding-right: 8px; + } + } + thead tr, tbody tr:not(:last-child) { + th, td { + padding-bottom: 8px; + } + } + .tb-time-series-chart-legend-item { + align-items: flex-end; + &.left { + align-items: flex-start; + } + } + } + .tb-time-series-chart-legend-item { + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + user-select: none; + .tb-time-series-chart-legend-item-label { + display: flex; + align-items: center; + gap: 4px; + color: #ccc; + white-space: nowrap; + cursor: pointer; + .tb-time-series-chart-legend-item-label-circle { + width: 8px; + height: 8px; + border-radius: 50%; + background-color: #ccc; + } + } + } + .tb-time-series-chart-legend-type-label { + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 16px; + color: rgba(0, 0, 0, 0.38); + white-space: nowrap; + text-align: left; + &.right { + text-align: right; + } + } + .tb-time-series-chart-legend-value { + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 16px; + color: rgba(0, 0, 0, 0.87); + white-space: nowrap; + text-align: right; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts new file mode 100644 index 0000000000..560309b08b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.component.ts @@ -0,0 +1,163 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// 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. +/// + +import { + AfterViewInit, + ChangeDetectorRef, + Component, + ElementRef, + Input, + OnDestroy, + OnInit, + Renderer2, + TemplateRef, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { timeSeriesChartKeyDefaultSettings, TimeSeriesChartKeySettings } from '@home/components/widget/lib/chart/time-series-chart.models'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { Observable } from 'rxjs'; +import { backgroundStyle, ComponentStyle, overlayStyle, textStyle } from '@shared/models/widget-settings.models'; +import { ImagePipe } from '@shared/pipe/image.pipe'; +import { DomSanitizer } from '@angular/platform-browser'; +import { LegendConfig, LegendData, LegendKey, LegendPosition } from '@shared/models/widget.models'; +import { TbTimeSeriesChart } from '@home/components/widget/lib/chart/time-series-chart'; +import { + timeSeriesChartWidgetDefaultSettings, + TimeSeriesChartWidgetSettings +} from '@home/components/widget/lib/chart/time-series-chart-widget.models'; +import { mergeDeep } from '@core/utils'; + +@Component({ + selector: 'tb-time-series-chart-widget', + templateUrl: './time-series-chart-widget.component.html', + styleUrls: ['./time-series-chart-widget.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class TimeSeriesChartWidgetComponent implements OnInit, OnDestroy, AfterViewInit { + + @ViewChild('chartShape', {static: false}) + chartShape: ElementRef; + + settings: TimeSeriesChartWidgetSettings; + + @Input() + ctx: WidgetContext; + + @Input() + widgetTitlePanel: TemplateRef; + + horizontalLegendPosition = false; + + showLegend: boolean; + legendClass: string; + legendConfig: LegendConfig; + legendData: LegendData; + legendKeys: LegendKey[]; + + backgroundStyle$: Observable; + overlayStyle: ComponentStyle = {}; + + legendLabelStyle: ComponentStyle; + disabledLegendLabelStyle: ComponentStyle; + + displayLegendValues = false; + + private timeSeriesChart: TbTimeSeriesChart; + + constructor(private imagePipe: ImagePipe, + private sanitizer: DomSanitizer, + private renderer: Renderer2, + private cd: ChangeDetectorRef) { + } + + ngOnInit(): void { + this.ctx.$scope.timeSeriesChartWidget = this; + this.settings = {...timeSeriesChartWidgetDefaultSettings, ...this.ctx.settings}; + + this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); + this.overlayStyle = overlayStyle(this.settings.background.overlay); + + this.showLegend = this.settings.showLegend; + if (this.showLegend) { + this.legendData = this.ctx.defaultSubscription.legendData; + this.legendConfig = this.settings.legendConfig; + this.legendKeys = this.legendData.keys; + if (this.legendConfig.sortDataKeys) { + this.legendKeys = this.legendData.keys.sort((key1, key2) => key1.dataKey.label.localeCompare(key2.dataKey.label)); + } + this.legendKeys.forEach(legendKey => { + legendKey.dataKey.settings = mergeDeep({} as TimeSeriesChartKeySettings, + timeSeriesChartKeyDefaultSettings, legendKey.dataKey.settings); + legendKey.dataKey.hidden = legendKey.dataKey.settings.dataHiddenByDefault; + }); + this.legendKeys = this.legendKeys.filter(legendKey => legendKey.dataKey.settings.showInLegend); + if (!this.legendKeys.length) { + this.showLegend = false; + } + } + + if (this.showLegend) { + this.horizontalLegendPosition = [LegendPosition.left, LegendPosition.right].includes(this.legendConfig.position); + this.legendClass = `legend-${this.legendConfig.position}`; + this.legendLabelStyle = textStyle(this.settings.legendLabelFont); + this.disabledLegendLabelStyle = textStyle(this.settings.legendLabelFont); + this.legendLabelStyle.color = this.settings.legendLabelColor; + this.displayLegendValues = this.legendConfig.showMin || this.legendConfig.showMax || + this.legendConfig.showAvg || this.legendConfig.showTotal || this.legendConfig.showLatest; + } + } + + ngAfterViewInit() { + this.timeSeriesChart = new TbTimeSeriesChart(this.ctx, this.settings, this.chartShape.nativeElement, this.renderer); + } + + ngOnDestroy() { + if (this.timeSeriesChart) { + this.timeSeriesChart.destroy(); + } + } + + public onInit() { + const borderRadius = this.ctx.$widgetElement.css('borderRadius'); + this.overlayStyle = {...this.overlayStyle, ...{borderRadius}}; + this.cd.detectChanges(); + } + + public onDataUpdated() { + if (this.timeSeriesChart) { + this.timeSeriesChart.update(); + } + } + + public onLatestDataUpdated() { + if (this.timeSeriesChart) { + this.timeSeriesChart.latestUpdated(); + } + } + + public onLegendKeyEnter(legendKey: LegendKey) { + this.timeSeriesChart.keyEnter(legendKey.dataKey); + } + + public onLegendKeyLeave(legendKey: LegendKey) { + this.timeSeriesChart.keyLeave(legendKey.dataKey); + } + + public toggleLegendKey(legendKey: LegendKey) { + this.timeSeriesChart.toggleKey(legendKey.dataKey); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.models.ts new file mode 100644 index 0000000000..ccfca7c4d0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-widget.models.ts @@ -0,0 +1,52 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// 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. +/// + +import { timeSeriesChartDefaultSettings, TimeSeriesChartSettings } from '@home/components/widget/lib/chart/time-series-chart.models'; +import { BackgroundSettings, BackgroundType, Font } from '@shared/models/widget-settings.models'; +import { defaultLegendConfig, LegendConfig, LegendPosition, widgetType } from '@shared/models/widget.models'; +import { mergeDeep } from '@core/utils'; + +export interface TimeSeriesChartWidgetSettings extends TimeSeriesChartSettings { + showLegend: boolean; + legendLabelFont: Font; + legendLabelColor: string; + legendConfig: LegendConfig; + background: BackgroundSettings; +} + +export const timeSeriesChartWidgetDefaultSettings: TimeSeriesChartWidgetSettings = + mergeDeep({} as TimeSeriesChartWidgetSettings, timeSeriesChartDefaultSettings as TimeSeriesChartWidgetSettings, { + showLegend: true, + legendLabelFont: { + family: 'Roboto', + size: 12, + sizeUnit: 'px', + style: 'normal', + weight: '400', + lineHeight: '16px' + }, + legendLabelColor: 'rgba(0, 0, 0, 0.76)', + legendConfig: {...defaultLegendConfig(widgetType.timeseries), position: LegendPosition.top}, + background: { + type: BackgroundType.color, + color: '#fff', + overlay: { + enabled: false, + color: 'rgba(255,255,255,0.72)', + blur: 3 + } + } + } as TimeSeriesChartWidgetSettings); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts new file mode 100644 index 0000000000..ef8b432e46 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts @@ -0,0 +1,873 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// 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. +/// + +import { + ECharts, + EChartsOption, + EChartsSeriesItem, + EChartsTooltipTrigger, + EChartsTooltipWidgetSettings, + measureThresholdLabelOffset +} from '@home/components/widget/lib/chart/echarts-widget.models'; +import { ComponentStyle, Font, simpleDateFormat, textStyle } from '@shared/models/widget-settings.models'; +import { XAXisOption, YAXisOption } from 'echarts/types/dist/shared'; +import { CustomSeriesOption, LineSeriesOption } from 'echarts/charts'; +import { formatValue, isDefinedAndNotNull, parseFunction } from '@core/utils'; +import { LinearGradientObject } from 'zrender/lib/graphic/LinearGradient'; +import tinycolor from 'tinycolor2'; +import Axis2D from 'echarts/types/src/coord/cartesian/Axis2D'; +import { Interval } from '@shared/models/time/time.models'; +import { ValueAxisBaseOption } from 'echarts/types/src/coord/axisCommonTypes'; +import { SeriesLabelOption } from 'echarts/types/src/util/types'; +import { + BarRenderContext, + BarVisualSettings, + renderTimeSeriesBar +} from '@home/components/widget/lib/chart/time-series-chart-bar.models'; +import { DataKey } from '@shared/models/widget.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { TbColorScheme } from '@shared/models/color.models'; + +export enum PointLabelPosition { + top = 'top', + bottom = 'bottom' +} + +export enum AxisPosition { + left = 'left', + right = 'right', + top = 'top', + bottom = 'bottom' +} + +export enum TimeSeriesChartShape { + emptyCircle = 'emptyCircle', + circle = 'circle', + rect = 'rect', + roundRect = 'roundRect', + triangle = 'triangle', + diamond = 'diamond', + pin = 'pin', + arrow = 'arrow', + none = 'none' +} + +export enum TimeSeriesChartLineType { + solid = 'solid', + dashed = 'dashed', + dotted = 'dotted' +} + +export enum ThresholdLabelPosition { + start = 'start', + middle = 'middle', + end = 'end', + insideStart = 'insideStart', + insideStartTop = 'insideStartTop', + insideStartBottom = 'insideStartBottom', + insideMiddle = 'insideMiddle', + insideMiddleTop = 'insideMiddleTop', + insideMiddleBottom = 'insideMiddleBottom', + insideEnd = 'insideEnd', + insideEndTop = 'insideEndTop', + insideEndBottom = 'insideEndBottom' +} + +export interface TimeSeriesChartAxisSettings { + show: boolean; + position: AxisPosition; + label?: string; + labelFont?: Font; + labelColor?: string; + showLine: boolean; + lineColor: string; + showTicks: boolean; + ticksColor: string; + showTickLabels: boolean; + tickLabelFont: Font; + tickLabelColor: string; + showSplitLines: boolean; + splitLinesColor: string; +} + +export interface TimeSeriesChartYAxisSettings extends TimeSeriesChartAxisSettings { + min?: number | string; + max?: number | string; + intervalCalculator?: string; +} + +export enum TimeSeriesChartThresholdType { + constant = 'constant', + latestKey = 'latestKey', + entity = 'entity' +} + +export interface TimeSeriesChartThreshold { + type: TimeSeriesChartThresholdType; + value?: number; + latestKeyName?: string; + entityAlias?: string; + entityKeyType?: DataKeyType.attribute | DataKeyType.timeseries; + entityKey?: string; + units?: string; + decimals?: number; + lineWidth: number; + lineType: TimeSeriesChartLineType; + lineColor: string; + startSymbol: TimeSeriesChartShape; + startSymbolSize: number; + endSymbol: TimeSeriesChartShape; + endSymbolSize: number; + showLabel: boolean; + labelPosition: ThresholdLabelPosition; + labelFont: Font; + labelColor: string; +} + +const timeSeriesChartColorScheme: TbColorScheme = { + 'threshold.line': { + light: 'rgba(0, 0, 0, 0.76)', + dark: '#eee' + }, + 'threshold.label': { + light: 'rgba(0, 0, 0, 0.76)', + dark: '#eee' + }, + 'axis.line': { + light: 'rgba(0, 0, 0, 0.54)', + dark: '#B9B8CE' + }, + 'axis.label': { + light: 'rgba(0, 0, 0, 0.54)', + dark: '#B9B8CE' + }, + 'axis.ticks': { + light: 'rgba(0, 0, 0, 0.54)', + dark: '#B9B8CE' + }, + 'axis.tickLabel': { + light: 'rgba(0, 0, 0, 0.54)', + dark: '#B9B8CE' + }, + 'axis.splitLine': { + light: 'rgba(0, 0, 0, 0.12)', + dark: '#484753' + }, + 'series.pointLabel': { + light: 'rgba(0, 0, 0, 0.76)', + dark: '#eee' + } +}; + +export const timeSeriesChartThresholdDefaultSettings: TimeSeriesChartThreshold = { + type: TimeSeriesChartThresholdType.constant, + units: '', + decimals: 0, + lineWidth: 1, + lineType: TimeSeriesChartLineType.solid, + lineColor: timeSeriesChartColorScheme['threshold.line'].light, + startSymbol: TimeSeriesChartShape.none, + startSymbolSize: 5, + endSymbol: TimeSeriesChartShape.arrow, + endSymbolSize: 5, + showLabel: true, + labelPosition: ThresholdLabelPosition.end, + labelFont: { + family: 'Roboto', + size: 12, + sizeUnit: 'px', + style: 'normal', + weight: '400', + lineHeight: '1' + }, + labelColor: timeSeriesChartColorScheme['threshold.label'].light +}; + +export interface TimeSeriesChartSettings extends EChartsTooltipWidgetSettings { + darkMode: boolean; + dataZoom: boolean; + stack: boolean; + thresholds: TimeSeriesChartThreshold[]; + xAxis: TimeSeriesChartAxisSettings; + yAxis: TimeSeriesChartYAxisSettings; +} + +export const timeSeriesChartDefaultSettings: TimeSeriesChartSettings = { + darkMode: false, + dataZoom: true, + stack: false, + thresholds: [], + xAxis: { + show: true, + position: AxisPosition.bottom, + label: '', + labelFont: { + family: 'Roboto', + size: 12, + sizeUnit: 'px', + style: 'normal', + weight: '600', + lineHeight: '1' + }, + labelColor: timeSeriesChartColorScheme['axis.label'].light, + showLine: true, + lineColor: timeSeriesChartColorScheme['axis.line'].light, + showTicks: true, + ticksColor: timeSeriesChartColorScheme['axis.ticks'].light, + showTickLabels: true, + tickLabelFont: { + family: 'Roboto', + size: 10, + sizeUnit: 'px', + style: 'normal', + weight: '400', + lineHeight: '1' + }, + tickLabelColor: timeSeriesChartColorScheme['axis.tickLabel'].light, + showSplitLines: true, + splitLinesColor: timeSeriesChartColorScheme['axis.splitLine'].light + }, + yAxis: { + show: true, + position: AxisPosition.left, + label: '', + labelFont: { + family: 'Roboto', + size: 12, + sizeUnit: 'px', + style: 'normal', + weight: '600', + lineHeight: '1' + }, + labelColor: timeSeriesChartColorScheme['axis.label'].light, + showLine: true, + lineColor: timeSeriesChartColorScheme['axis.line'].light, + showTicks: true, + ticksColor: timeSeriesChartColorScheme['axis.ticks'].light, + showTickLabels: true, + tickLabelFont: { + family: 'Roboto', + size: 12, + sizeUnit: 'px', + style: 'normal', + weight: '400', + lineHeight: '1' + }, + tickLabelColor: timeSeriesChartColorScheme['axis.tickLabel'].light, + showSplitLines: true, + splitLinesColor: timeSeriesChartColorScheme['axis.splitLine'].light + }, + showTooltip: true, + tooltipTrigger: EChartsTooltipTrigger.axis, + tooltipValueFont: { + family: 'Roboto', + size: 12, + sizeUnit: 'px', + style: 'normal', + weight: '500', + lineHeight: '16px' + }, + tooltipValueColor: 'rgba(0, 0, 0, 0.76)', + tooltipShowDate: true, + tooltipDateInterval: true, + tooltipDateFormat: simpleDateFormat('dd MMM yyyy HH:mm:ss'), + tooltipDateFont: { + family: 'Roboto', + size: 11, + sizeUnit: 'px', + style: 'normal', + weight: '400', + lineHeight: '16px' + }, + tooltipDateColor: 'rgba(0, 0, 0, 0.76)', + tooltipBackgroundColor: 'rgba(255, 255, 255, 0.76)', + tooltipBackgroundBlur: 4 +}; + +export enum SeriesFillType { + none = 'none', + opacity = 'opacity', + gradient = 'gradient' +} + +export interface SeriesFillSettings { + type: SeriesFillType; + opacity: number; + gradient: { + start: number; + end: number; + }; +} + +export interface LineSeriesSettings { + step: false | 'start' | 'end' | 'middle'; + smooth: boolean; + showLine: boolean; + lineWidth: number; + lineType: TimeSeriesChartLineType; + fillAreaSettings: SeriesFillSettings; + showPoints: boolean; + pointShape: TimeSeriesChartShape; + pointSize: number; +} + +export interface BarSeriesSettings { + showBorder: boolean; + borderWidth: number; + borderRadius: number; + backgroundSettings: SeriesFillSettings; +} + +export enum TimeSeriesChartSeriesType { + line = 'line', + bar = 'bar' +} + +export interface TimeSeriesChartKeySettings { + dataHiddenByDefault: boolean; + showInLegend: boolean; + showPointLabel: boolean; + pointLabelPosition: PointLabelPosition; + pointLabelFont: Font; + pointLabelColor: string; + type: TimeSeriesChartSeriesType; + lineSettings: LineSeriesSettings; + barSettings: BarSeriesSettings; +} + +export const timeSeriesChartKeyDefaultSettings: TimeSeriesChartKeySettings = { + showInLegend: true, + dataHiddenByDefault: false, + showPointLabel: false, + pointLabelPosition: PointLabelPosition.top, + pointLabelFont: { + family: 'Roboto', + size: 11, + sizeUnit: 'px', + style: 'normal', + weight: '400', + lineHeight: '1' + }, + pointLabelColor: timeSeriesChartColorScheme['series.pointLabel'].light, + type: TimeSeriesChartSeriesType.line, + lineSettings: { + step: false, + smooth: false, + showLine: true, + lineWidth: 2, + lineType: TimeSeriesChartLineType.solid, + fillAreaSettings: { + type: SeriesFillType.none, + opacity: 0.4, + gradient: { + start: 100, + end: 0 + } + }, + showPoints: false, + pointShape: TimeSeriesChartShape.emptyCircle, + pointSize: 4 + }, + barSettings: { + showBorder: false, + borderWidth: 2, + borderRadius: 0, + backgroundSettings: { + type: SeriesFillType.none, + opacity: 0.4, + gradient: { + start: 100, + end: 0 + } + } + } +}; + +export interface TimeSeriesChartDataItem extends EChartsSeriesItem { + yAxisIndex: number; + option?: LineSeriesOption | CustomSeriesOption; + barRenderContext?: BarRenderContext; +} + +type TimeSeriesChartThresholdValue = number | string | (number | string)[]; + +export interface TimeSeriesChartThresholdItem { + id: string; + yAxisIndex: number; + latestDataKey?: DataKey; + units?: string; + decimals?: number; + value: TimeSeriesChartThresholdValue; + settings: TimeSeriesChartThreshold; + option?: LineSeriesOption; +} + +export interface TimeSeriesChartYAxis { + id: string; + units: string; + option: YAXisOption & ValueAxisBaseOption; + intervalCalculator?: (axis: Axis2D) => number; +} + +export const createTimeSeriesYAxis = (axisId: string, units: string, + decimals: number, settings: TimeSeriesChartYAxisSettings, + darkMode: boolean): TimeSeriesChartYAxis => { + const yAxisTickLabelStyle = createChartTextStyle(settings.tickLabelFont, + settings.tickLabelColor, darkMode, 'axis.tickLabel'); + const yAxisNameStyle = createChartTextStyle(settings.labelFont, + settings.labelColor, darkMode, 'axis.label'); + const yAxis: TimeSeriesChartYAxis = { + id: axisId, + units, + option: { + show: settings.show, + type: 'value', + position: settings.position, + id: axisId, + offset: 0, + alignTicks: true, + scale: true, + min: settings.min, + max: settings.max, + name: settings.label, + nameLocation: 'middle', + nameRotate: settings.position === AxisPosition.left ? 90 : -90, + nameTextStyle: { + color: yAxisNameStyle.color, + fontStyle: yAxisNameStyle.fontStyle, + fontWeight: yAxisNameStyle.fontWeight, + fontFamily: yAxisNameStyle.fontFamily, + fontSize: yAxisNameStyle.fontSize + }, + axisLine: { + show: settings.showLine, + onZero: false, + lineStyle: { + color: prepareChartThemeColor(settings.lineColor, darkMode, 'axis.line') + } + }, + axisTick: { + show: settings.showTicks, + lineStyle: { + color: prepareChartThemeColor(settings.ticksColor, darkMode, 'axis.ticks') + } + }, + axisLabel: { + show: settings.showTickLabels, + color: yAxisTickLabelStyle.color, + fontStyle: yAxisTickLabelStyle.fontStyle, + fontWeight: yAxisTickLabelStyle.fontWeight, + fontFamily: yAxisTickLabelStyle.fontFamily, + fontSize: yAxisTickLabelStyle.fontSize, + formatter: (value: any) => formatValue(value, decimals, units, false) + }, + splitLine: { + show: settings.showSplitLines, + lineStyle: { + color: prepareChartThemeColor(settings.splitLinesColor, darkMode, 'axis.splitLine') + } + } + } + }; + if (settings.intervalCalculator && settings.intervalCalculator.length) { + yAxis.intervalCalculator = parseFunction(settings.intervalCalculator, ['axis']); + } + return yAxis; +}; + +export const createTimeSeriesXAxisOption = (settings: TimeSeriesChartAxisSettings, + min: number, max: number, darkMode: boolean): XAXisOption => { + const xAxisTickLabelStyle = createChartTextStyle(settings.tickLabelFont, + settings.tickLabelColor, darkMode, 'axis.tickLabel'); + const xAxisNameStyle = createChartTextStyle(settings.labelFont, + settings.labelColor, darkMode, 'axis.label'); + return { + show: settings.show, + type: 'time', + scale: true, + position: settings.position, + name: settings.label, + nameLocation: 'middle', + nameTextStyle: { + color: xAxisNameStyle.color, + fontStyle: xAxisNameStyle.fontStyle, + fontWeight: xAxisNameStyle.fontWeight, + fontFamily: xAxisNameStyle.fontFamily, + fontSize: xAxisNameStyle.fontSize + }, + axisTick: { + show: settings.showTicks, + lineStyle: { + color: prepareChartThemeColor(settings.ticksColor, darkMode, 'axis.ticks') + } + }, + axisLabel: { + show: settings.showTickLabels, + color: xAxisTickLabelStyle.color, + fontStyle: xAxisTickLabelStyle.fontStyle, + fontWeight: xAxisTickLabelStyle.fontWeight, + fontFamily: xAxisTickLabelStyle.fontFamily, + fontSize: xAxisTickLabelStyle.fontSize, + hideOverlap: true + }, + axisLine: { + show: settings.showLine, + onZero: false, + lineStyle: { + color: prepareChartThemeColor(settings.lineColor, darkMode, 'axis.line') + } + }, + splitLine: { + show: settings.showSplitLines, + lineStyle: { + color: prepareChartThemeColor(settings.splitLinesColor, darkMode, 'axis.splitLine') + } + }, + min, + max + }; +}; + +export const generateChartData = (dataItems: TimeSeriesChartDataItem[], + thresholdItems: TimeSeriesChartThresholdItem[], + timeInterval: Interval, + stack: boolean, + darkMode: boolean): Array => { + let series = generateChartSeries(dataItems, timeInterval, stack, darkMode); + if (thresholdItems.length) { + const thresholds = generateChartThresholds(thresholdItems, darkMode); + series = series.concat(thresholds); + } + return series; +}; + +export const calculateThresholdsOffset = (chart: ECharts, + thresholdItems: TimeSeriesChartThresholdItem[], + yAxisList: TimeSeriesChartYAxis[]): [number, number] => { + const result: [number, number] = [0, 0]; + for (const item of thresholdItems) { + const yAxis = yAxisList[item.yAxisIndex]; + const offset = measureThresholdLabelOffset(chart, yAxis.id, item.id, item.value); + result[0] = Math.max(result[0], offset[0]); + result[1] = Math.max(result[1], offset[1]); + } + return result; +}; + +export const parseThresholdData = (value: any): TimeSeriesChartThresholdValue => { + let thresholdValue: TimeSeriesChartThresholdValue; + if (Array.isArray(value)) { + thresholdValue = value; + } else { + try { + const parsedData = JSON.parse(value); + thresholdValue = Array.isArray(parsedData) ? parsedData : [parsedData]; + } catch (e) { + thresholdValue = [value]; + } + } + return thresholdValue; +}; + +const generateChartThresholds = (thresholdItems: TimeSeriesChartThresholdItem[], darkMode: boolean): Array => { + const series: Array = []; + for (const item of thresholdItems) { + if (isDefinedAndNotNull(item.value)) { + let seriesOption = item.option; + if (!item.option) { + const thresholdLabelStyle = createChartTextStyle(item.settings.labelFont, + item.settings.labelColor, darkMode, 'threshold.label'); + seriesOption = { + type: 'line', + id: item.id, + dataGroupId: item.id, + yAxisIndex: item.yAxisIndex, + animation: true, + data: [], + tooltip: { + show: false + }, + markLine: { + animation: true, + symbol: [item.settings.startSymbol, item.settings.endSymbol], + symbolSize: [item.settings.startSymbolSize, item.settings.endSymbolSize], + lineStyle: { + width: item.settings.lineWidth, + color: prepareChartThemeColor(item.settings.lineColor, darkMode, 'threshold.line'), + type: item.settings.lineType + }, + label: { + show: item.settings.showLabel, + position: item.settings.labelPosition, + color: thresholdLabelStyle.color, + fontStyle: thresholdLabelStyle.fontStyle, + fontWeight: thresholdLabelStyle.fontWeight, + fontFamily: thresholdLabelStyle.fontFamily, + fontSize: thresholdLabelStyle.fontSize, + formatter: params => formatValue(params.value, item.decimals, + item.units, false) + }, + emphasis: { + disabled: true + } + } + }; + item.option = seriesOption; + } + seriesOption.markLine.data = []; + if (Array.isArray(item.value)) { + for (const val of item.value) { + seriesOption.markLine.data.push({ + yAxis: val + }); + } + } else { + seriesOption.markLine.data.push({ + yAxis: item.value + }); + } + series.push(seriesOption); + } + } + return series; +}; + +const generateChartSeries = (dataItems: TimeSeriesChartDataItem[], + timeInterval: Interval, + stack: boolean, + darkMode: boolean): Array => { + const series: Array = []; + const enabledDataItems = dataItems.filter(d => d.enabled); + const barDataItems = enabledDataItems.filter(d => + d.dataKey.settings.type === TimeSeriesChartSeriesType.bar && d.data.length); + let barsCount = barDataItems.length; + const barGroups: number[] = []; + if (stack) { + barDataItems.forEach(item => { + if (barGroups.indexOf(item.yAxisIndex) === -1) { + barGroups.push(item.yAxisIndex); + } + }); + barsCount = barGroups.length; + } + for (const item of enabledDataItems) { + if (item.dataKey.settings.type === TimeSeriesChartSeriesType.bar) { + if (!item.barRenderContext) { + item.barRenderContext = {}; + } + item.barRenderContext.barsCount = barsCount; + item.barRenderContext.barIndex = stack ? barGroups.indexOf(item.yAxisIndex) : barDataItems.indexOf(item); + item.barRenderContext.timeInterval = timeInterval; + if (stack) { + const stackItems = enabledDataItems.filter(d => d.yAxisIndex === item.yAxisIndex); + item.barRenderContext.currentStackItems = stackItems; + item.barRenderContext.barStackIndex = stackItems.indexOf(item); + } + } + const seriesOption = createTimeSeriesChartSeries(item, stack, darkMode); + series.push(seriesOption); + } + return series; +}; + +export const updateDarkMode = (options: EChartsOption, settings: TimeSeriesChartSettings, + dataItems: TimeSeriesChartDataItem[], thresholdDataItems: TimeSeriesChartThresholdItem[], + darkMode: boolean): EChartsOption => { + options.darkMode = darkMode; + if (Array.isArray(options.yAxis)) { + for (const yAxis of options.yAxis) { + yAxis.nameTextStyle.color = prepareChartThemeColor(settings.yAxis.labelColor, darkMode, 'axis.label'); + yAxis.axisLabel.color = prepareChartThemeColor(settings.yAxis.tickLabelColor, darkMode, 'axis.tickLabel'); + yAxis.axisLine.lineStyle.color = prepareChartThemeColor(settings.yAxis.lineColor, darkMode, 'axis.line'); + yAxis.axisTick.lineStyle.color = prepareChartThemeColor(settings.yAxis.ticksColor, darkMode, 'axis.ticks'); + yAxis.splitLine.lineStyle.color = prepareChartThemeColor(settings.yAxis.splitLinesColor, darkMode, 'axis.splitLine'); + } + } + if (Array.isArray(options.xAxis)) { + for (const xAxis of options.xAxis) { + xAxis.nameTextStyle.color = prepareChartThemeColor(settings.xAxis.labelColor, darkMode, 'axis.label'); + xAxis.axisLabel.color = prepareChartThemeColor(settings.xAxis.tickLabelColor, darkMode, 'axis.tickLabel'); + xAxis.axisLine.lineStyle.color = prepareChartThemeColor(settings.xAxis.lineColor, darkMode, 'axis.line'); + xAxis.axisTick.lineStyle.color = prepareChartThemeColor(settings.xAxis.ticksColor, darkMode, 'axis.ticks'); + xAxis.splitLine.lineStyle.color = prepareChartThemeColor(settings.xAxis.splitLinesColor, darkMode, 'axis.splitLine'); + } + } + for (const item of dataItems) { + if (item.dataKey.settings.type === TimeSeriesChartSeriesType.line) { + if (item.option.label?.show) { + item.option.label.rich.value.color = prepareChartThemeColor(item.dataKey.settings.pointLabelColor, darkMode, 'series.pointLabel'); + } + if (Array.isArray(options.series)) { + const series = options.series.find(s => s.id === item.id); + if (series) { + if (series.label?.show) { + series.label.rich.value.color = prepareChartThemeColor(item.dataKey.settings.pointLabelColor, darkMode, 'series.pointLabel'); + } + } + } + } else { + if (item.barRenderContext?.labelOption?.show) { + item.barRenderContext.labelOption.rich.value.color = prepareChartThemeColor(item.dataKey.settings.pointLabelColor, + darkMode, 'series.pointLabel'); + } + } + } + for (const item of thresholdDataItems) { + if (Array.isArray(options.series)) { + const series = options.series.find(s => s.id === item.id); + if (series) { + series.markLine.lineStyle.color = prepareChartThemeColor(item.settings.lineColor, darkMode, 'threshold.line'); + if (series.markLine?.label?.show) { + series.markLine.label.color = prepareChartThemeColor(item.settings.labelColor, darkMode, 'threshold.label'); + } + } + } + } + return options; +}; + +const createTimeSeriesChartSeries = (item: TimeSeriesChartDataItem, + stack: boolean, + darkMode: boolean): LineSeriesOption | CustomSeriesOption => { + let seriesOption = item.option; + if (!seriesOption) { + const dataKey = item.dataKey; + const settings: TimeSeriesChartKeySettings = dataKey.settings; + const seriesColor = item.dataKey.color; + let pointLabelStyle: ComponentStyle = {}; + if (settings.showPointLabel) { + pointLabelStyle = createChartTextStyle(settings.pointLabelFont, settings.pointLabelColor, darkMode, 'series.pointLabel'); + } + const label: SeriesLabelOption = { + show: settings.showPointLabel, + position: settings.pointLabelPosition, + formatter: (params): string => { + const value = formatValue(params.value[1], item.decimals, item.units, false); + return `{value|${value}}`; + }, + rich: { + value: pointLabelStyle + } + }; + seriesOption = { + id: item.id, + dataGroupId: item.id, + yAxisIndex: item.yAxisIndex, + name: item.dataKey.label, + color: seriesColor, + stack: stack ? (item.yAxisIndex + '') : undefined, + stackStrategy: 'all', + emphasis: { + focus: 'series' + }, + animation: true, + dimensions: [ + {name: 'intervalStart', type: 'number'}, + {name: 'intervalEnd', type: 'number'} + ], + encode: { + intervalStart: 2, + intervalEnd: 3 + } + }; + item.option = seriesOption; + if (settings.type === TimeSeriesChartSeriesType.line) { + const lineSettings = settings.lineSettings; + const lineSeriesOption = seriesOption as LineSeriesOption; + lineSeriesOption.type = 'line'; + lineSeriesOption.label = label; + lineSeriesOption.step = lineSettings.step; + lineSeriesOption.smooth = lineSettings.smooth; + lineSeriesOption.lineStyle = { + width: lineSettings.showLine ? lineSettings.lineWidth : 0, + type: lineSettings.lineType + }; + if (lineSettings.fillAreaSettings.type !== SeriesFillType.none) { + lineSeriesOption.areaStyle = {}; + if (lineSettings.fillAreaSettings.type === SeriesFillType.opacity) { + lineSeriesOption.areaStyle.opacity = lineSettings.fillAreaSettings.opacity; + } else { + lineSeriesOption.areaStyle.opacity = 1; + lineSeriesOption.areaStyle.color = createLinearOpacityGradient(seriesColor, lineSettings.fillAreaSettings.gradient); + } + } + lineSeriesOption.showSymbol = lineSettings.showPoints; + lineSeriesOption.symbol = lineSettings.pointShape; + lineSeriesOption.symbolSize = lineSettings.pointSize; + } else { + const barSettings = settings.barSettings; + const barSeriesOption = (seriesOption as any) as CustomSeriesOption; + barSeriesOption.type = 'custom'; + const barVisualSettings: BarVisualSettings = { + color: seriesColor, + borderColor: seriesColor, + borderWidth: barSettings.showBorder ? barSettings.borderWidth : 0, + borderRadius: barSettings.borderRadius + }; + if (barSettings.backgroundSettings.type === SeriesFillType.none) { + barVisualSettings.color = seriesColor; + } else if (barSettings.backgroundSettings.type === SeriesFillType.opacity) { + barVisualSettings.color = tinycolor(seriesColor).setAlpha(barSettings.backgroundSettings.opacity).toRgbString(); + } else { + barVisualSettings.color = createLinearOpacityGradient(seriesColor, barSettings.backgroundSettings.gradient); + } + item.barRenderContext.visualSettings = barVisualSettings; + item.barRenderContext.labelOption = label; + barSeriesOption.renderItem = (params, api) => + renderTimeSeriesBar(params, api, item.barRenderContext); + } + } + seriesOption.data = item.data; + return seriesOption; +}; + +const createChartTextStyle = (font: Font, color: string, darkMode: boolean, colorKey?: string): ComponentStyle => { + const style = textStyle(font); + delete style.lineHeight; + style.fontSize = font.size; + style.color = prepareChartThemeColor(color, darkMode, colorKey); + return style; +}; + +const createLinearOpacityGradient = (color: string, gradient: {start: number; end: number}): LinearGradientObject => ({ + type: 'linear', + x: 0, + y: 0, + x2: 0, + y2: 1, + colorStops: [{ + offset: 0, color: tinycolor(color).setAlpha(gradient.start / 100).toRgbString() // color at 0% + }, { + offset: 1, color: tinycolor(color).setAlpha(gradient.end / 100).toRgbString() // color at 100% + }], + global: false +}); + +const prepareChartThemeColor = (color: string, darkMode: boolean, colorKey?: string): string => { + if (darkMode) { + let colorInstance = tinycolor(color); + if (colorInstance.isDark()) { + if (colorKey && timeSeriesChartColorScheme[colorKey]) { + return timeSeriesChartColorScheme[colorKey].dark; + } else { + const rgb = colorInstance.toRgb(); + colorInstance = tinycolor({r: 255 - rgb.r, g: 255 - rgb.g, b: 255 - rgb.b, a: rgb.a}); + return colorInstance.toRgbString(); + } + } + } + return color; +}; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts new file mode 100644 index 0000000000..48c2dfaf57 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts @@ -0,0 +1,642 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// 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. +/// + +import { WidgetContext } from '@home/models/widget-component.models'; +import { + AxisPosition, + calculateThresholdsOffset, + createTimeSeriesXAxisOption, + createTimeSeriesYAxis, + generateChartData, + TimeSeriesChartDataItem, + timeSeriesChartDefaultSettings, + timeSeriesChartKeyDefaultSettings, + TimeSeriesChartKeySettings, + TimeSeriesChartSeriesType, + TimeSeriesChartSettings, + TimeSeriesChartThresholdItem, + TimeSeriesChartThresholdType, + TimeSeriesChartYAxis, + parseThresholdData, + PointLabelPosition, + updateDarkMode +} from '@home/components/widget/lib/chart/time-series-chart.models'; +import { ResizeObserver } from '@juggle/resize-observer'; +import { + calculateXAxisHeight, + calculateYAxisWidth, + ECharts, + echartsModule, + EChartsOption, + echartsTooltipFormatter, + EChartsTooltipTrigger, + getAxisExtent, + getYAxis, + measureXAxisNameHeight, + measureYAxisNameWidth, + toNamedData +} from '@home/components/widget/lib/chart/echarts-widget.models'; +import { DateFormatProcessor } from '@shared/models/widget-settings.models'; +import { isDefinedAndNotNull, mergeDeep } from '@core/utils'; +import { DataKey, Datasource, DatasourceType, widgetType } from '@shared/models/widget.models'; +import * as echarts from 'echarts/core'; +import { CallbackDataParams } from 'echarts/types/dist/shared'; +import { Renderer2 } from '@angular/core'; +import { CustomSeriesOption, LineSeriesOption } from 'echarts/charts'; +import { BehaviorSubject } from 'rxjs'; +import { AggregationType } from '@shared/models/time/time.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { WidgetSubscriptionOptions } from '@core/api/widget-api.models'; + +export class TbTimeSeriesChart { + + private readonly shapeResize$: ResizeObserver; + + private dataItems: TimeSeriesChartDataItem[] = []; + private thresholdItems: TimeSeriesChartThresholdItem[] = []; + private yAxisList: TimeSeriesChartYAxis[] = []; + + private timeSeriesChart: ECharts; + private timeSeriesChartOptions: EChartsOption; + + private readonly tooltipDateFormat: DateFormatProcessor; + + private yMinSubject = new BehaviorSubject(-1); + private yMaxSubject = new BehaviorSubject(1); + + private darkMode = false; + + private messageChannel = new BroadcastChannel('tbMessageChannel'); + + private topPointLabels = false; + + private componentIndexCounter = 0; + + private highlightedDataKey: DataKey; + + yMin$ = this.yMinSubject.asObservable(); + yMax$ = this.yMaxSubject.asObservable(); + + constructor(private ctx: WidgetContext, + private readonly settings: TimeSeriesChartSettings, + private chartElement: HTMLElement, + private renderer: Renderer2, + private autoResize = true) { + + this.settings = mergeDeep({} as TimeSeriesChartSettings, timeSeriesChartDefaultSettings, this.settings); + this.darkMode = this.settings.darkMode; + this.setupData(); + this.setupThresholds(); + if (this.settings.showTooltip && this.settings.tooltipShowDate) { + this.tooltipDateFormat = DateFormatProcessor.fromSettings(this.ctx.$injector, this.settings.tooltipDateFormat); + } + this.onResize(); + if (this.autoResize) { + this.shapeResize$ = new ResizeObserver(() => { + this.onResize(); + }); + this.shapeResize$.observe(this.chartElement); + } + this.messageChannel.addEventListener('message', (event) => { + if (event?.data?.type === 'tbDarkMode') { + const darkMode = !!event?.data?.darkMode; + this.setDarkMode(darkMode); + } + }); + } + + public update(): void { + for (const item of this.dataItems) { + const datasourceData = this.ctx.data ? this.ctx.data.find(d => d.dataKey === item.dataKey) : null; + item.data = datasourceData?.data ? toNamedData(datasourceData.data) : []; + } + this.onResize(); + if (this.timeSeriesChart) { + this.timeSeriesChartOptions.xAxis[0].min = this.ctx.defaultSubscription.timeWindow.minTime; + this.timeSeriesChartOptions.xAxis[0].max = this.ctx.defaultSubscription.timeWindow.maxTime; + this.timeSeriesChartOptions.xAxis[0].tbTimeWindow = this.ctx.defaultSubscription.timeWindow; + if (this.ctx.defaultSubscription.timeWindowConfig?.aggregation?.type === AggregationType.NONE) { + this.timeSeriesChartOptions.tooltip[0].axisPointer.type = 'line'; + } else { + this.timeSeriesChartOptions.tooltip[0].axisPointer.type = 'shadow'; + } + this.updateSeriesData(true); + if (this.highlightedDataKey) { + this.keyEnter(this.highlightedDataKey); + } + } + } + + public latestUpdated() { + let update = false; + if (this.ctx.latestData) { + for (const item of this.thresholdItems) { + if (item.settings.type === TimeSeriesChartThresholdType.latestKey && item.latestDataKey) { + const data = this.ctx.latestData.find(d => d.dataKey === item.latestDataKey); + if (data.data[0]) { + item.value = parseThresholdData(data.data[0][1]); + update = true; + } + } + } + } + if (this.timeSeriesChart && update) { + this.updateSeriesData(); + } + } + + public keyEnter(dataKey: DataKey): void { + this.highlightedDataKey = dataKey; + const item = this.dataItems.find(d => d.dataKey === dataKey); + if (item) { + this.timeSeriesChart.dispatchAction({ + type: 'highlight', + seriesId: item.id + }); + } + } + + public keyLeave(dataKey: DataKey): void { + this.highlightedDataKey = null; + const item = this.dataItems.find(d => d.dataKey === dataKey); + if (item) { + this.timeSeriesChart.dispatchAction({ + type: 'downplay', + seriesId: item.id + }); + } + } + + public toggleKey(dataKey: DataKey): void { + const enable = dataKey.hidden; + const dataItem = this.dataItems.find(d => d.dataKey === dataKey); + if (dataItem) { + dataItem.enabled = enable; + if (!enable) { + this.timeSeriesChart.dispatchAction({ + type: 'downplay', + seriesId: dataItem.id + }); + } + this.timeSeriesChartOptions.series = this.updateSeries(); + const mergeList = ['series']; + if (this.updateYAxisScale(this.yAxisList)) { + this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option); + mergeList.push('yAxis'); + } + this.timeSeriesChart.setOption(this.timeSeriesChartOptions, this.settings.stack ? {notMerge: true} : {replaceMerge: mergeList}); + this.updateAxes(); + dataKey.hidden = !enable; + if (enable) { + this.timeSeriesChart.dispatchAction({ + type: 'highlight', + seriesId: dataItem.id + }); + } + } + } + + public destroy(): void { + if (this.shapeResize$) { + this.shapeResize$.disconnect(); + } + if (this.timeSeriesChart) { + this.timeSeriesChart.dispose(); + } + this.yMinSubject.complete(); + this.yMaxSubject.complete(); + this.messageChannel.close(); + } + + public resize(): void { + this.onResize(); + } + + public setDarkMode(darkMode: boolean): void { + if (this.darkMode !== darkMode) { + this.darkMode = darkMode; + if (this.timeSeriesChart) { + this.timeSeriesChartOptions = updateDarkMode(this.timeSeriesChartOptions, this.settings, this.dataItems, + this.thresholdItems, darkMode); + this.timeSeriesChart.setOption(this.timeSeriesChartOptions); + } + } + } + + public isDarkMode(): boolean { + return this.darkMode; + } + + private setupData(): void { + if (this.ctx.datasources.length) { + for (const datasource of this.ctx.datasources) { + const dataKeys = datasource.dataKeys; + for (const dataKey of dataKeys) { + const keySettings = mergeDeep({} as TimeSeriesChartKeySettings, + timeSeriesChartKeyDefaultSettings, dataKey.settings); + if (keySettings.showPointLabel && keySettings.pointLabelPosition === PointLabelPosition.top) { + this.topPointLabels = true; + } + dataKey.settings = keySettings; + const datasourceData = this.ctx.data ? this.ctx.data.find(d => d.dataKey === dataKey) : null; + const namedData = datasourceData?.data ? toNamedData(datasourceData.data) : []; + const units = dataKey.units && dataKey.units.length ? dataKey.units : this.ctx.units; + const decimals = isDefinedAndNotNull(dataKey.decimals) ? dataKey.decimals : + (isDefinedAndNotNull(this.ctx.decimals) ? this.ctx.decimals : 2); + this.dataItems.push({ + id: this.nextComponentId(), + units, + decimals, + yAxisIndex: this.getYAxis(units, decimals), + dataKey, + data: namedData, + enabled: !keySettings.dataHiddenByDefault + }); + } + } + } + } + + private setupThresholds(): void { + const thresholdDatasources: Datasource[] = []; + for (const threshold of this.settings.thresholds) { + let latestDataKey: DataKey = null; + let entityDataKey: DataKey = null; + let value = null; + if (threshold.type === TimeSeriesChartThresholdType.latestKey) { + if (this.ctx.datasources.length) { + for (const datasource of this.ctx.datasources) { + latestDataKey = datasource.latestDataKeys?.find(d => + (d.type === DataKeyType.function && d.label === threshold.latestKeyName) || + (d.type !== DataKeyType.function && d.name === threshold.latestKeyName)); + if (latestDataKey) { + break; + } + } + } + if (!latestDataKey) { + continue; + } + } else if (threshold.type === TimeSeriesChartThresholdType.entity) { + const entityAliasId = this.ctx.aliasController.getEntityAliasId(threshold.entityAlias); + if (!entityAliasId) { + continue; + } + let datasource = thresholdDatasources.find(d => d.entityAliasId === entityAliasId); + entityDataKey = { + type: threshold.entityKeyType, + name: threshold.entityKey, + label: threshold.entityKey, + settings: {} + }; + if (datasource) { + datasource.dataKeys.push(entityDataKey); + } + datasource = { + type: DatasourceType.entity, + name: threshold.entityAlias, + aliasName: threshold.entityAlias, + entityAliasId, + dataKeys: [ entityDataKey ] + }; + thresholdDatasources.push(datasource); + } else { // constant + value = threshold.value; + } + const units = threshold.units && threshold.units.length ? threshold.units : this.ctx.units; + const decimals = isDefinedAndNotNull(threshold.decimals) ? threshold.decimals : + (isDefinedAndNotNull(this.ctx.decimals) ? this.ctx.decimals : 2); + const thresholdItem: TimeSeriesChartThresholdItem = { + id: this.nextComponentId(), + units, + decimals, + yAxisIndex: this.getYAxis(units, decimals), + value, + latestDataKey, + settings: threshold + }; + if (entityDataKey) { + entityDataKey.settings.thresholdItemId = thresholdItem.id; + } + this.thresholdItems.push(thresholdItem); + } + this.subscribeForEntityThresholds(thresholdDatasources); + } + + private nextComponentId(): string { + return (this.componentIndexCounter++) + ''; + } + + private getYAxis(units: string, decimals: number): number { + let yAxisIndex = this.yAxisList.findIndex(axis => axis.units === units); + if (yAxisIndex === -1) { + const yAxisId = this.yAxisList.length + ''; + const yAxis = createTimeSeriesYAxis(yAxisId, units, decimals, this.settings.yAxis, this.darkMode); + this.yAxisList.push(yAxis); + yAxisIndex = this.yAxisList.length - 1; + } + return yAxisIndex; + } + + private subscribeForEntityThresholds(datasources: Datasource[]) { + if (datasources.length) { + const thresholdsSourcesSubscriptionOptions: WidgetSubscriptionOptions = { + datasources, + useDashboardTimewindow: false, + type: widgetType.latest, + callbacks: { + onDataUpdated: (subscription) => { + let update = false; + if (subscription.data) { + for (const item of this.thresholdItems) { + if (item.settings.type === TimeSeriesChartThresholdType.entity) { + const data = subscription.data.find(d => d.dataKey.settings?.thresholdItemId === item.id); + if (data.data[0]) { + item.value = parseThresholdData(data.data[0][1]); + update = true; + } + } + } + } + if (this.timeSeriesChart && update) { + this.updateSeriesData(); + } + } + } + }; + this.ctx.subscriptionApi.createSubscription(thresholdsSourcesSubscriptionOptions, true).subscribe(); + } + } + + private drawChart() { + echartsModule.init(); + this.timeSeriesChart = echarts.init(this.chartElement, null, { + renderer: 'canvas' + }); + const noAggregation = this.ctx.defaultSubscription.timeWindowConfig?.aggregation?.type === AggregationType.NONE; + this.timeSeriesChartOptions = { + darkMode: this.darkMode, + backgroundColor: 'transparent', + tooltip: [{ + trigger: this.settings.tooltipTrigger === EChartsTooltipTrigger.axis ? 'axis' : 'item', + confine: true, + appendToBody: true, + axisPointer: { + type: noAggregation ? 'line' : 'shadow' + }, + formatter: (params: CallbackDataParams[]) => + this.settings.showTooltip ? echartsTooltipFormatter(this.renderer, this.tooltipDateFormat, + this.settings, params, 0, '', -1, this.dataItems) : undefined, + padding: [8, 12], + backgroundColor: this.settings.tooltipBackgroundColor, + borderWidth: 0, + extraCssText: `line-height: 1; backdrop-filter: blur(${this.settings.tooltipBackgroundBlur}px);` + }], + grid: [{ + top: this.minTopOffset(), + left: this.settings.dataZoom ? 5 : 0, + right: this.settings.dataZoom ? 5 : 0, + bottom: this.minBottomOffset() + }], + xAxis: [ + createTimeSeriesXAxisOption(this.settings.xAxis, this.ctx.defaultSubscription.timeWindow.minTime, + this.ctx.defaultSubscription.timeWindow.maxTime, this.darkMode) + ], + yAxis: this.yAxisList.map(axis => axis.option), + dataZoom: [ + { + type: 'inside', + disabled: !this.settings.dataZoom, + realtime: true + }, + { + type: 'slider', + show: this.settings.dataZoom, + showDetail: false, + realtime: true, + bottom: 10 + } + ] + }; + + this.timeSeriesChartOptions.xAxis[0].tbTimeWindow = this.ctx.defaultSubscription.timeWindow; + + this.timeSeriesChartOptions.series = this.updateSeries(); + if (this.updateYAxisScale(this.yAxisList)) { + this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option); + } + + this.timeSeriesChart.setOption(this.timeSeriesChartOptions); + this.updateAxes(); + + if (this.settings.dataZoom) { + this.timeSeriesChart.on('datazoom', () => { + this.updateAxes(); + }); + } + } + + private updateSeriesData(updateScale = false): void { + this.timeSeriesChartOptions.series = this.updateSeries(); + if (updateScale && this.updateYAxisScale(this.yAxisList)) { + this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option); + } + this.timeSeriesChart.setOption(this.timeSeriesChartOptions); + this.updateAxes(); + } + + private updateSeries(): Array { + return generateChartData(this.dataItems, this.thresholdItems, + this.ctx.timeWindow.interval, this.settings.stack, this.darkMode); + } + + private updateAxes() { + const leftAxisList = this.yAxisList.filter(axis => axis.option.position === 'left'); + let res = this.updateYAxisOffset(leftAxisList); + let leftOffset = res.offset + (!res.offset && this.settings.dataZoom ? 5 : 0); + let changed = res.changed; + const rightAxisList = this.yAxisList.filter(axis => axis.option.position === 'right'); + res = this.updateYAxisOffset(rightAxisList); + let rightOffset = res.offset + (!res.offset && this.settings.dataZoom ? 5 : 0); + changed = changed || res.changed; + let bottomOffset = this.minBottomOffset(); + const minTopOffset = this.minTopOffset(); + let topOffset = minTopOffset; + if (this.timeSeriesChartOptions.xAxis[0].show) { + const xAxisHeight = calculateXAxisHeight(this.timeSeriesChart); + if (this.timeSeriesChartOptions.xAxis[0].position === AxisPosition.bottom) { + bottomOffset += xAxisHeight; + } else { + topOffset = Math.max(minTopOffset, xAxisHeight); + } + if (this.settings.xAxis.label) { + const nameHeight = measureXAxisNameHeight(this.timeSeriesChart, this.timeSeriesChartOptions.xAxis[0].name); + if (this.timeSeriesChartOptions.xAxis[0].position === AxisPosition.bottom) { + bottomOffset += nameHeight; + } else { + topOffset = Math.max(minTopOffset, xAxisHeight + nameHeight); + } + const nameGap = xAxisHeight; + if (this.timeSeriesChartOptions.xAxis[0].nameGap !== nameGap) { + this.timeSeriesChartOptions.xAxis[0].nameGap = nameGap; + changed = true; + } + } + } + + const thresholdsOffset = calculateThresholdsOffset(this.timeSeriesChart, this.thresholdItems, this.yAxisList); + leftOffset = Math.max(leftOffset, thresholdsOffset[0]); + rightOffset = Math.max(rightOffset, thresholdsOffset[1]); + + if (this.timeSeriesChartOptions.grid[0].left !== leftOffset || + this.timeSeriesChartOptions.grid[0].right !== rightOffset || + this.timeSeriesChartOptions.grid[0].bottom !== bottomOffset || + this.timeSeriesChartOptions.grid[0].top !== topOffset) { + this.timeSeriesChartOptions.grid[0].left = leftOffset; + this.timeSeriesChartOptions.grid[0].right = rightOffset; + this.timeSeriesChartOptions.grid[0].bottom = bottomOffset; + this.timeSeriesChartOptions.grid[0].top = topOffset; + changed = true; + } + if (changed) { + this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option); + this.timeSeriesChart.setOption(this.timeSeriesChartOptions, {replaceMerge: ['yAxis', 'xAxis', 'grid'], lazyUpdate: true}); + } + changed = this.calculateYAxisInterval(this.yAxisList); + if (changed) { + this.timeSeriesChartOptions.yAxis = this.yAxisList.map(axis => axis.option); + this.timeSeriesChart.setOption(this.timeSeriesChartOptions, {replaceMerge: ['yAxis'], lazyUpdate: true}); + } + if (this.yAxisList.length) { + const extent = getAxisExtent(this.timeSeriesChart, this.yAxisList[0].id); + const min = extent[0]; + const max = extent[1]; + if (this.yMinSubject.value !== min) { + this.yMinSubject.next(min); + } + if (this.yMaxSubject.value !== max) { + this.yMaxSubject.next(max); + } + } + } + + private updateYAxisScale(axisList: TimeSeriesChartYAxis[]): boolean { + let changed = false; + for (const yAxis of axisList) { + const scaleYAxis = this.scaleYAxis(yAxis); + if (yAxis.option.scale !== scaleYAxis) { + yAxis.option.scale = scaleYAxis; + changed = true; + } + } + return changed; + } + + private updateYAxisOffset(axisList: TimeSeriesChartYAxis[]): {offset: number; changed: boolean} { + const result = {offset: 0, changed: false}; + let width = 0; + for (const yAxis of axisList) { + const newWidth = calculateYAxisWidth(this.timeSeriesChart, yAxis.id); + if (width && newWidth) { + result.offset += 5; + } + width = newWidth; + const showLine = !!width && this.settings.yAxis.showLine; + if (yAxis.option.axisLine.show !== showLine) { + yAxis.option.axisLine.show = showLine; + result.changed = true; + } + if (yAxis.option.offset !== result.offset) { + yAxis.option.offset = result.offset; + result.changed = true; + } + if (this.settings.yAxis.label) { + if (!width) { + if (yAxis.option.name) { + yAxis.option.name = null; + result.changed = true; + } + } else { + if (!yAxis.option.name) { + yAxis.option.name = this.settings.yAxis.label; + result.changed = true; + } + const nameGap = width; + if (yAxis.option.nameGap !== nameGap) { + yAxis.option.nameGap = nameGap; + result.changed = true; + } + const nameWidth = measureYAxisNameWidth(this.timeSeriesChart, yAxis.id, this.settings.yAxis.label); + result.offset += nameWidth; + } + } + result.offset += width; + } + return result; + } + + private scaleYAxis(yAxis: TimeSeriesChartYAxis): boolean { + const yAxisIndex = this.yAxisList.indexOf(yAxis); + const axisBarDataItems = this.dataItems.filter(d => d.yAxisIndex === yAxisIndex && d.enabled && + d.data.length && d.dataKey.settings.type === TimeSeriesChartSeriesType.bar); + return !axisBarDataItems.length; + } + + private calculateYAxisInterval(axisList: TimeSeriesChartYAxis[]): boolean { + let changed = false; + for (const yAxis of axisList) { + if (yAxis.intervalCalculator) { + const axis = getYAxis(this.timeSeriesChart, yAxis.id); + if (axis) { + try { + const interval = yAxis.intervalCalculator(axis); + if ((yAxis.option as any).interval !== interval) { + (yAxis.option as any).interval = interval; + changed = true; + } + } catch (_e) {} + } + } + } + return changed; + } + + private minTopOffset(): number { + return (this.topPointLabels) ? 20 : + ((this.settings.yAxis.show && this.settings.yAxis.showTickLabels) ? 10 : 5); + } + + private minBottomOffset(): number { + return this.settings.dataZoom ? 45 : 5; + } + + private onResize() { + const shapeWidth = this.chartElement.offsetWidth; + const shapeHeight = this.chartElement.offsetHeight; + if (shapeWidth && shapeHeight) { + if (!this.timeSeriesChart) { + this.drawChart(); + } else { + const width = this.timeSeriesChart.getWidth(); + const height = this.timeSeriesChart.getHeight(); + if (width !== shapeWidth || height !== shapeHeight) { + this.timeSeriesChart.resize(); + } + } + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index 03bd97dd41..0ae45d1eff 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -76,6 +76,7 @@ import { CommandButtonWidgetComponent } from '@home/components/widget/lib/button import { PowerButtonWidgetComponent } from '@home/components/widget/lib/rpc/power-button-widget.component'; import { SliderWidgetComponent } from '@home/components/widget/lib/rpc/slider-widget.component'; import { ToggleButtonWidgetComponent } from '@home/components/widget/lib/button/toggle-button-widget.component'; +import { TimeSeriesChartWidgetComponent } from '@home/components/widget/lib/chart/time-series-chart-widget.component'; @NgModule({ declarations: @@ -124,7 +125,8 @@ import { ToggleButtonWidgetComponent } from '@home/components/widget/lib/button/ CommandButtonWidgetComponent, PowerButtonWidgetComponent, SliderWidgetComponent, - ToggleButtonWidgetComponent + ToggleButtonWidgetComponent, + TimeSeriesChartWidgetComponent ], imports: [ CommonModule, @@ -177,7 +179,8 @@ import { ToggleButtonWidgetComponent } from '@home/components/widget/lib/button/ CommandButtonWidgetComponent, PowerButtonWidgetComponent, SliderWidgetComponent, - ToggleButtonWidgetComponent + ToggleButtonWidgetComponent, + TimeSeriesChartWidgetComponent ], providers: [ {provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule } diff --git a/ui-ngx/src/app/shared/models/color.models.ts b/ui-ngx/src/app/shared/models/color.models.ts new file mode 100644 index 0000000000..7c1d08baf8 --- /dev/null +++ b/ui-ngx/src/app/shared/models/color.models.ts @@ -0,0 +1,24 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// 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. +/// + +export interface TbColor { + light: string; + dark: string; +} + +export interface TbColorScheme { + [key: string]: TbColor; +} diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 288db8c9d2..96d8ddebf7 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3169,6 +3169,11 @@ "avg": "avg", "total": "total", "latest": "latest", + "Min": "Min", + "Max": "Max", + "Avg": "Avg", + "Total": "Total", + "Latest": "Latest", "comparison-time-ago": { "previousInterval": "(previous interval)", "customInterval": "(custom interval)",