├── .gitattributes ├── .gitignore ├── .streamlit └── config.toml ├── Dockerfile ├── LICENSE.md ├── README.md ├── app.py ├── app ├── __init__.py ├── build.py ├── forms.py ├── plot.py └── utils.py ├── examples ├── addis-ababa-ethiopia-temperature-mean-2022-ref-1961-1990.png ├── atlantic-ocean-temperature-mean-2023-ref-1961-1990.png ├── bad-neuenahr-ahrweiler-germany-precipitation-rolling-2021-ref-1961-1990.png ├── beijing-china-temperature-mean-2022-ref-1961-1990.png ├── berlin-germany-temperature-mean-2023-ref-1961-1990.png ├── buenos-aires-argentina-temperature-mean-2022-ref-1961-1990.png ├── cape-town-south-africa-temperature-mean-2022-ref-1961-1990.png ├── caracas-venezuela-precipitation-cum-2014-ref-1961-1990.png ├── caracas-venezuela-temperature-mean-2022-ref-1961-1990.png ├── duisburg-germany-temperature-max-2019-ref-1961-1990.png ├── hawaii-county-united-states-temperature-mean-2022-ref-1961-1990.png ├── helsinki-finland-temperature-mean-2022-ref-1961-1990.png ├── kampala-uganda-temperature-mean-2022-ref-1961-1990.png ├── key-west-united-states-temperature-max-2023-ref-1961-1990.png ├── kyiv-ukraine-temperature-mean-2022-ref-1961-1990.png ├── madrid-spain-temperature-mean-2023-ref-1961-1990.png ├── monterrey-mexico-temperature-mean-2023-ref-1961-1990.png ├── mumbai-india-precipitation-cum-2022-ref-1961-1990.png ├── mumbai-india-precipitation-rolling-2021-ref-1961-1990.png ├── mumbai-india-temperature-mean-2022-ref-1961-1990.png ├── pacific-ocean-temperature-mean-2023-ref-1961-1990.png ├── prague-czechia-precipitation-rolling-2022-ref-1961-1990.png ├── riyadh-saudi-arabia-temperature-mean-2022-ref-1961-1990.png ├── rome-italy-temperature-max-2023-ref-1961-1990.png ├── san-francisco-united-states-temperature-mean-2022-ref-1961-1990.png ├── seville-spain-precipitation-cum-2023-ref-1961-1990.png └── sydney-australia-temperature-mean-2022-ref-1961-1990.png ├── header.png ├── meteo_hist ├── __init__.py ├── base.py ├── exceptions.py └── interactive.py ├── notebooks └── meteo_hist_example.ipynb ├── requirements.txt ├── sample_data ├── raw │ ├── precipitation_cum-caracas-venezuela.csv │ ├── precipitation_cum-oaxaca-city-mexico.csv │ ├── temperature_max-caracas-venezuela.csv │ ├── temperature_max-oaxaca-city-mexico.csv │ ├── temperature_mean-addis-ababa-ethiopia.csv │ └── temperature_mean-berlin-germany.csv └── transformed │ ├── precipitation_cum-caracas-venezuela_transformed.csv │ ├── precipitation_cum-oaxaca-city-mexico_transformed.csv │ ├── temperature_max-caracas-venezuela_transformed.csv │ ├── temperature_max-oaxaca-city-mexico_transformed.csv │ ├── temperature_mean-addis-ababa-ethiopia_transformed.csv │ └── temperature_mean-berlin-germany_transformed.csv ├── style.css └── tests ├── __init__.py └── test_base.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Tell linguist to count Jupyter Notebooks as Python 2 | *.ipynb linguist-language=Python -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | .directory 163 | tmp/ 164 | output/*.png 165 | .vscode/ 166 | -------------------------------------------------------------------------------- /.streamlit/config.toml: -------------------------------------------------------------------------------- 1 | [global] 2 | 3 | # By default, Streamlit checks if the Python watchdog module is available and, if not, prints a warning asking for you to install it. The watchdog module is not required, but highly recommended. It improves Streamlit's ability to detect changes to files in your filesystem. 4 | # If you'd like to turn off this warning, set this to True. 5 | # Default: false 6 | # disableWatchdogWarning = false 7 | 8 | # By default, Streamlit displays a warning when a user sets both a widget default value in the function defining the widget and a widget value via the widget's key in `st.session_state`. 9 | # If you'd like to turn off this warning, set this to True. 10 | # Default: false 11 | # disableWidgetStateDuplicationWarning = false 12 | 13 | # If True, will show a warning when you run a Streamlit-enabled script via "python my_script.py". 14 | # Default: true 15 | # showWarningOnDirectExecution = true 16 | 17 | # DataFrame serialization. 18 | # Acceptable values: - 'legacy': Serialize DataFrames using Streamlit's custom format. Slow but battle-tested. - 'arrow': Serialize DataFrames using Apache Arrow. Much faster and versatile. 19 | # Default: "arrow" 20 | # dataFrameSerialization = "arrow" 21 | 22 | 23 | [logger] 24 | 25 | # Level of logging: 'error', 'warning', 'info', or 'debug'. 26 | # Default: 'info' 27 | # level = "info" 28 | 29 | # String format for logging messages. If logger.datetimeFormat is set, logger messages will default to `%(asctime)s.%(msecs)03d %(message)s`. See [Python's documentation](https://docs.python.org/2.6/library/logging.html#formatter-objects) for available attributes. 30 | # Default: "%(asctime)s %(message)s" 31 | # messageFormat = "%(asctime)s %(message)s" 32 | 33 | 34 | [client] 35 | 36 | # Whether to enable st.cache. 37 | # Default: true 38 | caching = true 39 | 40 | # If false, makes your Streamlit script not draw to a Streamlit app. 41 | # Default: true 42 | # displayEnabled = true 43 | 44 | # Controls whether uncaught app exceptions and deprecation warnings are displayed in the browser. By default, this is set to True and Streamlit displays app exceptions and associated tracebacks, and deprecation warnings, in the browser. 45 | # If set to False, an exception or deprecation warning will result in a generic message being shown in the browser, and exceptions, tracebacks, and deprecation warnings will be printed to the console only. 46 | # Default: true 47 | # showErrorDetails = true 48 | 49 | # Change the visibility of items in the toolbar, options menu, and settings dialog (top right of the app). 50 | # Allowed values: * "auto" : Show the developer options if the app is accessed through localhost and hide them otherwise. * "developer" : Show the developer options. * "viewer" : Hide the developer options. * "minimal" : Show only options set externally (e.g. through Streamlit Community Cloud) or through st.set_page_config. If there are no options left, hide the menu. 51 | # Default: "auto" 52 | toolbarMode = "minimal" 53 | 54 | 55 | [runner] 56 | 57 | # Allows you to type a variable or string by itself in a single line of Python code to write it to the app. 58 | # Default: true 59 | # magicEnabled = true 60 | 61 | # Install a Python tracer to allow you to stop or pause your script at any point and introspect it. As a side-effect, this slows down your script's execution. 62 | # Default: false 63 | # installTracer = false 64 | 65 | # Sets the MPLBACKEND environment variable to Agg inside Streamlit to prevent Python crashing. 66 | # Default: true 67 | # fixMatplotlib = true 68 | 69 | # Run the Python Garbage Collector after each script execution. This can help avoid excess memory use in Streamlit apps, but could introduce delay in rerunning the app script for high-memory-use applications. 70 | # Default: true 71 | # postScriptGC = true 72 | 73 | # Handle script rerun requests immediately, rather than waiting for script execution to reach a yield point. This makes Streamlit much more responsive to user interaction, but it can lead to race conditions in apps that mutate session_state data outside of explicit session_state assignment statements. 74 | # Default: true 75 | # fastReruns = true 76 | 77 | # Raise an exception after adding unserializable data to Session State. Some execution environments may require serializing all data in Session State, so it may be useful to detect incompatibility during development, or when the execution environment will stop supporting it in the future. 78 | # Default: false 79 | # enforceSerializableSessionState = false 80 | 81 | 82 | [server] 83 | 84 | # List of folders that should not be watched for changes. This impacts both "Run on Save" and @st.cache. 85 | # Relative paths will be taken as relative to the current working directory. 86 | # Example: ['/home/user1/env', 'relative/path/to/folder'] 87 | # Default: [] 88 | # folderWatchBlacklist = [] 89 | 90 | # Change the type of file watcher used by Streamlit, or turn it off completely. 91 | # Allowed values: * "auto" : Streamlit will attempt to use the watchdog module, and falls back to polling if watchdog is not available. * "watchdog" : Force Streamlit to use the watchdog module. * "poll" : Force Streamlit to always use polling. * "none" : Streamlit will not watch files. 92 | # Default: "auto" 93 | # fileWatcherType = "auto" 94 | 95 | # Symmetric key used to produce signed cookies. If deploying on multiple replicas, this should be set to the same value across all replicas to ensure they all share the same secret. 96 | # Default: randomly generated secret key. 97 | # cookieSecret = "a753f1b3e04f77d51197fb1a9183909473f430ddbb69dfffe68b4cf24f44701b" 98 | 99 | # If false, will attempt to open a browser window on start. 100 | # Default: false unless (1) we are on a Linux box where DISPLAY is unset, or (2) we are running in the Streamlit Atom plugin. 101 | # headless = false 102 | 103 | # Automatically rerun script when the file is modified on disk. 104 | # Default: false 105 | # runOnSave = false 106 | 107 | # The address where the server will listen for client and browser connections. Use this if you want to bind the server to a specific address. If set, the server will only be accessible from this address, and not from any aliases (like localhost). 108 | # Default: (unset) 109 | # address = 110 | 111 | # The port where the server will listen for browser connections. 112 | # Default: 8501 113 | # port = 8501 114 | 115 | # The base path for the URL where Streamlit should be served from. 116 | # Default: "" 117 | # baseUrlPath = "" 118 | 119 | # Enables support for Cross-Origin Resource Sharing (CORS) protection, for added security. 120 | # Due to conflicts between CORS and XSRF, if `server.enableXsrfProtection` is on and `server.enableCORS` is off at the same time, we will prioritize `server.enableXsrfProtection`. 121 | # Default: true 122 | # enableCORS = true 123 | 124 | # Enables support for Cross-Site Request Forgery (XSRF) protection, for added security. 125 | # Due to conflicts between CORS and XSRF, if `server.enableXsrfProtection` is on and `server.enableCORS` is off at the same time, we will prioritize `server.enableXsrfProtection`. 126 | # Default: true 127 | # enableXsrfProtection = true 128 | 129 | # Max size, in megabytes, for files uploaded with the file_uploader. 130 | # Default: 200 131 | # maxUploadSize = 200 132 | 133 | # Max size, in megabytes, of messages that can be sent via the WebSocket connection. 134 | # Default: 200 135 | # maxMessageSize = 200 136 | 137 | # Enables support for websocket compression. 138 | # Default: false 139 | # enableWebsocketCompression = false 140 | 141 | # Enable serving files from a `static` directory in the running app's directory. 142 | # Default: false 143 | # enableStaticServing = false 144 | 145 | # Server certificate file for connecting via HTTPS. Must be set at the same time as "server.sslKeyFile". 146 | # ['DO NOT USE THIS OPTION IN A PRODUCTION ENVIRONMENT. It has not gone through security audits or performance tests. For the production environment, we recommend performing SSL termination by the load balancer or the reverse proxy.'] 147 | # sslCertFile = 148 | 149 | # Cryptographic key file for connecting via HTTPS. Must be set at the same time as "server.sslCertFile". 150 | # ['DO NOT USE THIS OPTION IN A PRODUCTION ENVIRONMENT. It has not gone through security audits or performance tests. For the production environment, we recommend performing SSL termination by the load balancer or the reverse proxy.'] 151 | # sslKeyFile = 152 | 153 | 154 | [browser] 155 | 156 | # Internet address where users should point their browsers in order to connect to the app. Can be IP address or DNS name and path. 157 | # This is used to: - Set the correct URL for CORS and XSRF protection purposes. - Show the URL on the terminal - Open the browser 158 | # Default: "localhost" 159 | # serverAddress = "localhost" 160 | 161 | # Whether to send usage statistics to Streamlit. 162 | # Default: true 163 | gatherUsageStats = false 164 | 165 | # Port where users should point their browsers in order to connect to the app. 166 | # This is used to: - Set the correct URL for CORS and XSRF protection purposes. - Show the URL on the terminal - Open the browser 167 | # Default: whatever value is set in server.port. 168 | # serverPort = 8501 169 | 170 | 171 | [mapbox] 172 | 173 | # Configure Streamlit to use a custom Mapbox token for elements like st.pydeck_chart and st.map. To get a token for yourself, create an account at https://mapbox.com. It's free (for moderate usage levels)! 174 | # Default: "" 175 | # token = "" 176 | 177 | 178 | [deprecation] 179 | 180 | # Set to false to disable the deprecation warning for the file uploader encoding. 181 | # Default: true 182 | # showfileUploaderEncoding = true 183 | 184 | # Set to false to disable the deprecation warning for using the global pyplot instance. 185 | # Default: true 186 | # showPyplotGlobalUse = true 187 | 188 | 189 | [theme] 190 | 191 | # The preset Streamlit theme that your custom theme inherits from. One of "light" or "dark". 192 | base = "light" 193 | 194 | # Primary accent color for interactive elements. 195 | # primaryColor = 196 | 197 | # Background color for the main content area. 198 | # backgroundColor = 199 | 200 | # Background color used for the sidebar and most interactive widgets. 201 | # secondaryBackgroundColor = 202 | 203 | # Color used for almost all text. 204 | # textColor = 205 | 206 | # Font family for all text in the app, except code blocks. One of "sans serif", "serif", or "monospace". 207 | # font = 208 | 209 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12-slim 2 | 3 | WORKDIR /app 4 | 5 | # Install dependencies 6 | RUN apt-get update && \ 7 | apt-get install -y \ 8 | wget \ 9 | unzip \ 10 | curl \ 11 | fontconfig && \ 12 | apt-get clean && \ 13 | rm -rf /var/lib/apt/lists/* 14 | 15 | # Install Lato font 16 | RUN mkdir -p /usr/share/fonts/truetype/lato && \ 17 | curl -L -o /usr/share/fonts/truetype/lato/Lato-Bold.ttf "https://github.com/google/fonts/raw/main/ofl/lato/Lato-Bold.ttf" && \ 18 | curl -L -o /usr/share/fonts/truetype/lato/Lato-BoldItalic.ttf "https://github.com/google/fonts/raw/main/ofl/lato/Lato-BoldItalic.ttf" && \ 19 | curl -L -o /usr/share/fonts/truetype/lato/Lato-Italic.ttf "https://github.com/google/fonts/raw/main/ofl/lato/Lato-Italic.ttf" && \ 20 | curl -L -o /usr/share/fonts/truetype/lato/Lato-Regular.ttf "https://github.com/google/fonts/raw/main/ofl/lato/Lato-Regular.ttf" && \ 21 | fc-cache -f -v 22 | 23 | # Install Python dependencies 24 | COPY requirements.txt . 25 | RUN pip install -r requirements.txt 26 | 27 | # Copy app files 28 | COPY *.py . 29 | COPY *.css . 30 | COPY .streamlit/config.toml .streamlit/config.toml 31 | COPY ./app/ app/ 32 | COPY ./meteo_hist/ meteo_hist/ 33 | COPY ./examples/ examples/ 34 | 35 | # Create log directory 36 | RUN mkdir -p /app/logs && chmod 777 /app/logs 37 | 38 | EXPOSE 8501 39 | 40 | HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health 41 | 42 | ENTRYPOINT ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"] -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International 2 | 3 | Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. 4 | 5 | **Using Creative Commons Public Licenses** 6 | 7 | Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. 8 | 9 | * __Considerations for licensors:__ Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. [More considerations for licensors](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensors). 10 | 11 | * __Considerations for the public:__ By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. [More considerations for the public](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensees). 12 | 13 | ## Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License 14 | 15 | By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. 16 | 17 | ### Section 1 – Definitions. 18 | 19 | a. __Adapted Material__ means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. 20 | 21 | b. __Adapter's License__ means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. 22 | 23 | c. __BY-NC-SA Compatible License__ means a license listed at [creativecommons.org/compatiblelicenses](http://creativecommons.org/compatiblelicenses), approved by Creative Commons as essentially the equivalent of this Public License. 24 | 25 | d. __Copyright and Similar Rights__ means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. 26 | 27 | e. __Effective Technological Measures__ means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. 28 | 29 | f. __Exceptions and Limitations__ means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. 30 | 31 | g. __License Elements__ means the license attributes listed in the name of a Creative Commons Public License. The License Elements of this Public License are Attribution, NonCommercial, and ShareAlike. 32 | 33 | h. __Licensed Material__ means the artistic or literary work, database, or other material to which the Licensor applied this Public License. 34 | 35 | i. __Licensed Rights__ means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. 36 | 37 | j. __Licensor__ means the individual(s) or entity(ies) granting rights under this Public License. 38 | 39 | k. __NonCommercial__ means not primarily intended for or directed towards commercial advantage or monetary compensation. For purposes of this Public License, the exchange of the Licensed Material for other material subject to Copyright and Similar Rights by digital file-sharing or similar means is NonCommercial provided there is no payment of monetary compensation in connection with the exchange. 40 | 41 | l. __Share__ means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. 42 | 43 | m. __Sui Generis Database Rights__ means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. 44 | 45 | n. __You__ means the individual or entity exercising the Licensed Rights under this Public License. __Your__ has a corresponding meaning. 46 | 47 | ### Section 2 – Scope. 48 | 49 | a. ___License grant.___ 50 | 51 | 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: 52 | 53 | A. reproduce and Share the Licensed Material, in whole or in part, for NonCommercial purposes only; and 54 | 55 | B. produce, reproduce, and Share Adapted Material for NonCommercial purposes only. 56 | 57 | 2. __Exceptions and Limitations.__ For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. 58 | 59 | 3. __Term.__ The term of this Public License is specified in Section 6(a). 60 | 61 | 4. __Media and formats; technical modifications allowed.__ The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. 62 | 63 | 5. __Downstream recipients.__ 64 | 65 | A. __Offer from the Licensor – Licensed Material.__ Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. 66 | 67 | B. __Additional offer from the Licensor – Adapted Material.__ Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapter’s License You apply. 68 | 69 | C. __No downstream restrictions.__ You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. 70 | 71 | 6. __No endorsement.__ Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). 72 | 73 | b. ___Other rights.___ 74 | 75 | 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. 76 | 77 | 2. Patent and trademark rights are not licensed under this Public License. 78 | 79 | 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties, including when the Licensed Material is used other than for NonCommercial purposes. 80 | 81 | ### Section 3 – License Conditions. 82 | 83 | Your exercise of the Licensed Rights is expressly made subject to the following conditions. 84 | 85 | a. ___Attribution.___ 86 | 87 | 1. If You Share the Licensed Material (including in modified form), You must: 88 | 89 | A. retain the following if it is supplied by the Licensor with the Licensed Material: 90 | 91 | i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); 92 | 93 | ii. a copyright notice; 94 | 95 | iii. a notice that refers to this Public License; 96 | 97 | iv. a notice that refers to the disclaimer of warranties; 98 | 99 | v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; 100 | 101 | B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and 102 | 103 | C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. 104 | 105 | 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. 106 | 107 | 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. 108 | 109 | b. ___ShareAlike.___ 110 | 111 | In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply. 112 | 113 | 1. The Adapter’s License You apply must be a Creative Commons license with the same License Elements, this version or later, or a BY-NC-SA Compatible License. 114 | 115 | 2. You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material. 116 | 117 | 3. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply. 118 | 119 | ### Section 4 – Sui Generis Database Rights. 120 | 121 | Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: 122 | 123 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database for NonCommercial purposes only; 124 | 125 | b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material, including for purposes of Section 3(b); and 126 | 127 | c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. 128 | 129 | For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. 130 | 131 | ### Section 5 – Disclaimer of Warranties and Limitation of Liability. 132 | 133 | a. __Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.__ 134 | 135 | b. __To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.__ 136 | 137 | c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. 138 | 139 | ### Section 6 – Term and Termination. 140 | 141 | a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. 142 | 143 | b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: 144 | 145 | 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or 146 | 147 | 2. upon express reinstatement by the Licensor. 148 | 149 | For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. 150 | 151 | c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. 152 | 153 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. 154 | 155 | ### Section 7 – Other Terms and Conditions. 156 | 157 | a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. 158 | 159 | b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. 160 | 161 | ### Section 8 – Interpretation. 162 | 163 | a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. 164 | 165 | b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. 166 | 167 | c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. 168 | 169 | d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. 170 | 171 | > Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at [creativecommons.org/policies](http://creativecommons.org/policies), Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. 172 | > 173 | > Creative Commons may be contacted at creativecommons.org 174 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![social-media-image](https://github.com/yotkadata/meteo_hist/assets/7913590/0d4dc378-a6be-4d61-bec8-a664d729a4e2) 2 | 3 | # MeteoHist - Historical Meteo Graphs 4 | 5 | ## A Streamlit app to create interactive temperature and precipitation graphs for places around the world. 6 | 7 | This app allows to create temperature and precipitation (rain, showers, and snowfall) graphs that compare the values of a given location in a given year to the values of a **reference period** at the same place. 8 | 9 | The reference period **defaults to 1961-1990** which [according](https://public.wmo.int/en/media/news/it%E2%80%99s-warmer-average-what-average) to the World Meteorological Organization (WMO) is currently the **best "long-term climate change assessment"**. Other reference periods of 30 years each can be selected, too. 10 | 11 | The **peaks** on the graph show how the displayed year's values deviate from the mean of the reference period. For temperature graphs, this means that the more and the higher the red peaks, the more "hotter days than usual" have been observed. The blue peaks indicate days colder than the historical mean. Precipitation graphs show blue peaks on top which means "more precipitation than normal" and in red "less than normal". 12 | 13 | The interactive plot is created using Python's **Plotly** library. In a first version with static images, **Matplotlib** came to use. 14 | 15 | By default, mean values of the reference period are **smoothed** using [Locally Weighted Scatterplot Smoothing (LOWESS)](https://www.statsmodels.org/devel/generated/statsmodels.nonparametric.smoothers_lowess.lowess.html). The value can be adjusted under "advanced settings" in the app. 16 | 17 | ### Interactive version 18 | 19 | In the latest version (first published on 17 August 2023), the graphs are displayed interactively on larger screens. That means you can hover over the graph and get the exact values displayed for every day. You can also zoom in to see parts of the plot. 20 | 21 | ### Data 22 | 23 | To create the graph, data from the open-source weather API [**Open-Meteo**](https://open-meteo.com/en/docs/historical-weather-api) is used. According to them, "the Historical Weather API is based on **reanalysis datasets** and uses a **combination of weather station, aircraft, buoy, radar, and satellite observations** to create a comprehensive record of past weather conditions. These datasets are able to **fill in gaps by using mathematical models** to estimate the values of various weather variables. As a result, reanalysis datasets are able to provide detailed historical weather information for **locations that may not have had weather stations nearby**, such as rural areas or the open ocean." 24 | 25 | The **Reanalysis Models** are based on [ERA5](https://cds.climate.copernicus.eu/cdsapp#!/dataset/reanalysis-era5-single-levels?tab=overview), [ERA5-Land](https://cds.climate.copernicus.eu/cdsapp#!/dataset/reanalysis-era5-land?tab=overview), and [CERRA](https://cds.climate.copernicus.eu/cdsapp#!/dataset/reanalysis-cerra-single-levels?tab=overview) from the [**European Union's Copernicus Programme**](https://www.copernicus.eu/en). 26 | 27 | To get location data (lat/lon) for the input location, [**Openstreetmap's Nominatim**](https://nominatim.openstreetmap.org/) is used. 28 | 29 | ### Metrics 30 | 31 | Available metrics are: 32 | 33 | - **Mean Temperature:** Mean daily air temperature at 2 meters above ground (24 hour aggregation from hourly values) 34 | - **Minimum Temperature:** Minimum daily air temperature at 2 meters above ground (24 hour aggregation from hourly values) 35 | - **Maximum Temperature:** Maximum daily air temperature at 2 meters above ground (24 hour aggregation from hourly values) 36 | - **Precipitation (Rolling Average):** 30-day rolling/moving average of the sum of daily precipitation (including rain, showers and snowfall) 37 | - **Precipitation (Cumulated):** Cumulated sum of daily precipitation (including rain, showers, and snowfall) 38 | 39 | ### Settings 40 | 41 | - **Location to display:** Name of the location you want to display. A search at Openstreetmap's Nominatim will be performed to find the location and get latitude and longitude. 42 | - **Year to show:** Year to be compared to reference period. 43 | - **Reference period:** The reference period is used to calculate the historical average of the daily values. The average is then used to compare the daily values of the selected year. 1961-1990 (default) is currently considered the best "long-term climate change assessment" by the World Meteorological Organization (WMO). 44 | - **Peaks to be annotated:** Number of maximum and minimum peaks to be annotated (default: 1). If peaks are too close together, the next highest/lowest peak is selected to avoid overlapping. 45 | - **Unit system:** Whether to use Metric System (°C, mm - default) or Imperial System (°F, In). 46 | - **Smoothing:** Degree of smoothing to apply to the historical data. 0 means no smoothing. The higher the value, the more smoothing is applied. Smoothing is done using LOWESS (Locally Weighted Scatterplot Smoothing). 47 | - **Peak method:** Method to determine the peaks. Either the difference to the historical mean (default) or the difference to the 05/95 percentiles. The percentile method focuses more on extreme events, while the mean method focuses more on the difference to the historical average. 48 | - **Emphasize peaks:** If checked, peaks that leave the gray area between the 5 and 95 percentiles will be highlighted more. 49 | 50 | ### Examples 51 | 52 |

53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 |

72 | 73 | ### License 74 | 75 | The app and the plots it produces are published under a [**Creative Commons license (CC by-sa-nc 4.0)**](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en). 76 | 77 | ### Try it 78 | 79 | You can try the app at [https://yotka.org/meteo-hist/](https://yotka.org/meteo-hist/) 80 | 81 | To use the app on your machine, there are two simple ways: 82 | 83 | **1. Set up a Python environment, clone the repository, and run app.py using streamlit:** 84 | 85 | ```bash 86 | git clone https://github.com/yotkadata/meteo_hist/ 87 | cd meteo_hist/ 88 | pip install -r requirements.txt 89 | streamlit run app.py 90 | ``` 91 | 92 | This should open a page in your default browser at http://localhost:8501 that shows the app. 93 | 94 | **2. Set up Docker and run it in a container (you can change the name and the tag, of course):** 95 | 96 | ```bash 97 | docker build -t meteo_hist:latest github.com/yotkadata/meteo_hist 98 | docker run -d --name meteo_hist -p 8501:8501 meteo_hist:latest 99 | ``` 100 | 101 | Then open http://localhost:8501 or http://0.0.0.0:8501/ in your browser to see the app. 102 | 103 | To save the generated files outside the Docker container, you can add a binding to a folder on your hard drive when you start the container: 104 | (replace `/home/user/path/output/` with the path to the folder to be used). 105 | 106 | ```bash 107 | docker run -d --name meteo_hist -p 8501:8501 -v /home/user/path/output/:/app/output meteo_hist:latest 108 | ``` 109 | 110 | ### User Query Tracking 111 | 112 | The app includes an anonymous query tracking feature that logs information about how the app is used. This helps improve the application by understanding which locations, metrics, and settings are most valuable to users. 113 | 114 | #### What is tracked: 115 | 116 | - Timestamp of the query 117 | - Location coordinates and resolved location name 118 | - Selected metric (temperature/precipitation type) 119 | - Year and reference period being viewed 120 | - Selected visualization settings 121 | 122 | No personal data or identifying information is collected. 123 | 124 | #### Log file location: 125 | 126 | - When running locally: Logs are stored in a `logs` directory in the project root 127 | - In Docker: Logs are stored in `/app/logs` inside the container 128 | 129 | #### Accessing logs from Docker: 130 | 131 | Mount a volume to access logs from your Docker container: 132 | 133 | ```bash 134 | docker run -d --name meteo_hist -p 8501:8501 \ 135 | -v /home/user/path/output/:/app/output \ 136 | -v /home/user/path/logs/:/app/logs \ 137 | meteo_hist:latest 138 | ``` 139 | 140 | #### Log format: 141 | 142 | Logs are stored as JSON entries in a `user_queries.log` file for easy parsing and analysis: 143 | 144 | ```json 145 | { 146 | "timestamp": "2025-05-04T20:25:23.808898", 147 | "location": "Caracas, Venezuela", 148 | "coords": [10.506093, -66.914601], 149 | "metric": "precipitation_cum", 150 | "year": 2025, 151 | "reference_period": [1961, 1990], 152 | "settings": { 153 | "highlight_max": 3, 154 | "highlight_min": 2, 155 | "peak_alpha": false, 156 | "peak_method": "percentile", 157 | "peak_distance": 10, 158 | "smooth": { "apply": false, "frac": 0.083 }, 159 | "system": "imperial" 160 | } 161 | } 162 | ``` 163 | 164 | #### Configuration: 165 | 166 | You can customize the log directory by setting the `LOG_DIR` environment variable: 167 | 168 | ```bash 169 | LOG_DIR=/custom/path streamlit run app.py 170 | ``` 171 | 172 | Or in Docker: 173 | 174 | ```bash 175 | docker run -d --name meteo_hist -p 8501:8501 -e LOG_DIR=/custom/logs meteo_hist:latest 176 | ``` 177 | 178 | ### Using the class without the web interface 179 | 180 | It is also possible to use the Python class directly, without the web app. See the `notebooks` directory for examples. 181 | 182 | ### Using Open-Meteo API keys 183 | 184 | The Open-Meteo API doesn't require an API key, but limits the number of API calls without one. To use an API key provided by Open-Meteo, simply add the `OPEN_METEO_API_KEY` variable to a file called `.env` in the base directory. Example (replace [my_api_key] with your key): 185 | 186 | ``` 187 | OPEN_METEO_API_KEY=[my_api_key] 188 | ``` 189 | 190 | ### Thanks 191 | 192 | - This app was inspired by [plots](https://twitter.com/dr_xeo/status/1656933695511511043) made by [Dr. Dominic Royé](https://github.com/dominicroye) - thanks for the idea and the exchange about it. 193 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | """ 2 | Streamlit app. 3 | """ 4 | 5 | import time 6 | 7 | import streamlit as st 8 | from streamlit_js_eval import streamlit_js_eval 9 | 10 | from app import build_content, build_menu, get_form_defaults 11 | 12 | 13 | def main() -> None: 14 | """ 15 | Main function. 16 | """ 17 | # Set page title 18 | st.set_page_config(page_title="Historical Meteo Graphs", layout="wide") 19 | 20 | # Get screen width using window.innerWidth because screen.width 21 | # does not react to window size changes 22 | st.session_state["screen_width"] = streamlit_js_eval( 23 | js_expressions="window.innerWidth", key="SCR" 24 | ) 25 | 26 | # Wait until screen width is set 27 | while ( 28 | "screen_width" not in st.session_state 29 | or st.session_state["screen_width"] is None 30 | ): 31 | time.sleep(0.1) 32 | 33 | # Include custom CSS 34 | with open("style.css", encoding="utf-8") as css: 35 | st.markdown(f"", unsafe_allow_html=True) 36 | 37 | # Define default values for the form 38 | if "form_defaults" not in st.session_state: 39 | st.session_state["form_defaults"] = get_form_defaults() 40 | 41 | # Set base URL 42 | if "base_url" not in st.session_state: 43 | st.session_state["base_url"] = "https://yotka.org/meteo-hist/" 44 | 45 | # If screen size is large enough, use two columns 46 | if st.session_state["screen_width"] > 1200: 47 | col1, col2 = st.columns([1, 3]) 48 | else: 49 | col1, col2 = st.container(), st.container() 50 | 51 | with col1: 52 | # Set page title 53 | st.markdown( 54 | "

Historical Meteo Graphs

", 55 | unsafe_allow_html=True, 56 | ) 57 | 58 | # Create a placeholder for messages 59 | message_box = st.empty() 60 | build_menu() 61 | 62 | with col2: 63 | plot_placeholder = st.empty() 64 | build_content(plot_placeholder, message_box) 65 | 66 | 67 | if __name__ == "__main__": 68 | main() 69 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Streamlit app utility functions 3 | """ 4 | from .utils import ( 5 | build_location_by_coords, 6 | build_location_by_name, 7 | create_share_url, 8 | get_base_url, 9 | get_form_defaults, 10 | get_query_params, 11 | ) 12 | from .forms import build_form, process_form 13 | from .plot import adjust_layout, create_graph, display_context_info 14 | from .build import build_content, build_menu 15 | -------------------------------------------------------------------------------- /app/build.py: -------------------------------------------------------------------------------- 1 | """ 2 | Streamlit app 3 | Functions related to building the page. 4 | """ 5 | 6 | import base64 7 | import time 8 | 9 | import extra_streamlit_components as stx 10 | import folium 11 | import streamlit as st 12 | from folium import Icon 13 | from pydantic.v1.utils import deep_update 14 | from streamlit_js_eval import streamlit_js_eval 15 | 16 | from app import ( 17 | build_form, 18 | create_graph, 19 | display_context_info, 20 | get_form_defaults, 21 | get_query_params, 22 | process_form, 23 | ) 24 | from meteo_hist import MeteoHist 25 | 26 | 27 | def build_menu() -> None: 28 | """ 29 | Create the column holding themenu. 30 | """ 31 | 32 | # Get query parameters 33 | query_params = get_query_params() 34 | 35 | if len(query_params) > 0: 36 | st.session_state["form_defaults"] = deep_update( 37 | st.session_state["form_defaults"], query_params 38 | ) 39 | 40 | default_tab = ( 41 | query_params["method"] 42 | if "method" in query_params.keys() 43 | else st.session_state["form_defaults"]["method"] 44 | ) 45 | 46 | # Create tab bar to select method for location input 47 | active_tab = stx.tab_bar( 48 | data=[ 49 | stx.TabBarItemData( 50 | id="by_name", 51 | title="By name", 52 | description="Location by name", 53 | ), 54 | stx.TabBarItemData( 55 | id="by_coords", 56 | title="By lat/lon", 57 | description="Location by coordinates", 58 | ), 59 | ], 60 | default=default_tab, 61 | ) 62 | 63 | if active_tab != st.session_state["form_defaults"]["method"]: 64 | # Reset form defaults once method is changed 65 | st.session_state["form_defaults"] = get_form_defaults() 66 | # Remove all query parameters from URL 67 | st.query_params.clear() 68 | 69 | # Build form 70 | st.session_state["input_values"] = build_form( 71 | method=active_tab, params=query_params 72 | ) 73 | 74 | # Create button to show random graph 75 | st.session_state["random_graph"] = st.button("Show random") 76 | 77 | st.markdown( 78 | """ 79 | yotka.org | 80 | 81 | github.com 82 | """, 83 | unsafe_allow_html=True, 84 | ) 85 | 86 | 87 | def build_content(plot_placeholder, message_box) -> None: 88 | """ 89 | Create the column holding the content. 90 | """ 91 | 92 | # Save viewport width to session state 93 | st.session_state["viewport_width"] = streamlit_js_eval( 94 | js_expressions="window.innerWidth", key="ViewportWidth" 95 | ) 96 | 97 | # Wait for viewport width to be set 98 | while ( 99 | "viewport_width" not in st.session_state 100 | or st.session_state["viewport_width"] is None 101 | ): 102 | time.sleep(0.1) 103 | 104 | # Show a random graph on start (but not when the user clicks the "Create" button) 105 | if ( 106 | "last_generated" not in st.session_state 107 | and st.session_state["input_values"] is None 108 | ): 109 | if "start_img" not in st.session_state: 110 | st.session_state["start_img"] = MeteoHist.show_random() 111 | 112 | if st.session_state["start_img"] is not None: 113 | plot_placeholder.image(st.session_state["start_img"]) 114 | 115 | if st.session_state["input_values"] is not None: 116 | # Process form values 117 | input_processed = process_form(st.session_state["input_values"], message_box) 118 | 119 | # Make sure lat/lon values are set 120 | if isinstance(input_processed, dict) and not [ 121 | x for x in (input_processed["lat"], input_processed["lon"]) if x is None 122 | ]: 123 | # Create figure for the graph 124 | plot_object, file_path = create_graph(input_processed, plot_placeholder) 125 | 126 | # Display some info about the data 127 | display_context_info(plot_object) 128 | 129 | # Display a download link 130 | try: 131 | with open(file_path, "rb") as file: 132 | img_b64 = base64.b64encode(file.read()).decode() 133 | 134 | st.markdown( 135 | f'Download file', 136 | unsafe_allow_html=True, 137 | ) 138 | except FileNotFoundError: 139 | st.write("File not found.") 140 | 141 | st.write("") 142 | 143 | with st.expander("Share graph"): 144 | st.write("To share this graph, you can use the following URL:") 145 | st.write(f"{st.session_state['share_url']}", unsafe_allow_html=True) 146 | 147 | with st.expander("Show map"): 148 | with st.spinner("Creating map..."): 149 | # Show a map 150 | folium_map = folium.Map( 151 | location=[input_processed["lat"], input_processed["lon"]], 152 | zoom_start=4, 153 | ) 154 | 155 | # Create a marker with a custom icon 156 | folium.Marker( 157 | location=[input_processed["lat"], input_processed["lon"]], 158 | popup=input_processed["location_name"], 159 | icon=Icon(icon="cloud"), 160 | ).add_to(folium_map) 161 | 162 | # Data from https://xyzservices.readthedocs.io/en/latest/introduction.html 163 | folium.TileLayer( 164 | tiles="https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png", 165 | attr="(C) OpenStreetMap contributors (C) CARTO", 166 | name="CartoDB.Positron", 167 | max_zoom=20, 168 | subdomains="abcd", 169 | ).add_to(folium_map) 170 | 171 | # Convert the map to an HTML string 172 | html_string = folium_map._repr_html_() 173 | 174 | # Display the map 175 | st.components.v1.html(html_string, width=500, height=500) 176 | 177 | if st.session_state["random_graph"]: 178 | st.write("Random graph from the list of graphs created before.") 179 | with plot_placeholder: 180 | img = MeteoHist.show_random() 181 | if img: 182 | st.session_state["start_img"] = img 183 | st.image(img) 184 | else: 185 | st.error("No graph found.") 186 | -------------------------------------------------------------------------------- /app/forms.py: -------------------------------------------------------------------------------- 1 | """ 2 | Streamlit app 3 | Functions related to form building and processing. 4 | """ 5 | 6 | import datetime as dt 7 | 8 | import streamlit as st 9 | from pydantic.v1.utils import deep_update 10 | 11 | from app import build_location_by_coords, build_location_by_name, create_share_url 12 | 13 | 14 | def build_form(method: str = "by_name", params: dict = None) -> dict: 15 | """ 16 | Build form for input values. 17 | """ 18 | params_set = False 19 | 20 | defaults = st.session_state["form_defaults"] 21 | 22 | if isinstance(params, dict): 23 | if len(params) > 0: 24 | defaults = deep_update(defaults, params) 25 | params_set = True 26 | 27 | form_values = {"method": method} 28 | 29 | with st.form("settings_form"): 30 | if method == "by_coords": 31 | # Create input widgets for lat, lon, and display_name 32 | lat_col, lon_col = st.columns([1, 1]) 33 | 34 | with lat_col: 35 | form_values["lat"] = st.text_input("Latitude:", value=defaults["lat"]) 36 | 37 | with lon_col: 38 | form_values["lon"] = st.text_input("Longitude:", value=defaults["lon"]) 39 | 40 | form_values["display_name"] = st.text_input( 41 | "Display name:", 42 | value=defaults["display_name"], 43 | help="""In case no display name can be found for the 44 | given lat/lon coordinates, this name will be used""", 45 | ) 46 | if len(form_values["display_name"]) == 0: 47 | form_values["display_name"] = None 48 | 49 | if method == "by_name": 50 | # Create a text input widget for location name 51 | form_values["location"] = st.text_input( 52 | "Location to display:", 53 | help="""Write the name of the location you want to display. 54 | A search at Openstreetmap's Nominatim will be performed to 55 | find the location and get latitude and longitude. If the 56 | location cannot be found, please try again with a more 57 | specific name.""", 58 | ) 59 | 60 | year_col, ref_col = st.columns([1, 1]) 61 | 62 | with year_col: 63 | # Create input widget for year 64 | years = list(range(dt.datetime.now().year, 1939, -1)) 65 | form_values["year"] = st.selectbox( 66 | "Year to show:", years, index=years.index(defaults["year"]) 67 | ) 68 | 69 | with ref_col: 70 | # Selectbox for reference period 71 | periods_tuples = [ 72 | (1991, 2020), 73 | (1981, 2010), 74 | (1971, 2000), 75 | (1961, 1990), 76 | (1951, 1980), 77 | (1941, 1970), 78 | ] 79 | 80 | # Create str representation of tuples 81 | periods = [f"{period[0]}-{period[1]}" for period in periods_tuples] 82 | 83 | select_ref_period = st.selectbox( 84 | "Reference period:", 85 | periods, 86 | index=periods_tuples.index(defaults["ref_period"]), 87 | help=""" 88 | The reference period is used to calculate the historical average of 89 | the daily values. The average is then used to compare the daily 90 | values of the selected year. 1961-1990 is currently considered 91 | the best "long-term climate change assessment" by the World Meteorological 92 | Organization (WMO). 93 | """, 94 | ) 95 | 96 | # Use tuples as saved values 97 | if select_ref_period: 98 | form_values["ref_period"] = periods_tuples[ 99 | periods.index(select_ref_period) 100 | ] 101 | 102 | # Selector for metric 103 | metrics = [ 104 | { 105 | "name": "temperature_mean", 106 | "data": "temperature_2m_mean", 107 | }, 108 | { 109 | "name": "temperature_min", 110 | "data": "temperature_2m_min", 111 | }, 112 | { 113 | "name": "temperature_max", 114 | "data": "temperature_2m_max", 115 | }, 116 | { 117 | "name": "precipitation_rolling", 118 | "data": "precipitation_sum", 119 | }, 120 | { 121 | "name": "precipitation_cum", 122 | "data": "precipitation_sum", 123 | }, 124 | ] 125 | metrics_names = [ 126 | "Mean temperature", 127 | "Minimum temperature", 128 | "Maximum temperature", 129 | "Precipitation (Rolling average)", 130 | "Precipitation (Cumulated)", 131 | ] 132 | 133 | selected_metric = st.selectbox( 134 | "Metric:", 135 | metrics_names, 136 | index=next( 137 | i 138 | for i, metric_dict in enumerate(metrics) 139 | if metric_dict["name"] == defaults["metric"] 140 | ), 141 | ) 142 | form_values["metric"] = metrics[metrics_names.index(selected_metric)] 143 | 144 | # Number of max peaks to annotate 145 | form_values["highlight_max"] = st.slider( 146 | "Maximum peaks to be annotated:", 147 | min_value=0, 148 | max_value=5, 149 | value=defaults["highlight_max"], 150 | help=""" 151 | Number of peaks above the mean to be annotated. If peaks are close together, 152 | the text might overlap. In this case, reduce the number of peaks. 153 | """, 154 | ) 155 | 156 | # Number of min peaks to annotate 157 | form_values["highlight_min"] = st.slider( 158 | "Minimum peaks to be annotated:", 159 | min_value=0, 160 | max_value=5, 161 | value=defaults["highlight_min"], 162 | help=""" 163 | Number of peaks below the mean to be annotated. If peaks are close together, 164 | the text might overlap. In this case, reduce the number of peaks. 165 | """, 166 | ) 167 | 168 | with st.expander("Advanced settings"): 169 | # Selection for unit system 170 | system = ["metric", "imperial"] 171 | system_names = ["Metric (°C, mm)", "Imperial (°F, In)"] 172 | 173 | selected_system = st.selectbox( 174 | "Unit system:", 175 | system_names, 176 | index=system.index(defaults["system"]), 177 | ) 178 | form_values["system"] = system[system_names.index(selected_system)] 179 | 180 | # Slider to apply LOWESS smoothing 181 | form_values["smooth"] = st.slider( 182 | "Smoothing:", 183 | min_value=0, 184 | max_value=3, 185 | value=defaults["smooth"], 186 | help="""Degree of smoothing to apply to the historical data. 187 | 0 means no smoothing. The higher the value, the more smoothing 188 | is applied. Smoothing is done using LOWESS (Locally Weighted 189 | Scatterplot Smoothing).""", 190 | ) 191 | 192 | # Select method to calculate peaks 193 | peak_methods = ["mean", "percentile"] 194 | peak_methods_names = ["Historical mean", "5/95 percentile"] 195 | 196 | peak_method = st.radio( 197 | "Peak method - Difference to:", 198 | peak_methods_names, 199 | index=peak_methods.index(defaults["peak_method"]), 200 | help=""" 201 | Method to determine the peaks. Either the difference to the historical 202 | mean or the difference to the 5/95 percentile respectively. The percentile 203 | method focuses more on extreme events, while the mean method focuses more 204 | on the difference to the historical average. 205 | """, 206 | ) 207 | form_values["peak_method"] = peak_methods[ 208 | peak_methods_names.index(peak_method) 209 | ] 210 | 211 | # Checkbox to decide if peaks should be emphasized 212 | form_values["peak_alpha"] = st.checkbox( 213 | "Emphasize peaks", 214 | value=defaults["peak_alpha"], 215 | help=""" 216 | If checked, peaks that leave the gray area between the 5 and 95 217 | percentile will be highlighted more. 218 | """, 219 | ) 220 | 221 | # Checkbox to decide if months should have alternating background 222 | form_values["alternate_months"] = st.checkbox( 223 | "Alternate months", 224 | value=defaults["alternate_months"], 225 | help=""" 226 | If checked, the background color of months is alternated. 227 | """, 228 | ) 229 | 230 | # Create button to start the analysis 231 | form_values["create_graph"] = st.form_submit_button("Create") 232 | 233 | if form_values["create_graph"] or params_set: 234 | return form_values 235 | 236 | return None 237 | 238 | 239 | def process_form(form_values: dict, message_box) -> dict: 240 | """ 241 | Process form values. 242 | """ 243 | # Sanity checks 244 | if form_values is None: 245 | message_box.error("Please fill out the form.") 246 | return None 247 | 248 | if form_values["method"] == "by_name": 249 | if len(form_values["location"]) == 0: 250 | message_box.error("Please enter a location name.") 251 | return None 252 | 253 | lat, lon, location = build_location_by_name( 254 | form_values["location"], message_box 255 | ) 256 | form_values["lat"] = lat 257 | form_values["lon"] = lon 258 | form_values["location_name"] = location 259 | 260 | if form_values["method"] == "by_coords": 261 | if len(form_values["lat"]) == 0 or len(form_values["lon"]) == 0: 262 | message_box.error("Please enter latitude and longitude.") 263 | return None 264 | 265 | try: 266 | form_values["lat"] = float(form_values["lat"]) 267 | form_values["lon"] = float(form_values["lon"]) 268 | except ValueError: 269 | message_box.error("Latitude and longitude must be numbers.") 270 | return None 271 | 272 | if form_values["lat"] < -90 or form_values["lat"] > 90: 273 | message_box.error("Latitude must be between -90 and 90.") 274 | return None 275 | 276 | if form_values["lon"] < -180 or form_values["lon"] > 180: 277 | message_box.error("Longitude must be between -180 and 180.") 278 | return None 279 | 280 | form_values["location_name"] = build_location_by_coords( 281 | form_values["lat"], 282 | form_values["lon"], 283 | form_values["display_name"], 284 | message_box, 285 | ) 286 | if form_values["location_name"] is None: 287 | return None 288 | 289 | # Set share URL 290 | st.session_state["share_url"] = create_share_url(form_values) 291 | 292 | # Calculate values for smoothing 293 | if form_values["smooth"] == 0: 294 | form_values["smooth"] = {"apply": False} 295 | else: 296 | frac_values = {1: 1 / 52, 2: 1 / 24, 3: 1 / 12} 297 | form_values["smooth"] = { 298 | "apply": True, 299 | # Get frac value based on smoothing value in form 300 | # 1->1/52, 2->1/24, 3->1/12 (lower values mean less smoothing) 301 | "frac": frac_values[form_values["smooth"]], 302 | } 303 | 304 | # Setting for alternating background colors for months 305 | form_values["alternate_months"] = {"apply": form_values["alternate_months"]} 306 | 307 | return form_values 308 | -------------------------------------------------------------------------------- /app/plot.py: -------------------------------------------------------------------------------- 1 | """ 2 | Streamlit app 3 | Functions related to plotting and displaying data. 4 | """ 5 | 6 | from copy import deepcopy 7 | 8 | import plotly.graph_objects as go 9 | import streamlit as st 10 | 11 | from meteo_hist import MeteoHist, MeteoHistInteractive, OpenMeteoAPIException 12 | 13 | 14 | def create_graph(inputs: dict, plot_placeholder) -> MeteoHist: 15 | """ 16 | Create the graph. 17 | """ 18 | with st.spinner("Creating graph..."): 19 | with plot_placeholder: 20 | # Don't save plot to file here, first show it 21 | inputs["save_file"] = False 22 | 23 | # Calculate width and height based on viewport width 24 | width = ( 25 | st.session_state["viewport_width"] 26 | if st.session_state["viewport_width"] is not None 27 | else 1200 28 | ) 29 | height = width * 3 / 5 30 | 31 | # Determine if we need to create a new plot object or update an existing one 32 | if "last_settings" in st.session_state: 33 | reload_keys = ["lat", "lon", "year", "ref_period", "metric", "system"] 34 | reload = any( 35 | inputs[key] != st.session_state["last_settings"][key] 36 | for key in reload_keys 37 | ) 38 | else: 39 | reload = True 40 | 41 | if "plot" in st.session_state and not reload: 42 | # If plot object is already in session state, use it 43 | plot = st.session_state["plot"] 44 | plot.update_settings(inputs) 45 | 46 | else: 47 | try: 48 | # Instantiate the plot object 49 | plot = MeteoHistInteractive( 50 | coords=(inputs["lat"], inputs["lon"]), 51 | year=inputs["year"], 52 | reference_period=inputs["ref_period"], 53 | metric=inputs["metric"]["name"], 54 | settings=inputs, 55 | ) 56 | except OpenMeteoAPIException as exc: 57 | st.error( 58 | f"There was an error retrieving the data from open-meteo.com. Error message:\n\n {exc}" 59 | ) 60 | st.stop() 61 | 62 | # Save plot object and settings to session state 63 | st.session_state["plot"] = plot 64 | st.session_state["last_settings"] = inputs 65 | 66 | # Create figure 67 | figure, file_path = plot.create_plot() 68 | 69 | # Create a copy of the figure to display in Streamlit 70 | figure_display = deepcopy(figure) 71 | 72 | # Adjust font sizes etc. for display in Streamlit 73 | figure_display = adjust_layout(figure_display, width, height) 74 | 75 | # Display an interactive plot for large screens 76 | if st.session_state["screen_width"] >= 1200: 77 | st.plotly_chart(figure_display, theme=None, width=width, height=height) 78 | 79 | # Save the plot as a file 80 | file_path = plot.save_plot_to_file() 81 | 82 | # Display the plot as an image for small screens 83 | if st.session_state["screen_width"] < 1200: 84 | st.image(file_path) 85 | 86 | # Save the file path to session state 87 | st.session_state["last_generated"] = file_path 88 | 89 | return plot, file_path 90 | 91 | 92 | def adjust_layout(fig: go.Figure, width: int, height: int) -> go.Figure: 93 | """ 94 | Adjust layout of plotly figure just for display in Streamlit. 95 | (This is a hacky workaround for the fact that Streamlit and Plotly 96 | have some deficiencies when it comes to responsive design.) 97 | """ 98 | # Calculate factor based on viewport width 99 | factor = st.session_state["viewport_width"] / 1000 100 | 101 | # Adjust font sizes accordingly 102 | 103 | font_size = int(fig["layout"]["font"]["size"] * factor) 104 | font_size_title = int(fig["layout"]["title"]["font"]["size"] * factor) 105 | font_size_datainfo = int(12 * factor) 106 | margin_b = int(fig["layout"]["margin"]["b"] * factor) 107 | margin_l = int(fig["layout"]["margin"]["l"] * factor) 108 | margin_r = int(fig["layout"]["margin"]["r"] * factor) 109 | margin_t = int(fig["layout"]["margin"]["t"] * factor) 110 | margin_pad = int(fig["layout"]["margin"]["pad"] * factor) 111 | 112 | # Set layout options just for the plot in Streamlit 113 | layout_options = { 114 | "width": width, 115 | "height": height, 116 | "font_size": font_size, 117 | "title_font_size": font_size_title, 118 | "font": {"family": "Lato, Arial, sans-serif"}, 119 | "title": {"font": {"family": "Lato, Arial, sans-serif"}}, 120 | "margin": { 121 | "b": margin_b, 122 | "l": margin_l, 123 | "r": margin_r, 124 | "t": margin_t, 125 | "pad": margin_pad, 126 | }, 127 | } 128 | fig.update_layout(layout_options) 129 | 130 | # Adjust position and font size of annotations 131 | for annotation_name in ["Data source", "Data info"]: 132 | fig.update_annotations( 133 | selector={"name": annotation_name}, 134 | font_size=font_size_datainfo, 135 | font={"family": "Lato, Arial, sans-serif"}, 136 | ) 137 | 138 | return fig 139 | 140 | 141 | def display_context_info(graph: MeteoHist) -> None: 142 | """ 143 | Display context information about the graph. 144 | """ 145 | if graph.coords is not None: 146 | url = ( 147 | f"https://www.openstreetmap.org/" 148 | f"?mlat={graph.coords[0]}&mlon={graph.coords[1]}" 149 | f"#map=6/{graph.coords[0]}/{graph.coords[1]}&layers=H" 150 | ) 151 | 152 | st.markdown( 153 | f"""
154 | Using location: {graph.settings["location_name"]} 155 | (lat: {graph.coords[0]}, lon: {graph.coords[1]}) 156 |
""", 157 | unsafe_allow_html=True, 158 | ) 159 | 160 | st.markdown( 161 | f"""
162 | Last available date: {graph.last_date} 163 |
""", 164 | unsafe_allow_html=True, 165 | ) 166 | -------------------------------------------------------------------------------- /app/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Streamlit app utility functions 3 | """ 4 | 5 | import datetime as dt 6 | import urllib.parse 7 | 8 | import streamlit as st 9 | 10 | from meteo_hist import MeteoHist 11 | 12 | 13 | def get_form_defaults() -> dict: 14 | """ 15 | Get default values for the form. 16 | """ 17 | form_defaults = { 18 | "method": "by_name", 19 | "lat": "", 20 | "lon": "", 21 | "display_name": "", 22 | "year": dt.datetime.now().year, 23 | "ref_period": (1961, 1990), 24 | "highlight_max": 1, 25 | "highlight_min": 1, 26 | "metric": "temperature_mean", 27 | "plot_type": "interactive", 28 | "system": "metric", 29 | "smooth": 3, 30 | "peak_method": "mean", 31 | "peak_alpha": True, 32 | "alternate_months": True, 33 | } 34 | 35 | return form_defaults 36 | 37 | 38 | def get_base_url() -> str: 39 | """ 40 | Get base URL from current session. 41 | """ 42 | 43 | if "base_url" in st.session_state: 44 | return st.session_state["base_url"] 45 | 46 | # If not set manually, get base URL 47 | session = st.runtime.get_instance()._session_mgr.list_active_sessions()[0] 48 | base_url = urllib.parse.urlunparse( 49 | [session.client.request.protocol, session.client.request.host, "", "", "", ""] 50 | ) 51 | 52 | return base_url 53 | 54 | 55 | def get_query_params() -> dict: 56 | """ 57 | Get query parameters from URL. 58 | """ 59 | 60 | query = st.query_params 61 | 62 | allowed_params = [ 63 | "lat", 64 | "lon", 65 | "display_name", 66 | "year", 67 | "ref_period", 68 | "highlight_max", 69 | "highlight_min", 70 | "metric", 71 | "system", 72 | "smooth", 73 | "peak_method", 74 | "peak_alpha", 75 | "alternate_months", 76 | ] 77 | 78 | # Filter out values not allowed or empty 79 | params = { 80 | key: query[key] 81 | for key in allowed_params 82 | if key in query.keys() and query[key] != "" 83 | } 84 | 85 | # Convert parameters and apply sanity checks 86 | remove_keys = [] 87 | 88 | for key, value in params.items(): 89 | # Check keys with float values 90 | if key in ["lat", "lon"] and isinstance(value, str): 91 | try: 92 | params[key] = float(value) 93 | if key == "lat" and (params[key] < -90 or params[key] > 90): 94 | remove_keys.append(key) 95 | if key == "lon" and (params[key] < -180 or params[key] > 180): 96 | remove_keys.append(key) 97 | except ValueError: 98 | remove_keys.append(key) 99 | 100 | # Check keys with int values 101 | elif key in ["year", "highlight_max", "highlight_min", "smooth"] and isinstance( 102 | value, str 103 | ): 104 | try: 105 | params[key] = int(value) 106 | if key == "year" and ( 107 | params[key] < 1940 or params[key] > dt.datetime.now().year 108 | ): 109 | remove_keys.append(key) 110 | if ( 111 | key in ["highlight_max", "highlight_min"] 112 | and not 0 <= params[key] <= 5 113 | ): 114 | remove_keys.append(key) 115 | if key == "smooth" and not 0 <= params[key] <= 3: 116 | remove_keys.append(key) 117 | except ValueError: 118 | remove_keys.append(key) 119 | 120 | # Check ref_period 121 | elif key == "ref_period" and isinstance(value, str): 122 | params[key] = ( 123 | int("".join(value).split("-", maxsplit=1)[0]), 124 | int("".join(value).split("-", maxsplit=1)[1]), 125 | ) 126 | if not ( 127 | params[key][0] in [1941, 1951, 1961, 1971, 1981, 1991] 128 | and params[key][1] - params[key][0] == 29 129 | ): 130 | remove_keys.append(key) 131 | 132 | # Check boolean values 133 | elif key in ["peak_alpha", "alternate_months"] and isinstance(value, str): 134 | params[key] = value != "false" 135 | 136 | # Check unit system 137 | elif key == "system" and isinstance(value, str) and value != "imperial": 138 | remove_keys.append(key) 139 | 140 | # Check metric 141 | elif ( 142 | key == "metric" 143 | and isinstance(value, str) 144 | and value 145 | not in [ 146 | "temperature_min", 147 | "temperature_max", 148 | "precipitation_rolling", 149 | "precipitation_cum", 150 | ] 151 | ): 152 | remove_keys.append(key) 153 | 154 | # Remove invalid keys 155 | params = {key: value for key, value in params.items() if key not in remove_keys} 156 | 157 | if "lat" in params.keys() and "lon" in params.keys(): 158 | params["method"] = "by_coords" 159 | return params 160 | 161 | return {} 162 | 163 | 164 | def create_share_url(params: dict) -> str: 165 | """ 166 | Create URL with settings parameters to share graph. 167 | """ 168 | params = params.copy() 169 | 170 | allowed_keys = [ 171 | "lat", 172 | "lon", 173 | "display_name", 174 | "year", 175 | "ref_period", 176 | "highlight_max", 177 | "highlight_min", 178 | "metric", 179 | "system", 180 | "smooth", 181 | "peak_method", 182 | "peak_alpha", 183 | "alternate_months", 184 | ] 185 | 186 | # Change metric to str 187 | params["metric"] = params["metric"]["name"] 188 | 189 | # Filter out values not allowed, defaults, and empty values 190 | params = { 191 | key: params[key] 192 | for key in allowed_keys 193 | if key in params.keys() 194 | and params[key] != get_form_defaults()[key] 195 | and params[key] is not None 196 | } 197 | 198 | # Change ref_period to str 199 | if "ref_period" in params.keys(): 200 | params["ref_period"] = f"{params['ref_period'][0]}-{params['ref_period'][1]}" 201 | 202 | # Change boolean values to lowercase 203 | if "peak_alpha" in params.keys(): 204 | params["peak_alpha"] = str(params["peak_alpha"]).lower() 205 | if "alternate_months" in params.keys(): 206 | params["alternate_months"] = str(params["alternate_months"]).lower() 207 | 208 | return f"{get_base_url()}?{urllib.parse.urlencode(params)}" 209 | 210 | 211 | def build_location_by_name(location: str, message_box) -> tuple[float, float, str]: 212 | """ 213 | Build location by name. 214 | """ 215 | with st.spinner("Searching for latitude and longitude..."): 216 | # Get the latitude and longitude 217 | location = MeteoHist.get_lat_lon(location) 218 | 219 | if len(location) == 0: 220 | message_box.error("Location not found. Please try again.") 221 | return None, None, None 222 | 223 | lat = location[0]["lat"] 224 | lon = location[0]["lon"] 225 | location_name = location[0]["location_name"] 226 | 227 | return lat, lon, location_name 228 | 229 | 230 | def build_location_by_coords( 231 | lat: float, lon: float, display_name: str, message_box 232 | ) -> str: 233 | """ 234 | Build location by coordinates. 235 | """ 236 | with st.spinner("Searching for location name..."): 237 | # Get the location name 238 | location = MeteoHist.get_location((lat, lon)) 239 | 240 | if location is None and display_name is None: 241 | location = None 242 | message_box.error("Location not found. Please provide a display name.") 243 | return None 244 | 245 | if location is None and display_name is not None: 246 | location = display_name 247 | message_box.info("Location not found. Using display name.") 248 | 249 | return location 250 | -------------------------------------------------------------------------------- /examples/addis-ababa-ethiopia-temperature-mean-2022-ref-1961-1990.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yotkadata/meteo_hist/d03b3b6d3887b85886685b69a07a5972adf8e068/examples/addis-ababa-ethiopia-temperature-mean-2022-ref-1961-1990.png -------------------------------------------------------------------------------- /examples/atlantic-ocean-temperature-mean-2023-ref-1961-1990.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yotkadata/meteo_hist/d03b3b6d3887b85886685b69a07a5972adf8e068/examples/atlantic-ocean-temperature-mean-2023-ref-1961-1990.png -------------------------------------------------------------------------------- /examples/bad-neuenahr-ahrweiler-germany-precipitation-rolling-2021-ref-1961-1990.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yotkadata/meteo_hist/d03b3b6d3887b85886685b69a07a5972adf8e068/examples/bad-neuenahr-ahrweiler-germany-precipitation-rolling-2021-ref-1961-1990.png -------------------------------------------------------------------------------- /examples/beijing-china-temperature-mean-2022-ref-1961-1990.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yotkadata/meteo_hist/d03b3b6d3887b85886685b69a07a5972adf8e068/examples/beijing-china-temperature-mean-2022-ref-1961-1990.png -------------------------------------------------------------------------------- /examples/berlin-germany-temperature-mean-2023-ref-1961-1990.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yotkadata/meteo_hist/d03b3b6d3887b85886685b69a07a5972adf8e068/examples/berlin-germany-temperature-mean-2023-ref-1961-1990.png -------------------------------------------------------------------------------- /examples/buenos-aires-argentina-temperature-mean-2022-ref-1961-1990.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yotkadata/meteo_hist/d03b3b6d3887b85886685b69a07a5972adf8e068/examples/buenos-aires-argentina-temperature-mean-2022-ref-1961-1990.png -------------------------------------------------------------------------------- /examples/cape-town-south-africa-temperature-mean-2022-ref-1961-1990.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yotkadata/meteo_hist/d03b3b6d3887b85886685b69a07a5972adf8e068/examples/cape-town-south-africa-temperature-mean-2022-ref-1961-1990.png -------------------------------------------------------------------------------- /examples/caracas-venezuela-precipitation-cum-2014-ref-1961-1990.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yotkadata/meteo_hist/d03b3b6d3887b85886685b69a07a5972adf8e068/examples/caracas-venezuela-precipitation-cum-2014-ref-1961-1990.png -------------------------------------------------------------------------------- /examples/caracas-venezuela-temperature-mean-2022-ref-1961-1990.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yotkadata/meteo_hist/d03b3b6d3887b85886685b69a07a5972adf8e068/examples/caracas-venezuela-temperature-mean-2022-ref-1961-1990.png -------------------------------------------------------------------------------- /examples/duisburg-germany-temperature-max-2019-ref-1961-1990.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yotkadata/meteo_hist/d03b3b6d3887b85886685b69a07a5972adf8e068/examples/duisburg-germany-temperature-max-2019-ref-1961-1990.png -------------------------------------------------------------------------------- /examples/hawaii-county-united-states-temperature-mean-2022-ref-1961-1990.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yotkadata/meteo_hist/d03b3b6d3887b85886685b69a07a5972adf8e068/examples/hawaii-county-united-states-temperature-mean-2022-ref-1961-1990.png -------------------------------------------------------------------------------- /examples/helsinki-finland-temperature-mean-2022-ref-1961-1990.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yotkadata/meteo_hist/d03b3b6d3887b85886685b69a07a5972adf8e068/examples/helsinki-finland-temperature-mean-2022-ref-1961-1990.png -------------------------------------------------------------------------------- /examples/kampala-uganda-temperature-mean-2022-ref-1961-1990.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yotkadata/meteo_hist/d03b3b6d3887b85886685b69a07a5972adf8e068/examples/kampala-uganda-temperature-mean-2022-ref-1961-1990.png -------------------------------------------------------------------------------- /examples/key-west-united-states-temperature-max-2023-ref-1961-1990.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yotkadata/meteo_hist/d03b3b6d3887b85886685b69a07a5972adf8e068/examples/key-west-united-states-temperature-max-2023-ref-1961-1990.png -------------------------------------------------------------------------------- /examples/kyiv-ukraine-temperature-mean-2022-ref-1961-1990.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yotkadata/meteo_hist/d03b3b6d3887b85886685b69a07a5972adf8e068/examples/kyiv-ukraine-temperature-mean-2022-ref-1961-1990.png -------------------------------------------------------------------------------- /examples/madrid-spain-temperature-mean-2023-ref-1961-1990.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yotkadata/meteo_hist/d03b3b6d3887b85886685b69a07a5972adf8e068/examples/madrid-spain-temperature-mean-2023-ref-1961-1990.png -------------------------------------------------------------------------------- /examples/monterrey-mexico-temperature-mean-2023-ref-1961-1990.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yotkadata/meteo_hist/d03b3b6d3887b85886685b69a07a5972adf8e068/examples/monterrey-mexico-temperature-mean-2023-ref-1961-1990.png -------------------------------------------------------------------------------- /examples/mumbai-india-precipitation-cum-2022-ref-1961-1990.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yotkadata/meteo_hist/d03b3b6d3887b85886685b69a07a5972adf8e068/examples/mumbai-india-precipitation-cum-2022-ref-1961-1990.png -------------------------------------------------------------------------------- /examples/mumbai-india-precipitation-rolling-2021-ref-1961-1990.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yotkadata/meteo_hist/d03b3b6d3887b85886685b69a07a5972adf8e068/examples/mumbai-india-precipitation-rolling-2021-ref-1961-1990.png -------------------------------------------------------------------------------- /examples/mumbai-india-temperature-mean-2022-ref-1961-1990.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yotkadata/meteo_hist/d03b3b6d3887b85886685b69a07a5972adf8e068/examples/mumbai-india-temperature-mean-2022-ref-1961-1990.png -------------------------------------------------------------------------------- /examples/pacific-ocean-temperature-mean-2023-ref-1961-1990.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yotkadata/meteo_hist/d03b3b6d3887b85886685b69a07a5972adf8e068/examples/pacific-ocean-temperature-mean-2023-ref-1961-1990.png -------------------------------------------------------------------------------- /examples/prague-czechia-precipitation-rolling-2022-ref-1961-1990.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yotkadata/meteo_hist/d03b3b6d3887b85886685b69a07a5972adf8e068/examples/prague-czechia-precipitation-rolling-2022-ref-1961-1990.png -------------------------------------------------------------------------------- /examples/riyadh-saudi-arabia-temperature-mean-2022-ref-1961-1990.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yotkadata/meteo_hist/d03b3b6d3887b85886685b69a07a5972adf8e068/examples/riyadh-saudi-arabia-temperature-mean-2022-ref-1961-1990.png -------------------------------------------------------------------------------- /examples/rome-italy-temperature-max-2023-ref-1961-1990.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yotkadata/meteo_hist/d03b3b6d3887b85886685b69a07a5972adf8e068/examples/rome-italy-temperature-max-2023-ref-1961-1990.png -------------------------------------------------------------------------------- /examples/san-francisco-united-states-temperature-mean-2022-ref-1961-1990.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yotkadata/meteo_hist/d03b3b6d3887b85886685b69a07a5972adf8e068/examples/san-francisco-united-states-temperature-mean-2022-ref-1961-1990.png -------------------------------------------------------------------------------- /examples/seville-spain-precipitation-cum-2023-ref-1961-1990.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yotkadata/meteo_hist/d03b3b6d3887b85886685b69a07a5972adf8e068/examples/seville-spain-precipitation-cum-2023-ref-1961-1990.png -------------------------------------------------------------------------------- /examples/sydney-australia-temperature-mean-2022-ref-1961-1990.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yotkadata/meteo_hist/d03b3b6d3887b85886685b69a07a5972adf8e068/examples/sydney-australia-temperature-mean-2022-ref-1961-1990.png -------------------------------------------------------------------------------- /header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yotkadata/meteo_hist/d03b3b6d3887b85886685b69a07a5972adf8e068/header.png -------------------------------------------------------------------------------- /meteo_hist/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | MeteoHist is a Python package to visualize historical weather data from OpenMeteo API. 3 | """ 4 | 5 | from .exceptions import OpenMeteoAPIException, APICallFailed 6 | from .base import MeteoHist 7 | from .interactive import MeteoHistInteractive 8 | -------------------------------------------------------------------------------- /meteo_hist/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Classes to define exceptions for the meteo_hist package. 3 | """ 4 | 5 | 6 | class OpenMeteoAPIException(Exception): 7 | "Raised when an error occurs during data retrieval from OpenMeteo API." 8 | 9 | 10 | class APICallFailed(Exception): 11 | "Raised when an API call fails." 12 | -------------------------------------------------------------------------------- /meteo_hist/interactive.py: -------------------------------------------------------------------------------- 1 | """ 2 | Class to generate an interactive version of the plot using Plotly. 3 | """ 4 | 5 | import datetime as dt 6 | 7 | import numpy as np 8 | import pandas as pd 9 | import plotly.graph_objects as go 10 | from plotly.express.colors import sample_colorscale 11 | 12 | from meteo_hist import MeteoHist, OpenMeteoAPIException 13 | 14 | 15 | class MeteoHistInteractive(MeteoHist): 16 | """ 17 | Class to create an interactive plot of a year's meteo values compared to historical values. 18 | Inherits from MeteoHist, which does the data processing. 19 | """ 20 | 21 | def __init__( 22 | self, 23 | coords: tuple[float, float], 24 | year: int = None, 25 | reference_period: tuple[int, int] = (1961, 1990), 26 | metric: str = "temperature_mean", 27 | settings: dict = None, 28 | data: pd.DataFrame = None, 29 | layout_options: dict = None, 30 | ): 31 | """ 32 | Parameters 33 | ---------- 34 | coords: tuple of floats 35 | Latitude and longitude of the location. 36 | year: int 37 | Year to plot. 38 | reference_period: tuple of ints 39 | Reference period to compare the data, by default (1961, 1990). 40 | metric: str 41 | Metric to plot. Allowed values: temperature_mean (default), temperature_min, 42 | temperature_max, precipitation_rolling, precipitation_cum. 43 | settings: dict, optional 44 | Settings dictionary, by default None. 45 | data: pd.DataFrame, optional 46 | Dataframe with metric data, by default None. 47 | layout_options: dict, optional 48 | Dictionary with layout options for the plot, by default None. 49 | """ 50 | # Call the base class constructor using super() 51 | super().__init__(coords, year, reference_period, metric, settings, data) 52 | self.layout_options = layout_options 53 | self.fig = None 54 | 55 | def get_colorscale(self) -> np.ndarray: 56 | """ 57 | Get the colorscale for the plot as a combination of two colormaps. 58 | Values above/below the mean are separately normalized to 0-1 and then 59 | mapped to a colormap. 60 | """ 61 | # Get difference between year's value and mean of reference period 62 | diff = self.data[f"{self.year}_diff"].copy().to_numpy() 63 | 64 | # Create masks for above and below mean 65 | mask_above = diff > 0 66 | mask_below = diff < 0 67 | 68 | # Get absolute value of difference 69 | diff = abs(diff) 70 | 71 | # Create array of zeros with same shape as diff 72 | diff_norm = np.zeros_like(diff) 73 | 74 | # Create array of white colors with same shape as diff 75 | colors = np.full_like(diff, "rgb(255, 255, 255)", dtype="object") 76 | 77 | def normalize_and_get_colors( 78 | diff: np.ndarray, diff_norm: np.ndarray, mask: np.ndarray, colormap: str 79 | ) -> np.ndarray: 80 | """ 81 | Normalize values and map to a colormap. 82 | 83 | Parameters 84 | ---------- 85 | diff: np.ndarray 86 | Array with difference values. 87 | diff_norm: np.ndarray 88 | Array with normalized difference values. 89 | mask: np.ndarray 90 | Mask to apply to the arrays. 91 | colormap: str 92 | Name of the colormap to use. 93 | """ 94 | if len(diff[mask]) == 0: 95 | return None 96 | 97 | if len(diff[mask]) == 1: 98 | # Only one value, assign it a default value of 1 99 | diff_norm[mask] = np.array([1]) 100 | else: 101 | # Normalize to 0-1 102 | max_val = np.nanmax(diff[mask]) 103 | min_val = np.nanmin(diff[mask]) 104 | diff_norm[mask] = (diff[mask] - min_val) / (max_val - min_val) 105 | 106 | return sample_colorscale( 107 | self.settings["metric"]["colors"][colormap], diff_norm[mask] 108 | ) 109 | 110 | # Apply normalization and get colors for above and below mean 111 | colors[mask_above] = normalize_and_get_colors( 112 | diff, diff_norm, mask_above, "cmap_above" 113 | ) 114 | colors[mask_below] = normalize_and_get_colors( 115 | diff, diff_norm, mask_below, "cmap_below" 116 | ) 117 | 118 | return colors 119 | 120 | def get_opacity(self) -> np.ndarray: 121 | """ 122 | Get the opacity for the plot 123 | """ 124 | # Define array of ones with same shape as yearly values 125 | opacity = np.ones_like(self.data[f"{self.year}"]) 126 | 127 | if self.settings["peak_alpha"]: 128 | # Create mask for values between p05 and p95 129 | mask_between = (self.data[f"{self.year}"] >= self.data["p05"]) & ( 130 | self.data[f"{self.year}"] <= self.data["p95"] 131 | ) 132 | 133 | # Set opacity to 0.6 for values between p05 and p95 134 | opacity[mask_between] = 0.6 135 | 136 | return opacity 137 | 138 | def add_alternating_bg(self, fig: go.Figure) -> go.Figure: 139 | """ 140 | Add alternating background color for months. 141 | 142 | Parameters 143 | ---------- 144 | fig: go.Figure 145 | Plotly figure object. 146 | """ 147 | 148 | # Define dict with first and last day of each month, ignoring leap days 149 | months_with_days = { 150 | month: ( 151 | dt.datetime(self.year, month, 1), 152 | dt.datetime( 153 | self.year, 154 | month, 155 | 28 if month == 2 else 30 if month in [4, 6, 9, 11] else 31, 156 | ), 157 | ) 158 | for month in range(1, 13) 159 | } 160 | 161 | for month, days in months_with_days.items(): 162 | # Define background color 163 | bg_color = ( 164 | self.settings["alternate_months"]["even_color"] 165 | if (month % 2) == 0 166 | else self.settings["alternate_months"]["odd_color"] 167 | ) 168 | 169 | # Define background opacity 170 | bg_opacity = ( 171 | self.settings["alternate_months"]["even_alpha"] 172 | if (month % 2) == 0 173 | else self.settings["alternate_months"]["odd_alpha"] 174 | ) 175 | 176 | fig.add_shape( 177 | type="rect", 178 | yref="paper", 179 | x0=days[0], 180 | x1=days[1], 181 | y0=0, 182 | y1=1, 183 | fillcolor=bg_color, 184 | opacity=bg_opacity, 185 | layer="below", 186 | line_width=0, 187 | ) 188 | 189 | return fig 190 | 191 | def plot_percentile_area(self, fig: go.Figure) -> go.Figure: 192 | """ 193 | Add filled area between p05 and p95 to plot. 194 | 195 | Parameters 196 | ---------- 197 | fig: go.Figure 198 | Plotly figure object. 199 | """ 200 | 201 | fig.add_traces( 202 | [ 203 | # p95 trace used as upper bound 204 | go.Scatter( 205 | x=self.data["date"], 206 | y=self.data["p95"], 207 | name="Percentile area upper bound (p95)", 208 | # Make line invisible 209 | line_color="rgba(0,0,0,0)", 210 | showlegend=False, 211 | hoverinfo="skip", 212 | ), 213 | # Fill area between p05 and p95 214 | go.Scatter( 215 | x=self.data["date"], 216 | y=self.data["p05"], 217 | name="Area between p05 and p95", 218 | fill="tonexty", 219 | fillcolor=self.settings["fill_percentiles"], 220 | # Make line invisible 221 | line_color="rgba(0,0,0,0)", 222 | showlegend=False, 223 | hoverinfo="skip", 224 | ), 225 | ] 226 | ) 227 | 228 | return fig 229 | 230 | def plot_percentile_lines(self, fig: go.Figure) -> go.Figure: 231 | """ 232 | Add percentile lines to plot. 233 | 234 | Parameters 235 | ---------- 236 | fig: go.Figure 237 | Plotly figure object. 238 | """ 239 | 240 | fig.add_traces( 241 | [ 242 | # p95 trace 243 | go.Scatter( 244 | x=self.data["date"], 245 | y=self.data["p95"], 246 | name="P95", 247 | line={"color": "#000", "width": 1, "dash": "dot"}, 248 | showlegend=False, 249 | hovertemplate=( 250 | "%{y:.1f}" 251 | f"{super().get_units()}" 252 | f"95th percentile {self.reference_period[0]}-" 253 | f"{self.reference_period[1]}" 254 | ), 255 | ), 256 | # p05 trace 257 | go.Scatter( 258 | x=self.data["date"], 259 | y=self.data["p05"], 260 | name="P05", 261 | line={"color": "#000", "width": 1, "dash": "dot"}, 262 | showlegend=False, 263 | hovertemplate=( 264 | "%{y:.1f}" 265 | f"{super().get_units()}" 266 | f"5th percentile {self.reference_period[0]}-" 267 | f"{self.reference_period[1]}" 268 | ), 269 | ), 270 | ] 271 | ) 272 | 273 | return fig 274 | 275 | def plot_mean(self, fig: go.Figure) -> go.Figure: 276 | """ 277 | Plot the the long-term mean. 278 | 279 | Parameters 280 | ---------- 281 | fig: go.Figure 282 | Plotly figure object. 283 | """ 284 | 285 | fig.add_trace( 286 | go.Scatter( 287 | x=self.data["date"], 288 | y=self.data["mean"], 289 | name="Mean", 290 | line={"color": "#000", "width": 2.5}, 291 | showlegend=False, 292 | hovertemplate=( 293 | "%{y:.1f}" 294 | f"{super().get_units()}" 295 | f"Mean {self.reference_period[0]}-" 296 | f"{self.reference_period[1]}" 297 | ), 298 | ), 299 | ) 300 | 301 | return fig 302 | 303 | def plot_diff(self, fig: go.Figure, chart_type: str = "area") -> go.Figure: 304 | """ 305 | Plot the difference between the year's value and the long-term mean. 306 | 307 | Parameters 308 | ---------- 309 | fig: go.Figure 310 | Plotly figure object. 311 | chart_type: str 312 | Type of chart to display, either "area" or "bar". 313 | """ 314 | 315 | opacity = self.get_opacity() 316 | colors = self.get_colorscale() 317 | 318 | # Display a simpler and faster plot if chart_type is "bar" 319 | if chart_type == "bar": 320 | fig.add_trace( 321 | go.Bar( 322 | x=self.data["date"], 323 | y=self.data[f"{self.year}_diff"], 324 | base=self.data["mean"], 325 | name=f"{self.year} value", 326 | marker={"color": colors, "line_width": 0, "opacity": opacity}, 327 | showlegend=False, 328 | hovertemplate=("%{y:.1f}" f"{super().get_units()}"), 329 | ) 330 | ) 331 | 332 | return fig 333 | 334 | # Invisible trace just to show the correct hover info 335 | fig.add_trace( 336 | go.Scatter( 337 | x=self.data["date"], 338 | y=self.data[f"{self.year}"], 339 | showlegend=False, 340 | mode="markers", 341 | name="Hoverinfo current date", 342 | hovertemplate=("%{y:.1f}" f"{super().get_units()}" f""), 343 | marker={ 344 | "color": colors, # This color will be shown on hover 345 | "opacity": 0, # Hide the marker 346 | }, 347 | ) 348 | ) 349 | 350 | # For each day, add a filled area between the mean and the year's value 351 | for i in range(len(self.data) - 1): 352 | # Define x and y values to draw a polygon between mean and values of today and tomorrow 353 | date_today = self.data["date"].iloc[i] 354 | date_tomorrow = self.data["date"].iloc[i + 1] 355 | mean_today = self.data["mean"].iloc[i] 356 | mean_tomorrow = self.data["mean"].iloc[i + 1] 357 | value_today = self.data[f"{self.year}"].iloc[i] 358 | value_tomorrow = self.data[f"{self.year}"].iloc[i + 1] 359 | 360 | # If one day is above and the other below the mean, set the value to the mean 361 | if (value_today > mean_today) ^ (value_tomorrow > mean_tomorrow): 362 | value_tomorrow = mean_tomorrow 363 | 364 | fig.add_trace( 365 | go.Scatter( 366 | name=f"Daily value {self.data['date'].iloc[i].strftime('%d.%m.%Y')}", 367 | x=[date_today, date_today, date_tomorrow, date_tomorrow], 368 | y=[mean_today, value_today, value_tomorrow, mean_tomorrow], 369 | line_width=0, 370 | fill="toself", 371 | fillcolor=colors[i], 372 | showlegend=False, 373 | mode="lines", 374 | opacity=opacity[i], 375 | hoverinfo="skip", 376 | ) 377 | ) 378 | 379 | return fig 380 | 381 | def annotate_peaks(self, fig: go.Figure, how: str = "max") -> go.Figure: 382 | """ 383 | Annotate maximum or minimum values. 384 | 385 | Parameters 386 | ---------- 387 | fig: go.Figure 388 | Plotly figure object. 389 | how: str 390 | Annotation type, either "max" or "min". 391 | """ 392 | if how not in ["max", "min"]: 393 | return fig 394 | 395 | conf_options = { 396 | "max": { 397 | "text": "Maximum", 398 | "setting": "highlight_max", 399 | "ref": "p95", 400 | "asc": False, 401 | "prefix": "+", 402 | "yanchor": "bottom", 403 | "yshift": 10, 404 | }, 405 | "min": { 406 | "text": "Minimum", 407 | "setting": "highlight_min", 408 | "ref": "p05", 409 | "asc": True, 410 | "prefix": "", 411 | "yanchor": "top", 412 | "yshift": -10, 413 | }, 414 | } 415 | conf = conf_options[how] 416 | 417 | # Create a copy of the dataframe to sort 418 | df_sorted = self.data.copy() 419 | 420 | if self.settings["peak_method"] != "percentile": 421 | # By default, sort by difference between year's value and mean 422 | sort_column = f"{self.year}_diff" 423 | else: 424 | # If peak method is percentile, sort by difference between year's value and p95/p05 425 | sort_column = f"{self.year}_diff_minmax" 426 | df_sorted[sort_column] = df_sorted[f"{self.year}"] - df_sorted[conf["ref"]] 427 | 428 | df_sorted = df_sorted.sort_values(sort_column, ascending=conf["asc"]) 429 | 430 | # Remove values that are too close together (min_distance) 431 | for i in range(self.settings[conf["setting"]]): 432 | current = df_sorted["dayofyear"].iloc[i] 433 | min_distance = self.settings["peak_distance"] 434 | range_around_current = [ 435 | day 436 | for day in range(current - min_distance, current + min_distance + 1) 437 | if day != current 438 | ] 439 | # Filter out values that are too close to the current one 440 | df_sorted = df_sorted[~df_sorted["dayofyear"].isin(range_around_current)] 441 | 442 | for i in range(self.settings[conf["setting"]]): 443 | # Add text 444 | fig.add_annotation( 445 | x=df_sorted["date"].iloc[i], 446 | y=df_sorted[f"{self.year}"].iloc[i], 447 | text=( 448 | f"{conf['prefix']}{df_sorted[f'{self.year}_diff'].values[i]:.1f}" 449 | f"{super().get_units()}" 450 | ), 451 | showarrow=False, 452 | xanchor="center", 453 | yanchor=conf["yanchor"], 454 | yshift=conf["yshift"], 455 | ) 456 | # Add circles 457 | fig.add_trace( 458 | go.Scatter( 459 | x=[df_sorted["date"].iloc[i]], 460 | y=[df_sorted[f"{self.year}"].iloc[i]], 461 | mode="markers", 462 | name=f"{conf['text']} {i+1}", 463 | marker={ 464 | "color": "rgba(255,255,255,0)", 465 | "size": 10, 466 | "line": {"color": "#000", "width": 1}, 467 | }, 468 | showlegend=False, 469 | hoverinfo="skip", 470 | ) 471 | ) 472 | 473 | return fig 474 | 475 | def add_annotations(self, fig: go.Figure) -> go.Figure: 476 | """ 477 | Add annotations to the plot to explain the data. 478 | 479 | Parameters 480 | ---------- 481 | fig: go.Figure 482 | Plotly figure object. 483 | """ 484 | y_min, y_max = self.get_y_limits() 485 | 486 | # Annotations for the mean line 487 | 488 | if self.settings["metric"]["name"] == "precipitation_cum": 489 | # Position arrow on the mean line in mid April 490 | arrow_x = dt.datetime.strptime(f"{self.year}-04-15", "%Y-%m-%d") 491 | arrow_y = self.data[self.data["date"] == arrow_x]["mean"].values[0] 492 | 493 | # Position text center mid March at 1/6 of the distance 494 | # between maximum value for February to April and y axis maximum 495 | text_x = dt.datetime.strptime(f"{self.year}-03-15", "%Y-%m-%d") 496 | max_value = super().get_min_max((46, 105)) 497 | text_y = max_value + (y_max - max_value) / 6 498 | 499 | elif self.settings["metric"]["name"] == "precipitation_rolling": 500 | # Position arrow on the mean line in mid March 501 | arrow_x = dt.datetime.strptime(f"{self.year}-03-15", "%Y-%m-%d") 502 | arrow_y = self.data[self.data["date"] == arrow_x]["mean"].values[0] 503 | 504 | # Position text center in February at 1/6 of the distance 505 | # between maximum value for January to February and y axis maximum 506 | text_x = dt.datetime.strptime(f"{self.year}-02-01", "%Y-%m-%d") 507 | max_value = super().get_min_max((1, 90)) 508 | text_y = max_value + (y_max - max_value) / 6 509 | 510 | else: 511 | # Position arrow on the mean line in March 512 | arrow_x = dt.datetime.strptime(f"{self.year}-03-15", "%Y-%m-%d") 513 | arrow_y = self.data[self.data["date"] == arrow_x]["mean"].values[0] 514 | 515 | # Position text center in mid April at 1/3 of the distance 516 | # between minimum value for March to May and y axis minimum 517 | text_x = dt.datetime.strptime(f"{self.year}-04-15", "%Y-%m-%d") 518 | min_value = super().get_min_max((74, 135), which="min") 519 | text_y = min_value - (min_value - y_min) / 3 520 | 521 | fig.add_annotation( 522 | x=arrow_x, 523 | y=arrow_y, 524 | xref="x", 525 | yref="y", 526 | ax=text_x, 527 | ay=text_y, 528 | axref="x", 529 | ayref="y", 530 | text=( 531 | f"{self.settings['metric']['description']}
" 532 | f"{self.reference_period[0]}-{self.reference_period[1]}" 533 | ), 534 | showarrow=True, 535 | xanchor="center", 536 | yanchor="middle", 537 | arrowwidth=2, 538 | arrowcolor="#000", 539 | name="Reference period mean", 540 | ) 541 | 542 | # Annotations for the area between p05 and p95 543 | 544 | if self.settings["metric"]["name"] == "precipitation_cum": 545 | # Position arrow 1/6 into the p05/p95 area in mid September 546 | arrow_x = dt.datetime.strptime(f"{self.year}-09-15", "%Y-%m-%d") 547 | idx = self.data[self.data["date"] == arrow_x].index[0] 548 | mean, p05 = self.data.iloc[idx]["mean"], self.data.iloc[idx]["p05"] 549 | arrow_y = p05 + (mean - p05) / 6 550 | 551 | # Position text center mid October at 1/3 of the distance 552 | # between minimum value for September to November and y axis minimum 553 | text_x = dt.datetime.strptime(f"{self.year}-10-15", "%Y-%m-%d") 554 | min_value = super().get_min_max((258, 319), which="min") 555 | text_y = min_value - (min_value - y_min) / 3 556 | 557 | elif self.settings["metric"]["name"] == "precipitation_rolling": 558 | # Position arrow 1/6 into the p05/p95 area in mid September 559 | arrow_x = dt.datetime.strptime(f"{self.year}-09-15", "%Y-%m-%d") 560 | idx = self.data[self.data["date"] == arrow_x].index[0] 561 | mean, p95 = self.data.iloc[idx]["mean"], self.data.iloc[idx]["p95"] 562 | arrow_y = p95 - (p95 - mean) / 6 563 | 564 | # Position text center mid October at 1/3 of the distance 565 | # between maximum value for September to November and y axis maximum 566 | text_x = dt.datetime.strptime(f"{self.year}-10-15", "%Y-%m-%d") 567 | max_value = super().get_min_max((258, 319)) 568 | text_y = max_value + (y_max - max_value) / 3 569 | 570 | else: 571 | # Position arrow 1/6 into the p05/p95 area in mid October 572 | arrow_x = dt.datetime.strptime(f"{self.year}-10-15", "%Y-%m-%d") 573 | idx = self.data[self.data["date"] == arrow_x].index[0] 574 | mean, p05 = self.data.iloc[idx]["mean"], self.data.iloc[idx]["p05"] 575 | arrow_y = p05 + (mean - p05) / 6 576 | 577 | # Position text center mid September at 1/3 of the distance 578 | # between minimum value for August to October and y axis minimum 579 | text_x = dt.datetime.strptime(f"{self.year}-09-15", "%Y-%m-%d") 580 | min_value = super().get_min_max((227, 288), which="min") 581 | text_y = min_value - (min_value - y_min) / 3 582 | 583 | fig.add_annotation( 584 | x=arrow_x, 585 | y=arrow_y, 586 | xref="x", 587 | yref="y", 588 | ax=text_x, 589 | ay=text_y, 590 | axref="x", 591 | ayref="y", 592 | text="90% of reference period
values fall within the gray area", 593 | showarrow=True, 594 | xanchor="center", 595 | yanchor="middle", 596 | arrowwidth=2, 597 | arrowcolor="#000", 598 | name="Reference period mean", 599 | ) 600 | 601 | # Annotations for percentile lines 602 | for percentile in ["p05", "p95"]: 603 | fig.add_annotation( 604 | x=self.data["date"].iloc[-1], 605 | y=self.data[percentile].iloc[-1], 606 | text=percentile.upper(), 607 | showarrow=False, 608 | xanchor="left", 609 | yanchor="middle", 610 | ) 611 | 612 | return fig 613 | 614 | def add_data_source(self, fig: go.Figure) -> go.Figure: 615 | """ 616 | Add data source to the plot. 617 | 618 | Parameters 619 | ---------- 620 | fig: go.Figure 621 | Plotly figure object. 622 | """ 623 | fig.add_annotation( 624 | xref="paper", 625 | yref="paper", 626 | name="Data source", 627 | x=1, 628 | y=-0.14, 629 | xanchor="right", 630 | showarrow=False, 631 | text="Data: open-meteo.com, OSM, " 632 | "License: CC by-sa-nc 4.0 " 633 | "Graph: Jan Kühn, https://yotka.org", 634 | opacity=0.5, 635 | font_size=12, 636 | ) 637 | 638 | return fig 639 | 640 | def add_data_info(self, fig: go.Figure) -> go.Figure: 641 | """ 642 | Add coordinates and last avalable date to the plot. 643 | 644 | Parameters 645 | ---------- 646 | fig: go.Figure 647 | Plotly figure object. 648 | """ 649 | if self.coords[0] is None or self.coords[1] is None: 650 | return fig 651 | 652 | last_date_text = ( 653 | f" (last date included: {self.last_date})" 654 | if self.year == dt.datetime.now().year 655 | else "" 656 | ) 657 | 658 | fig.add_annotation( 659 | xref="paper", 660 | yref="paper", 661 | name="Data info", 662 | x=0, 663 | y=-0.14, 664 | xanchor="left", 665 | showarrow=False, 666 | text=f"lat: {self.coords[0]}, lon: {self.coords[1]}{last_date_text}", 667 | opacity=0.5, 668 | font_size=12, 669 | ) 670 | 671 | return fig 672 | 673 | def layout(self, fig: go.Figure) -> go.Figure: 674 | """ 675 | Update layout options. 676 | 677 | Parameters 678 | ---------- 679 | fig: go.Figure 680 | Plotly figure object. 681 | """ 682 | 683 | fig.update_layout( 684 | title={ 685 | "text": ( 686 | f"{self.settings['metric']['title']} in {self.settings['location_name']} " 687 | f"{self.year}
{self.settings['metric']['subtitle']} " 688 | f"({self.reference_period[0]}-{self.reference_period[1]})" 689 | ), 690 | "font": {"family": "Lato, Arial, sans-serif", "size": 32, "color": "#1f1f1f"}, 691 | "x": 0.98, 692 | "y": 0.93, 693 | "xanchor": "right", 694 | "yanchor": "top", 695 | }, 696 | template="plotly_white", 697 | paper_bgcolor="#fff", 698 | plot_bgcolor="#fff", 699 | margin={"b": 70, "l": 60, "r": 20, "t": 100, "pad": 10}, 700 | hovermode="x", 701 | bargap=0, 702 | width=1000, 703 | height=600, 704 | font={"family": "Lato, Arial, sans-serif", "size": 14, "color": "#1f1f1f"}, 705 | xaxis={ 706 | "dtick": "M1", # Tick every month 707 | "hoverformat": "%e %B", 708 | "range": [f"{self.year-1}-12-20", f"{self.year+1}-01-10"], 709 | "showgrid": False, 710 | "tickformat": "%b", # Month name 711 | "ticklabelmode": "period", # Center tick labels 712 | }, 713 | yaxis={ 714 | "range": super().get_y_limits(), 715 | "showgrid": True, 716 | "ticksuffix": super().get_units(), 717 | }, 718 | ) 719 | 720 | # Update layout with user defined options 721 | if isinstance(self.layout_options, dict): 722 | fig.update_layout(self.layout_options) 723 | 724 | return fig 725 | 726 | def create_plot(self) -> tuple[go.Figure, str]: 727 | """ 728 | Creates the plot. 729 | """ 730 | 731 | # Create a new Figure object 732 | fig = go.Figure() 733 | 734 | # Plot percentile area 735 | fig = self.plot_percentile_area(fig) 736 | 737 | # Plot daily values 738 | fig = self.plot_diff(fig, chart_type="area") 739 | 740 | # Plot percentile lines 741 | fig = self.plot_percentile_lines(fig) 742 | 743 | # Plot the historical value for each day of the year 744 | fig = self.plot_mean(fig) 745 | 746 | # Add alternating background colors 747 | if self.settings["alternate_months"]["apply"]: 748 | fig = self.add_alternating_bg(fig) 749 | 750 | # Add annotations to explain the data 751 | fig = self.add_annotations(fig) 752 | 753 | # Annotate maximum values 754 | if self.settings["highlight_max"] > 0: 755 | fig = self.annotate_peaks(fig, how="max") 756 | 757 | # Annotate minimum values 758 | if self.settings["highlight_min"] > 0: 759 | fig = self.annotate_peaks(fig, how="min") 760 | 761 | # Add lat/lon and last date info 762 | fig = self.add_data_info(fig) 763 | 764 | # Data source and attribution 765 | fig = self.add_data_source(fig) 766 | 767 | # Update layout 768 | fig = self.layout(fig) 769 | 770 | # Save figure object as class attribute 771 | self.fig = fig 772 | 773 | # Save the plot to a file if requested 774 | file_path = self.save_plot_to_file() if self.settings["save_file"] else None 775 | 776 | return fig, file_path 777 | 778 | def save_plot_to_file(self) -> None: 779 | """ 780 | Save the plot to a file. 781 | """ 782 | file_path = super().create_file_path() 783 | 784 | if not isinstance(self.fig, go.Figure): 785 | self.fig = self.create_plot()[0] 786 | 787 | # Save the plot 788 | self.fig.write_image( 789 | file_path, 790 | width=1000, 791 | height=600, 792 | scale=2, 793 | ) 794 | 795 | # Clean up the output directory 796 | super().clean_output_dir() 797 | 798 | return file_path 799 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | extra_streamlit_components==0.1.71 2 | folium==0.16.0 3 | geopy==2.4.1 4 | kaleido==0.2.1 5 | nbformat==5.10.4 6 | numpy==2.1.1 7 | pandas==2.2.2 8 | plotly==5.22.0 9 | pydantic==2.9.1 10 | pytest==8.2.2 11 | python-dotenv==1.0.1 12 | Requests==2.32.3 13 | statsmodels==0.14.2 14 | streamlit==1.41.1 15 | streamlit_js_eval==0.1.5 16 | tenacity==8.3.0 17 | Unidecode==1.3.8 18 | -------------------------------------------------------------------------------- /sample_data/transformed/temperature_max-caracas-venezuela_transformed.csv: -------------------------------------------------------------------------------- 1 | dayofyear,min,p05,p40,mean,p60,p95,max,2024,2024_diff,date 2 | 1,21.9,23.073110572669627,24.96,24.984310193760177,25.68,26.842898325838586,27.5,24.2,-0.7843101937601773,2024-01-01 3 | 2,22.5,23.07651143996058,24.560000000000002,25.014809101408574,25.7,26.919036629061342,27.7,25.2,0.18519089859142568,2024-01-02 4 | 3,22.8,23.079863253315967,25.0,25.045574680862906,25.48,26.99497943611871,27.0,26.2,1.1544253191370935,2024-01-03 5 | 4,22.5,23.083267209466527,24.8,25.076577852347757,25.54,27.07058135207473,27.1,27.6,2.5234221476522443,2024-01-04 6 | 5,22.7,23.0868189918814,24.560000000000002,25.10777010018597,25.4,27.145740409891207,27.8,25.0,-0.10777010018597011,2024-01-05 7 | 6,21.7,23.09057655673206,24.7,25.13908063027652,25.34,27.220415243840442,27.2,25.1,-0.0390806302765192,2024-01-06 8 | 7,23.4,23.09455951793291,24.560000000000002,25.170427196101997,25.34,27.29458852368138,27.7,25.6,0.42957280389800445,2024-01-07 9 | 8,22.4,23.098751914884858,24.46,25.2017396957097,25.46,27.368279671900055,28.0,26.5,1.2982603042902987,2024-01-08 10 | 9,22.2,23.103059355407535,24.720000000000002,25.23294761038461,25.4,27.441505258644,27.7,28.6,3.3670523896153917,2024-01-09 11 | 10,22.4,23.107350921801117,24.76,25.264001422548947,25.4,27.51430670984422,28.4,27.2,1.935998577451052,2024-01-10 12 | 11,23.6,23.111496164117515,25.0,25.29489702021437,25.5,27.58675503306733,27.5,26.1,0.8051029797856302,2024-01-11 13 | 12,22.6,23.11527044784649,25.2,25.325705732749043,25.86,27.65897078731096,28.2,26.9,1.5742942672509557,2024-01-12 14 | 13,22.5,23.118199394854955,25.02,25.356577468994708,25.34,27.731207721251895,28.6,26.3,0.943422531005293,2024-01-13 15 | 14,22.1,23.119543925628534,25.2,25.387851039762936,25.54,27.803862114035855,28.4,25.9,0.512148960237063,2024-01-14 16 | 15,22.3,23.11835970331239,24.8,25.420460807881437,25.82,27.877087196947986,28.3,26.5,1.0795391921185633,2024-01-15 17 | 16,23.1,23.11926102711754,25.2,25.454083676060208,25.74,27.944932147809904,28.3,26.7,1.2459163239397917,2024-01-16 18 | 17,23.2,23.12282873294594,24.8,25.487145178723658,25.72,28.009710632746398,28.9,28.3,2.8128548212763427,2024-01-17 19 | 18,22.4,23.129970773991378,24.76,25.518788159848953,25.599999999999998,28.068225258715305,28.8,28.6,3.081211840151049,2024-01-18 20 | 19,22.7,23.140124676352123,25.400000000000002,25.548401374271236,26.04,28.117836564934905,28.5,29.0,3.4515986257287636,2024-01-19 21 | 20,23.0,23.152425523146206,25.220000000000002,25.575497081168493,25.98,28.157087455063166,28.4,28.6,3.0245029188315087,2024-01-20 22 | 21,23.0,23.16625769361779,25.220000000000002,25.59953229041154,26.439999999999998,28.185536467468477,29.0,28.5,2.9004677095884617,2024-01-21 23 | 22,22.0,23.180562980751198,24.86,25.620153538962644,25.72,28.203928757557964,28.6,27.8,2.179846461037357,2024-01-22 24 | 23,22.2,23.194050918678034,24.96,25.63723084252973,25.52,28.213354426142725,28.8,28.7,3.062769157470271,2024-01-23 25 | 24,22.1,23.206941428739864,25.26,25.651499076572314,25.76,28.215877233898553,29.1,27.7,2.048500923427685,2024-01-24 26 | 25,22.7,23.219868383988196,24.9,25.664608806359226,26.2,28.21544395874516,29.7,28.1,2.4353911936407755,2024-01-25 27 | 26,22.3,23.232610503601702,25.54,25.67846923235289,26.48,28.217157134090723,29.7,27.8,2.121530767647112,2024-01-26 28 | 27,22.9,23.244256875505503,25.78,25.694454846344,26.34,28.225083829489908,30.6,27.4,1.7055451536559971,2024-01-27 29 | 28,22.9,23.255195708708108,25.42,25.713349114339607,26.04,28.24134331293514,29.2,27.8,2.0866508856603936,2024-01-28 30 | 29,22.4,23.265572579692,24.86,25.735255132415094,26.06,28.26557808457406,29.7,28.2,2.464744867584905,2024-01-29 31 | 30,23.0,23.275054005213445,24.9,25.75949472142536,25.92,28.29497225396036,29.6,27.5,1.7405052785746413,2024-01-30 32 | 31,23.1,23.283332688456767,25.4,25.785501288971638,25.939999999999998,28.325598961868206,29.2,26.7,0.9144987110283616,2024-01-31 33 | 32,21.8,23.290117433559615,25.1,25.812892167775075,25.88,28.354902636038723,28.2,27.5,1.6871078322249247,2024-02-01 34 | 33,23.0,23.29556548124125,24.92,25.84181739179244,25.8,28.38303767710625,28.0,26.2,0.3581826082075601,2024-02-02 35 | 34,23.0,23.30069743551201,25.46,25.872508785846566,25.779999999999998,28.411361959993997,29.9,27.6,1.727491214153435,2024-02-03 36 | 35,22.7,23.305974858652043,25.1,25.90477216917578,26.1,28.442227093479023,27.7,26.5,0.5952278308242214,2024-02-04 37 | 36,22.8,23.31202066367674,25.26,25.93869289555351,26.26,28.477430972031588,28.7,30.3,4.361307104446492,2024-02-05 38 | 37,22.4,23.320408125100972,25.060000000000002,25.974810682338692,26.5,28.519069232831068,29.4,28.8,2.8251893176613088,2024-02-06 39 | 38,22.8,23.333447650410893,25.7,26.014678675376768,26.4,28.57009557114894,29.9,26.5,0.4853213246232322,2024-02-07 40 | 39,21.8,23.352832783535842,25.62,26.060082867188957,26.64,28.632467644382547,29.5,22.0,-4.060082867188957,2024-02-08 41 | 40,21.8,23.3789961395202,26.0,26.111546881628573,26.6,28.705201965680292,29.6,20.3,-5.811546881628573,2024-02-09 42 | 41,22.3,23.411470762981192,26.060000000000002,26.168032577960236,26.82,28.78589453344587,29.8,25.6,-0.5680325779602349,2024-02-10 43 | 42,20.9,23.448428427943195,26.060000000000002,26.227009705808303,27.08,28.872125468323397,29.1,26.3,0.07299029419169756,2024-02-11 44 | 43,21.5,23.487946409881697,25.66,26.285205120788156,26.74,28.960015661208814,29.2,27.1,0.8147948792118456,2024-02-12 45 | 44,22.2,23.528128093093287,26.26,26.3400627193887,26.939999999999998,29.044693943579645,29.7,28.2,1.8599372806112981,2024-02-13 46 | 45,22.8,23.568304607855012,25.86,26.390645125751895,26.54,29.121584857097965,30.9,28.0,1.6093548742481047,2024-02-14 47 | 46,22.3,23.60923251794104,25.92,26.437627713913443,26.58,29.188585365665872,29.8,29.6,3.1623722860865584,2024-02-15 48 | 47,22.6,23.652619165394185,25.9,26.482329344210395,27.0,29.245935659808907,29.6,30.2,3.717670655789604,2024-02-16 49 | 48,23.1,23.700263584253808,26.66,26.525688972319962,27.52,29.294955223398187,30.7,29.9,3.3743110276800365,2024-02-17 50 | 49,23.7,23.7535104990142,26.62,26.568064589610128,27.54,29.3374487078909,29.6,30.7,4.131935410389872,2024-02-18 51 | 50,24.0,23.812872929779278,26.36,26.60964247149857,27.0,29.375290806094778,30.3,29.4,2.7903575285014277,2024-02-19 52 | 51,24.2,23.87744515049705,26.6,26.65067329917456,27.4,29.410619055087338,30.2,28.0,1.34932670082544,2024-02-20 53 | 52,24.5,23.945941254431755,26.48,26.691584405947836,27.24,29.445123067970794,30.3,24.9,-1.7915844059478374,2024-02-21 54 | 53,23.6,24.01677361144515,26.52,26.732878861336903,27.04,29.480262254275843,31.4,25.1,-1.632878861336902,2024-02-22 55 | 54,22.9,24.088022697192304,26.66,26.774553970507974,27.8,29.516196171874594,30.4,24.7,-2.0745539705079743,2024-02-23 56 | 55,23.4,24.156498333732653,26.02,26.815785420896866,27.279999999999998,29.551741740871606,29.5,26.5,-0.3157854208968658,2024-02-24 57 | 56,21.5,24.219162558133622,26.42,26.85549432801737,27.439999999999998,29.585263316641562,29.4,25.6,-1.2554943280173703,2024-02-25 58 | 57,23.2,24.273435546327956,26.46,26.89266142524444,26.84,29.615894519673056,30.4,25.9,-0.9926614252444423,2024-02-26 59 | 58,23.6,24.318644038845893,26.36,26.927298598090857,27.1,29.64416983384933,29.7,25.1,-1.8272985980908558,2024-02-27 60 | 59,23.8,24.356197847558654,26.26,26.96069587216252,27.42,29.671317368968353,30.9,25.1,-1.8606958721625197,2024-02-28 61 | 60,24.8,24.388313951243596,27.0,26.99463742768154,27.34,29.698877062398218,30.0,24.2,-2.7946374276815398,2024-03-01 62 | 61,23.3,24.417390732257005,26.7,27.030714046186173,27.52,29.72800203917167,31.1,23.8,-3.230714046186172,2024-03-02 63 | 62,21.2,24.44567021221126,26.5,27.069280978983773,27.5,29.760123774973945,30.2,24.6,-2.4692809789837717,2024-03-03 64 | 63,22.7,24.47532146939781,26.74,27.109549606302846,27.42,29.796869365127677,30.3,26.0,-1.1095496063028456,2024-03-04 65 | 64,23.1,24.50710447805628,26.82,27.15038937559729,27.3,29.83955077427944,30.3,27.3,0.14961062440271178,2024-03-05 66 | 65,22.2,24.54023720653123,26.9,27.190930768247615,27.58,29.88905155026167,29.8,28.5,1.3090692317523853,2024-03-06 67 | 66,22.3,24.573585645134784,26.720000000000002,27.231184358291944,27.34,29.94515159326948,30.6,29.7,2.468815641708055,2024-03-07 68 | 67,23.3,24.605239838621777,26.66,27.271466798211115,27.36,30.00721962415066,30.1,30.3,3.0285332017888855,2024-03-08 69 | 68,23.8,24.63281053217813,26.86,27.311955121201056,27.58,30.074131139242365,31.0,28.9,1.588044878798943,2024-03-09 70 | 69,23.3,24.654910987010734,27.04,27.35276903965566,27.58,30.144701261743535,31.6,29.9,2.54723096034434,2024-03-10 71 | 70,24.2,24.67180072222972,27.560000000000002,27.39421926644078,28.08,30.217641965232318,31.2,29.0,1.6057807335592216,2024-03-11 72 | 71,22.1,24.6850225065572,27.48,27.436742651227018,28.24,30.29161503715877,30.1,28.9,1.4632573487729807,2024-03-12 73 | 72,23.4,24.696297022562568,27.560000000000002,27.480039606839046,27.98,30.36464593646215,30.3,29.0,1.5199603931609538,2024-03-13 74 | 73,24.1,24.706677278318715,27.14,27.523255318388834,28.04,30.43486587676403,31.4,28.8,1.2767446816111665,2024-03-14 75 | 74,23.9,24.71579026625192,26.9,27.565094138788236,27.7,30.500807237518337,31.2,29.4,1.8349058612117624,2024-03-15 76 | 75,24.0,24.722364885237017,27.220000000000002,27.60413728812062,27.74,30.562274973289565,31.7,28.5,0.8958627118793814,2024-03-16 77 | 76,24.6,24.726303568056395,26.4,27.639855893913673,27.22,30.62030226744067,31.9,29.9,2.260144106086326,2024-03-17 78 | 77,23.8,24.729117709411703,26.8,27.672706192579252,28.1,30.675743730740884,30.9,31.8,4.127293807420749,2024-03-18 79 | 78,24.4,24.731096515648865,26.96,27.702911047940773,27.98,30.728418509288417,31.8,31.8,4.097088952059227,2024-03-19 80 | 79,23.5,24.73230839965597,26.96,27.730588049097715,28.7,30.777608511258165,32.1,31.4,3.6694119509022833,2024-03-20 81 | 80,24.1,24.733859798834384,27.76,27.755994843094243,28.62,30.822092936748334,31.4,27.9,0.14400515690575588,2024-03-21 82 | 81,23.9,24.73629567156351,27.46,27.779544374288182,28.8,30.861078317274693,31.6,28.2,0.42045562571181705,2024-03-22 83 | 82,23.7,24.740051352850116,27.66,27.802066194715664,29.34,30.893384726451508,31.3,28.6,0.7979338052843374,2024-03-23 84 | 83,24.0,24.745608860947875,28.400000000000002,27.824612762901292,29.14,30.918770999668862,32.0,29.2,1.3753872370987068,2024-03-24 85 | 84,24.2,24.753433649030832,28.24,27.847965364078163,28.74,30.937898704535215,31.4,28.3,0.45203463592183724,2024-03-25 86 | 85,24.1,24.76371441934081,27.1,27.87191451168406,27.979999999999997,30.95217951170962,32.0,27.7,-0.17191451168406147,2024-03-26 87 | 86,23.2,24.77688113828918,27.560000000000002,27.895292285105164,28.84,30.96310792050758,31.8,27.2,-0.6952922851051646,2024-03-27 88 | 87,24.0,24.79335223561162,27.46,27.916770632567506,28.68,30.97156631353286,32.0,26.9,-1.016770632567507,2024-03-28 89 | 88,23.2,24.812356563116385,28.0,27.935092611966038,28.439999999999998,30.97745583090191,31.9,25.7,-2.2350926119660386,2024-03-29 90 | 89,24.3,24.832242167431083,27.46,27.949668825068933,28.14,30.980615989030355,32.6,26.5,-1.4496688250689331,2024-03-30 91 | 90,24.6,24.85114550242294,27.86,27.960930686482303,28.34,30.981115661316515,33.1,26.1,-1.8609306864823019,2024-03-31 92 | 91,22.6,24.86756659485323,27.220000000000002,27.969707129398653,28.54,30.97943038286667,32.9,25.6,-2.369707129398652,2024-04-01 93 | 92,24.0,24.88122054082553,27.42,27.977302024878203,28.779999999999998,30.976672963364436,31.1,25.7,-2.277302024878203,2024-04-02 94 | 93,22.0,24.893954212600836,27.32,27.98524268280798,28.62,30.973507946952193,30.9,26.4,-1.58524268280798,2024-04-03 95 | 94,22.7,24.906390215369395,27.82,27.994356375769616,28.7,30.970466726038833,31.7,29.3,1.3056436242303846,2024-04-04 96 | 95,24.4,24.91803835505515,27.36,28.00421312147358,28.62,30.96787128243041,31.1,29.3,1.295786878526421,2024-04-05 97 | 96,25.5,24.928823998662697,27.66,28.014258850308945,28.639999999999997,30.96598488188875,31.4,30.4,2.3857411496910537,2024-04-06 98 | 97,24.1,24.9378660559413,27.76,28.023649188477243,28.34,30.964155913170092,31.9,30.9,2.876350811522755,2024-04-07 99 | 98,24.7,24.943670810009046,27.64,28.03157703939457,28.38,30.961473445083037,32.6,31.5,3.46842296060543,2024-04-08 100 | 99,25.1,24.943577218858252,27.48,28.036799402851706,28.14,30.95668011403702,31.9,29.7,1.6632005971482933,2024-04-09 101 | 100,25.1,24.935104477404657,27.54,28.038097571946384,28.2,30.9487679798347,31.8,28.7,0.6619024280536152,2024-04-10 102 | 101,24.6,24.917259817343034,27.720000000000002,28.035123618273364,28.56,30.937364521504044,31.9,28.4,0.36487638172663495,2024-04-11 103 | 102,24.6,24.889630814010644,28.02,28.027321311697815,28.68,30.921764253612256,31.1,28.6,0.5726786883021866,2024-04-12 104 | 103,23.7,24.853366523221048,27.86,28.014436495188953,28.58,30.902371655128363,31.2,28.8,0.7855635048110479,2024-04-13 105 | 104,24.1,24.810221900056177,27.6,27.996571797025535,28.439999999999998,30.878998396813568,31.2,26.8,-1.196571797025534,2024-04-14 106 | 105,24.0,24.763106595513328,27.82,27.97399619678712,28.88,30.85037046957663,32.4,27.9,-0.073996196787121,2024-04-15 107 | 106,24.3,24.714239789747793,27.42,27.947177595804455,28.88,30.81530019181313,32.2,30.2,2.2528224041955447,2024-04-16 108 | 107,24.0,24.6650157690241,27.96,27.916997183697443,28.8,30.773770573543246,32.4,27.6,-0.3169971836974419,2024-04-17 109 | 108,21.4,24.616636742301477,27.46,27.88374509091748,28.18,30.726612673545876,31.7,27.0,-0.8837450909174791,2024-04-18 110 | 109,23.7,24.57028356819205,27.88,27.8474557960714,29.02,30.67546666775027,32.2,26.0,-1.8474557960714009,2024-04-19 111 | 110,24.1,24.525620188278804,27.38,27.80839546128174,28.22,30.622477600421767,31.1,25.4,-2.408395461281742,2024-04-20 112 | 111,24.5,24.481704522636996,27.4,27.767321553460913,28.939999999999998,30.5693872665292,31.2,26.3,-1.4673215534609128,2024-04-21 113 | 112,22.0,24.44008745923872,27.32,27.72500958227325,28.46,30.517316556749954,30.6,28.2,0.474990417726751,2024-04-22 114 | 113,22.6,24.403375949704753,27.46,27.682213265984846,28.02,30.466595378328595,30.5,27.1,-0.5822132659848442,2024-04-23 115 | 114,21.8,24.372628771975037,26.76,27.639372534575802,28.0,30.417605556885583,30.6,27.9,0.26062746542419646,2024-04-24 116 | 115,22.5,24.347979689071757,27.2,27.597106323991706,28.04,30.370875280441687,30.8,27.3,-0.2971063239917058,2024-04-25 117 | 116,24.9,24.32849572974068,27.68,27.556260122089466,28.04,30.32693223658552,31.2,27.2,-0.35626012208946634,2024-04-26 118 | 117,23.6,24.31452861895233,27.16,27.517823911351712,28.24,30.28634587994716,31.3,26.2,-1.317823911351713,2024-04-27 119 | 118,24.7,24.307525712678572,27.16,27.482312889697806,27.76,30.249177863260403,30.9,24.9,-2.582312889697807,2024-04-28 120 | 119,21.5,24.30910167360922,27.16,27.449953698277263,28.08,30.21388509343265,30.4,25.6,-1.849953698277261,2024-04-29 121 | 120,21.4,24.318026131843897,26.82,27.420297910494202,28.08,30.178607201675135,30.2,26.2,-1.220297910494203,2024-04-30 122 | 121,23.4,24.332197920573602,27.02,27.392424139125144,27.66,30.14198204734521,30.2,29.2,1.8075758608748558,2024-05-01 123 | 122,23.8,24.351192452553917,26.6,27.365608870797445,27.48,30.10366577364226,30.3,28.6,1.2343911292025567,2024-05-02 124 | 123,23.0,24.37482757244883,26.36,27.339308096252353,27.96,30.063222817512695,30.8,28.9,1.5606919037476459,2024-05-03 125 | 124,22.8,24.401606342212634,27.0,27.313336377044322,28.08,30.020060485216852,31.5,28.7,1.3866636229556768,2024-05-04 126 | 125,24.1,24.43015983979666,26.62,27.28774807658707,27.6,29.973559033811163,30.3,27.7,0.4122519234129278,2024-05-05 127 | 126,23.9,24.459286818385742,26.82,27.26259946675954,27.54,29.924020696172335,30.9,27.3,0.03740053324046144,2024-05-06 128 | 127,24.9,24.487886339695464,27.060000000000002,27.237675232848233,27.939999999999998,29.87266017036199,30.4,27.8,0.5623247671517682,2024-05-07 129 | 128,23.6,24.515312653101475,27.1,27.213313562403602,27.96,29.821618549469754,30.5,28.5,1.2866864375963978,2024-05-08 130 | 129,21.0,24.5426130391804,26.76,27.19030802287065,27.68,29.772806256834038,31.2,27.9,0.7096919771293493,2024-05-09 131 | 130,23.7,24.569698195909726,27.3,27.16893467104952,27.7,29.726693687872345,30.3,27.5,0.3310653289504799,2024-05-10 132 | 131,25.0,24.596014863621008,27.1,27.1487149330113,27.779999999999998,29.682045857191294,30.7,28.3,1.1512850669887023,2024-05-11 133 | 132,23.3,24.620528377443154,26.8,27.128526447051485,27.34,29.63668232935942,30.1,27.5,0.37147355294851536,2024-05-12 134 | 133,23.6,24.642665394666125,26.48,27.107183029804997,27.279999999999998,29.58906220794669,31.0,29.9,2.792816970195002,2024-05-13 135 | 134,22.1,24.66073260962852,26.7,27.083914144112594,27.34,29.5395890952643,29.4,29.7,2.6160858558874054,2024-05-14 136 | 135,23.4,24.67320263858214,26.9,27.058834965043314,27.38,29.489476174289056,29.6,27.4,0.3411650349566848,2024-05-15 137 | 136,22.5,24.678823055394513,26.86,27.032312688034864,27.14,29.439537882868365,29.9,27.2,0.16768731196513542,2024-05-16 138 | 137,22.7,24.678191892379928,26.36,27.005282943090165,27.14,29.3906828425568,29.4,27.6,0.5947170569098361,2024-05-17 139 | 138,24.7,24.673897341251934,26.7,26.979040517586974,26.939999999999998,29.343501947494786,29.0,27.7,0.7209594824130257,2024-05-18 140 | 139,24.2,24.66932416094969,27.12,26.95463696619492,27.58,29.29775383756272,30.1,29.3,2.345363033805082,2024-05-19 141 | 140,22.7,24.666435977743113,26.060000000000002,26.932259360509846,27.24,29.254196118912688,30.2,27.8,0.8677406394901546,2024-05-20 142 | 141,23.7,24.665170022302725,26.220000000000002,26.911623634637387,26.88,29.214168783044723,30.7,26.9,-0.011623634637388136,2024-05-21 143 | 142,22.3,24.664843359930018,26.76,26.892409068253034,27.2,29.17882199732285,30.2,28.7,1.8075909317469652,2024-05-22 144 | 143,24.1,24.663914627408264,26.88,26.874053163201662,27.279999999999998,29.14787242223809,29.5,26.7,-0.17405316320166264,2024-05-23 145 | 144,24.1,24.660725340578704,26.46,26.855672877882498,27.34,29.11985314989508,28.7,27.8,0.9443271221175031,2024-05-24 146 | 145,24.2,24.654716818197418,26.5,26.836468031591927,27.14,29.092167706904004,30.3,28.0,1.1635319684080727,2024-05-25 147 | 146,22.8,24.646178803579954,26.5,26.81573610106355,26.92,29.06209607062431,30.3,26.1,-0.7157361010635483,2024-05-26 148 | 147,21.8,24.634447401345096,26.58,26.7929289550417,27.279999999999998,29.027359312506572,30.7,28.2,1.4070710449583004,2024-05-27 149 | 148,22.4,24.619999973628563,27.0,26.76815847041535,27.64,28.987119556040966,30.0,28.6,1.8318415295846506,2024-05-28 150 | 149,21.7,24.605006618065385,26.36,26.742031849330573,27.24,28.941157689897835,29.3,28.2,1.4579681506694264,2024-05-29 151 | 150,22.2,24.59040071372869,26.6,26.7151901455802,27.14,28.889553311532723,29.6,26.1,-0.6151901455801969,2024-05-30 152 | 151,25.3,24.57548870447796,26.6,26.6877975443275,27.4,28.83295638784206,30.7,,,2024-05-31 153 | 152,23.1,24.560205763497315,26.52,26.660008255021353,27.1,28.77245206951114,29.1,,,2024-06-01 154 | 153,24.1,24.545043618325142,26.16,26.63220241262337,27.0,28.709671934319363,29.2,,,2024-06-02 155 | 154,24.1,24.52882150185037,26.0,26.604300698810814,26.88,28.646587344656087,29.1,,,2024-06-03 156 | 155,23.9,24.510376264837298,26.6,26.575846794710422,27.1,28.58408302952849,29.6,,,2024-06-04 157 | 156,23.5,24.489782578346244,26.46,26.546872884440205,26.939999999999998,28.521893304304598,29.1,,,2024-06-05 158 | 157,23.8,24.467841146144327,26.16,26.517337197967315,26.7,28.4590225507626,28.5,,,2024-06-06 159 | 158,23.2,24.445474440537122,26.28,26.48684137150556,26.8,28.394563354016654,30.2,,,2024-06-07 160 | 159,24.3,24.42464688251093,25.9,26.455505569400593,26.7,28.328512240170635,30.4,,,2024-06-08 161 | 160,23.1,24.407550490581496,25.9,26.424242961325877,26.48,28.26166954481757,30.4,,,2024-06-09 162 | 161,24.0,24.395346301426706,26.560000000000002,26.39414577275802,27.1,28.19506971285796,29.4,,,2024-06-10 163 | 162,24.2,24.38855114730833,26.0,26.366406260300565,26.3,28.13028504411793,29.1,,,2024-06-11 164 | 163,23.4,24.387817723758406,26.4,26.341725945367045,26.939999999999998,28.068598748324806,27.9,,,2024-06-12 165 | 164,22.6,24.39289827086457,26.42,26.31988440138088,26.8,28.010647672651253,28.0,,,2024-06-13 166 | 165,23.5,24.401737239137,26.0,26.300156045520367,26.54,27.95670164309736,28.2,,,2024-06-14 167 | 166,24.1,24.413027868937167,26.5,26.282061778044294,26.84,27.906490617428254,28.2,,,2024-06-15 168 | 167,23.0,24.427209277900698,26.2,26.265693303971226,26.439999999999998,27.860005009577183,28.4,,,2024-06-16 169 | 168,23.3,24.44536519451091,25.9,26.251234732014474,26.5,27.817552021040697,27.8,,,2024-06-17 170 | 169,24.9,24.466941908373112,25.8,26.238593447324526,26.3,27.77824967814976,28.4,,,2024-06-18 171 | 170,24.1,24.490088104317437,26.1,26.227370620173648,26.439999999999998,27.74075431366951,28.2,,,2024-06-19 172 | 171,23.5,24.512796662964565,25.76,26.21722814807445,26.279999999999998,27.704341132874873,27.5,,,2024-06-20 173 | 172,23.3,24.534713062331186,26.0,26.208268639970626,26.4,27.669574383757745,27.5,,,2024-06-21 174 | 173,23.5,24.55513327783209,26.060000000000002,26.200592298498044,26.5,27.63706359145335,27.7,,,2024-06-22 175 | 174,24.0,24.57402467641579,25.9,26.19437559935492,26.54,27.607698144031932,28.7,,,2024-06-23 176 | 175,23.6,24.59193460051504,26.16,26.189780504786043,26.48,27.581425962535423,28.4,,,2024-06-24 177 | 176,24.7,24.60954267459663,25.86,26.186818899677544,26.38,27.557546035061044,28.1,,,2024-06-25 178 | 177,23.7,24.626706862966575,25.8,26.18509479141342,26.439999999999998,27.535559272990948,28.2,,,2024-06-26 179 | 178,23.6,24.64305787227376,25.82,26.18399738806161,26.36,27.51522378265496,27.3,,,2024-06-27 180 | 179,23.5,24.658471117067563,26.0,26.182916954269352,26.5,27.49682895438302,27.6,,,2024-06-28 181 | 180,24.8,24.672457108159882,26.36,26.181631841133655,26.5,27.48079983535213,27.7,,,2024-06-29 182 | 181,24.3,24.684549138714814,26.060000000000002,26.18000455568921,26.3,27.467091223083305,28.1,,,2024-06-30 183 | 182,23.5,24.69377753031516,26.12,26.177677998158238,26.6,27.455068882297624,27.5,,,2024-07-01 184 | 183,24.2,24.7001550704912,26.0,26.17457669188746,26.439999999999998,27.443736455717403,27.3,,,2024-07-02 185 | 184,23.7,24.70434326952321,26.26,26.17090377958006,26.7,27.432379862434367,27.5,,,2024-07-03 186 | 185,24.7,24.706754079162863,26.2,26.166848662461547,26.54,27.420470518674954,28.3,,,2024-07-04 187 | 186,24.0,24.708841790212364,25.66,26.162697162625992,26.3,27.40814725452596,28.4,,,2024-07-05 188 | 187,23.9,24.71147761567523,25.96,26.158560854295427,26.5,27.396230390204142,28.0,,,2024-07-06 189 | 188,24.5,24.715283434492168,26.060000000000002,26.154359322596015,26.4,27.385174005820065,27.7,,,2024-07-07 190 | 189,24.3,24.720158276560625,26.16,26.149822715634787,26.34,27.374825559944412,27.2,,,2024-07-08 191 | 190,23.3,24.726077394922285,26.04,26.14486199812718,26.74,27.365008564889646,28.7,,,2024-07-09 192 | 191,23.3,24.7330705569219,25.82,26.139782121527396,26.34,27.355687730487958,27.8,,,2024-07-10 193 | 192,23.4,24.740489572456514,25.9,26.13495509141738,26.4,27.346965995036385,27.8,,,2024-07-11 194 | 193,23.5,24.747604804050166,26.060000000000002,26.13078992602985,26.3,27.33907566019333,28.6,,,2024-07-12 195 | 194,23.1,24.75421268419399,25.96,26.12783511540226,26.279999999999998,27.332853170232706,27.4,,,2024-07-13 196 | 195,24.4,24.75951902300959,26.16,26.12628181714629,26.3,27.328500089351795,27.7,,,2024-07-14 197 | 196,24.1,24.762979978935075,26.26,26.1262642382814,26.439999999999998,27.325766105382858,27.3,,,2024-07-15 198 | 197,24.6,24.765342796346285,25.98,26.12814153284608,26.34,27.324288332283334,27.2,,,2024-07-16 199 | 198,23.9,24.76832523220541,26.1,26.132006399248716,26.4,27.323710618669026,27.5,,,2024-07-17 200 | 199,25.2,24.773206659973727,26.02,26.137793951983785,26.3,27.32383598042013,27.2,,,2024-07-18 201 | 200,23.6,24.78144235569919,25.76,26.14549263634082,26.14,27.32465424122559,27.7,,,2024-07-19 202 | 201,22.7,24.79368209451886,26.060000000000002,26.15526774535212,26.4,27.32650514928466,27.7,,,2024-07-20 203 | 202,24.0,24.81063427429566,26.220000000000002,26.16759733161221,26.5,27.330282541301187,27.2,,,2024-07-21 204 | 203,24.7,24.83152818890207,26.060000000000002,26.1826030545522,26.34,27.33715487199442,27.6,,,2024-07-22 205 | 204,24.6,24.85438033306722,26.060000000000002,26.19986723746874,26.54,27.34806087106499,27.5,,,2024-07-23 206 | 205,24.5,24.877875946253432,25.8,26.218971518175454,26.5,27.36341792911732,28.2,,,2024-07-24 207 | 206,23.6,24.900363248576635,26.1,26.239454807637994,26.3,27.382721523064493,27.5,,,2024-07-25 208 | 207,24.5,24.92080159222895,26.16,26.260996130709724,26.4,27.404661034135998,27.7,,,2024-07-26 209 | 208,23.1,24.93914854752819,26.16,26.283106737284946,26.54,27.427541085258312,27.3,,,2024-07-27 210 | 209,24.4,24.956761189034964,26.2,26.305370101981282,26.54,27.450048389278347,27.9,,,2024-07-28 211 | 210,25.0,24.973927817449304,26.36,26.327430534640236,26.64,27.471601721995665,27.6,,,2024-07-29 212 | 211,23.9,24.990545908672132,26.16,26.348837851806344,26.5,27.49178178777988,28.1,,,2024-07-30 213 | 212,23.8,25.00627378794181,26.2,26.369182142432344,26.6,27.51030152129793,28.2,,,2024-07-31 214 | 213,25.1,25.020544526108598,26.36,26.388391509355095,26.74,27.527983922994988,28.0,,,2024-08-01 215 | 214,24.6,25.032586608650856,26.46,26.406503586386588,26.84,27.545870044507645,28.4,,,2024-08-02 216 | 215,24.7,25.042507399311674,26.32,26.423444829667307,26.68,27.564156257258233,28.0,,,2024-08-03 217 | 216,24.1,25.0516790576201,26.26,26.439409546631996,26.62,27.58219877050926,28.6,,,2024-08-04 218 | 217,24.3,25.060889712551738,26.48,26.454403688017194,26.84,27.599451745048718,27.9,,,2024-08-05 219 | 218,25.5,25.070644266171996,26.26,26.468301397032853,26.54,27.615281580815196,28.0,,,2024-08-06 220 | 219,23.0,25.080780108184396,26.46,26.48106743326385,26.7,27.629031291169714,28.1,,,2024-08-07 221 | 220,25.0,25.090370760579752,26.3,26.49268755732399,26.82,27.6408160118351,27.8,,,2024-08-08 222 | 221,24.4,25.098526435202448,26.4,26.503353178594597,26.64,27.651141236678896,27.4,,,2024-08-09 223 | 222,24.1,25.105653243361584,26.36,26.513578374685657,26.7,27.66087923741398,28.5,,,2024-08-10 224 | 223,23.1,25.113123667318888,26.4,26.523949782753473,26.74,27.67074840973968,28.2,,,2024-08-11 225 | 224,22.7,25.122696924129382,26.6,26.53514579300085,26.9,27.681685447997722,28.0,,,2024-08-12 226 | 225,24.5,25.135696573675954,25.96,26.547707996607993,26.34,27.694338870091656,27.8,,,2024-08-13 227 | 226,25.0,25.15275489675513,26.52,26.56197363784667,26.9,27.709628716322072,28.2,,,2024-08-14 228 | 227,22.1,25.17400660395234,26.560000000000002,26.578226755549895,27.14,27.728834757849384,28.5,,,2024-08-15 229 | 228,25.2,25.199801965035782,26.220000000000002,26.596507838150995,26.6,27.752400964809187,27.6,,,2024-08-16 230 | 229,25.3,25.229423049752263,26.36,26.61623588332251,26.84,27.77941286261305,27.6,,,2024-08-17 231 | 230,23.7,25.26156195542173,26.3,26.636652318408146,26.7,27.808474821939036,27.7,,,2024-08-18 232 | 231,25.2,25.295414326233303,26.36,26.657264105720984,26.8,27.83815845647475,28.2,,,2024-08-19 233 | 232,25.0,25.330595924682186,26.560000000000002,26.67783599893542,27.1,27.86729133219142,28.4,,,2024-08-20 234 | 233,25.2,25.36698869124191,26.46,26.69822217055646,26.7,27.89531971327029,28.5,,,2024-08-21 235 | 234,24.4,25.403082511662976,26.66,26.717988475770067,27.04,27.92165161276059,28.1,,,2024-08-22 236 | 235,24.0,25.437101029261825,26.86,26.73664111500476,27.24,27.946088497102934,28.3,,,2024-08-23 237 | 236,25.0,25.467231661467544,26.5,26.75387244458409,27.24,27.969283725361915,28.4,,,2024-08-24 238 | 237,25.5,25.492661298511248,26.9,26.769793067236456,27.34,27.99180343455785,28.3,,,2024-08-25 239 | 238,25.5,25.51321868338795,26.86,26.78479278451347,27.2,28.013539851063246,28.1,,,2024-08-26 240 | 239,23.9,25.529750692760096,26.7,26.799335401657117,27.04,28.034342789124597,28.6,,,2024-08-27 241 | 240,23.6,25.54246416455506,26.66,26.813537407604098,26.9,28.053950954527323,28.4,,,2024-08-28 242 | 241,25.6,25.55129573034686,26.86,26.827074959263186,27.2,28.071967353113685,28.8,,,2024-08-29 243 | 242,25.4,25.557495072766073,26.58,26.83948521372601,26.939999999999998,28.086963809527862,28.2,,,2024-08-30 244 | 243,25.1,25.563090292167452,26.560000000000002,26.850617096880388,26.84,28.09793102839893,28.5,,,2024-08-31 245 | 244,25.4,25.569129594422755,26.6,26.860588803594226,27.2,28.10483438905332,28.3,,,2024-09-01 246 | 245,24.5,25.57588143048932,26.8,26.869496722835745,27.2,28.10812744469234,28.6,,,2024-09-02 247 | 246,24.5,25.58250133396846,26.6,26.877643573507143,26.84,28.109292453703528,29.2,,,2024-09-03 248 | 247,24.8,25.587681358466217,26.8,26.885453974568446,26.9,28.110403641257758,28.5,,,2024-09-04 249 | 248,24.7,25.590510174056117,26.6,26.893301806821164,27.04,28.113515375102125,28.3,,,2024-09-05 250 | 249,23.9,25.59111025933063,26.8,26.90155487393024,27.1,28.119889429159965,28.4,,,2024-09-06 251 | 250,24.5,25.590433207156238,26.7,26.91051066189165,27.1,28.129891109703316,28.7,,,2024-09-07 252 | 251,25.1,25.58934157990389,26.9,26.92012577439681,27.34,28.14257550997881,28.1,,,2024-09-08 253 | 252,24.8,25.587686927497792,26.9,26.930002322070727,27.3,28.156379245238487,28.2,,,2024-09-09 254 | 253,24.3,25.585335702763647,26.96,26.93956283914019,27.3,28.169792210827186,28.2,,,2024-09-10 255 | 254,24.1,25.58217936168518,26.8,26.948405858810872,27.1,28.18197835708455,28.4,,,2024-09-11 256 | 255,25.0,25.578996426930964,26.8,26.956464499414096,27.22,28.192341162858487,28.0,,,2024-09-12 257 | 256,24.5,25.57716069695356,26.92,26.963842225437848,27.4,28.200926853244766,28.5,,,2024-09-13 258 | 257,25.3,25.57738500886661,26.98,26.970691883135864,27.2,28.2088795873448,29.0,,,2024-09-14 259 | 258,24.5,25.579498843647166,26.82,26.97721815506172,27.2,28.21755962571152,28.3,,,2024-09-15 260 | 259,23.8,25.582420286686915,26.7,26.983526580345167,27.14,28.228001804832175,29.0,,,2024-09-16 261 | 260,25.1,25.584763135405343,26.8,26.98947162730543,27.1,28.241230268315398,29.0,,,2024-09-17 262 | 261,25.3,25.58585656147187,26.720000000000002,26.99464630472679,27.14,28.257325027822674,29.3,,,2024-09-18 263 | 262,24.6,25.584425783021835,26.66,26.998409752240644,27.14,28.27504469908043,28.7,,,2024-09-19 264 | 263,25.4,25.5799426939275,26.8,27.000254315761925,27.34,28.292887975608956,28.7,,,2024-09-20 265 | 264,24.8,25.572714408955385,27.02,27.000253657159565,27.38,28.309532272799792,28.5,,,2024-09-21 266 | 265,25.1,25.563077040705068,26.58,26.998848293606276,27.439999999999998,28.32413135464425,28.4,,,2024-09-22 267 | 266,24.6,25.55019317210839,26.8,26.99605331132975,27.1,28.33624268575196,28.1,,,2024-09-23 268 | 267,24.8,25.53343311453669,26.96,26.991600364513268,27.2,28.345387815081608,28.5,,,2024-09-24 269 | 268,24.8,25.512854625221863,26.96,26.985529716634368,27.4,28.351278107242543,29.0,,,2024-09-25 270 | 269,24.1,25.489761809890535,26.96,26.978362362432602,27.279999999999998,28.353996690228044,28.5,,,2024-09-26 271 | 270,26.0,25.46568394057047,27.16,26.970834723761826,27.5,28.35394147445194,29.2,,,2024-09-27 272 | 271,23.7,25.44123916270308,27.0,26.96330573382302,27.54,28.35191106794111,29.2,,,2024-09-28 273 | 272,24.9,25.41658374458477,26.76,26.95601561719276,27.439999999999998,28.348657381406046,29.3,,,2024-09-29 274 | 273,25.4,25.391076109127777,26.8,26.94907553583767,27.18,28.345019281786044,28.8,,,2024-09-30 275 | 274,24.8,25.364606308058203,26.560000000000002,26.94258867411125,26.939999999999998,28.341538099440335,28.6,,,2024-10-01 276 | 275,24.8,25.338309668671243,26.560000000000002,26.936507494385673,26.8,28.337628720013303,28.4,,,2024-10-02 277 | 276,23.9,25.312893634252596,26.560000000000002,26.930463532026096,27.1,28.332242141869973,28.3,,,2024-10-03 278 | 277,24.5,25.287996304120025,26.92,26.92382475041388,27.24,28.324484379973082,28.5,,,2024-10-04 279 | 278,25.6,25.26240259945794,26.82,26.9159685844739,27.4,28.31374738962046,28.8,,,2024-10-05 280 | 279,24.4,25.234963609761706,26.66,26.90644565605172,27.2,28.3000320063982,28.6,,,2024-10-06 281 | 280,24.4,25.20611825273128,26.560000000000002,26.895326974018328,27.0,28.28422999680823,28.4,,,2024-10-07 282 | 281,24.0,25.177756326727078,26.6,26.883038480140222,27.3,28.267665415079673,28.5,,,2024-10-08 283 | 282,24.8,25.151504298019265,26.8,26.87010518729911,27.2,28.251720027882584,28.2,,,2024-10-09 284 | 283,24.5,25.12858745964685,27.060000000000002,26.856947652327616,27.2,28.237306814148077,28.4,,,2024-10-10 285 | 284,24.3,25.109911363961125,26.66,26.843881321399188,27.04,28.225005363395457,29.1,,,2024-10-11 286 | 285,24.8,25.09528051025392,26.76,26.831050980735487,27.38,28.214506030494285,28.7,,,2024-10-12 287 | 286,22.4,25.08368861194403,26.88,26.818139568222666,27.4,28.204453420270724,28.0,,,2024-10-13 288 | 287,24.5,25.07455993609931,26.7,26.804475398025772,27.3,28.193296535646404,28.5,,,2024-10-14 289 | 288,24.4,25.067827455944578,26.86,26.789565209412483,27.279999999999998,28.179896374719885,29.2,,,2024-10-15 290 | 289,22.5,25.062377311870016,26.9,26.772922859832242,27.4,28.163495243770654,28.6,,,2024-10-16 291 | 290,24.5,25.056828885098625,26.5,26.754448206266204,27.0,28.14440345221482,29.2,,,2024-10-17 292 | 291,24.3,25.05013004805071,26.5,26.734054435480676,27.0,28.123274206927434,28.9,,,2024-10-18 293 | 292,23.7,25.04148973983236,26.96,26.711511075979164,27.24,28.100232011131695,28.9,,,2024-10-19 294 | 293,24.8,25.02963613276868,26.7,26.6862995614748,27.279999999999998,28.075122359113703,29.1,,,2024-10-20 295 | 294,24.5,25.013425545334524,26.0,26.658033872466344,26.84,28.04803414551856,29.8,,,2024-10-21 296 | 295,24.3,24.99249941873476,26.5,26.62699489042094,26.7,28.0194024602752,29.5,,,2024-10-22 297 | 296,23.5,24.967310587483365,26.6,26.594065956059353,27.0,27.98946269290528,29.6,,,2024-10-23 298 | 297,23.2,24.9394600479687,26.5,26.5604280436608,26.9,27.958672490280357,28.5,,,2024-10-24 299 | 298,24.9,24.91003729758272,26.560000000000002,26.52718326582141,26.9,27.927600374064422,28.4,,,2024-10-25 300 | 299,24.4,24.87866534464555,26.62,26.495090408144993,26.9,27.896572127148634,28.5,,,2024-10-26 301 | 300,24.7,24.84524388820944,26.1,26.46482131576951,26.6,27.865873757251734,27.9,,,2024-10-27 302 | 301,24.5,24.80960581592742,26.36,26.436659037882546,27.04,27.835726566020686,27.8,,,2024-10-28 303 | 302,24.3,24.771467170461417,26.060000000000002,26.41045104719403,26.439999999999998,27.806642816354046,28.5,,,2024-10-29 304 | 303,24.5,24.7300761533372,26.26,26.385138897068316,26.64,27.77868946853417,28.2,,,2024-10-30 305 | 304,23.7,24.68443334237557,25.96,26.3593670192826,26.439999999999998,27.75178713530292,27.5,,,2024-10-31 306 | 305,24.2,24.634954796957306,26.1,26.33221317294393,26.64,27.72594978185181,27.6,,,2024-11-01 307 | 306,23.1,24.582962926711456,25.92,26.303231506804654,26.54,27.701730220631884,27.9,,,2024-11-02 308 | 307,24.1,24.529109939688517,25.96,26.27239732757125,26.4,27.679928755055176,28.1,,,2024-11-03 309 | 308,23.5,24.47351013093393,25.92,26.23987059130887,26.48,27.66070413806765,28.1,,,2024-11-04 310 | 309,23.1,24.416380082545135,26.4,26.206146247595637,26.54,27.643941957430155,28.1,,,2024-11-05 311 | 310,25.3,24.35958862922997,26.26,26.172306618734158,26.74,27.629328145213986,28.8,,,2024-11-06 312 | 311,24.6,24.305430910475806,26.3,26.139632197155017,26.8,27.61613112457284,27.8,,,2024-11-07 313 | 312,23.7,24.256158916548056,26.16,26.109013695560215,26.58,27.60354556890182,28.1,,,2024-11-08 314 | 313,23.2,24.212847191421183,26.3,26.08087005951338,26.6,27.590871554427338,28.8,,,2024-11-09 315 | 314,24.0,24.17611077376965,26.38,26.055200081047918,26.8,27.57753708639004,28.2,,,2024-11-10 316 | 315,23.3,24.14565069316819,26.14,26.031644834747006,26.4,27.56327046115802,28.2,,,2024-11-11 317 | 316,22.5,24.120646320260303,25.8,26.009540217434182,26.1,27.548071039999037,28.1,,,2024-11-12 318 | 317,21.2,24.09971644398376,25.8,25.987577748583863,26.34,27.531368226399245,28.8,,,2024-11-13 319 | 318,23.9,24.081612220892435,25.62,25.964547046238312,26.2,27.512160210675226,28.4,,,2024-11-14 320 | 319,22.7,24.06484714268047,25.64,25.939409410558866,25.939999999999998,27.489296359769707,27.8,,,2024-11-15 321 | 320,21.3,24.04836566660602,25.6,25.91207885767433,25.9,27.462504527139973,27.6,,,2024-11-16 322 | 321,21.7,24.03060720972054,25.5,25.883142267636604,25.92,27.43223271711857,27.5,,,2024-11-17 323 | 322,22.5,24.01130468510225,25.82,25.853741706480406,26.279999999999998,27.399702371110543,27.9,,,2024-11-18 324 | 323,22.0,23.991469103931415,25.7,25.824887110044994,26.0,27.36628014006207,28.1,,,2024-11-19 325 | 324,22.7,23.972981577240173,25.86,25.797195738534285,26.38,27.33291695711899,27.6,,,2024-11-20 326 | 325,24.1,23.956343132192327,25.76,25.77092031664851,26.24,27.301055755360263,27.5,,,2024-11-21 327 | 326,24.1,23.940731798349184,25.4,25.746123700566635,26.2,27.272048499284896,27.8,,,2024-11-22 328 | 327,23.7,23.92522114125292,25.66,25.72304710407057,26.04,27.247375086887633,27.5,,,2024-11-23 329 | 328,23.2,23.908888080238416,25.9,25.701724416282282,26.2,27.22799421199982,28.1,,,2024-11-24 330 | 329,23.4,23.890088921591207,25.560000000000002,25.681596332231106,26.2,27.213701442905183,27.8,,,2024-11-25 331 | 330,23.8,23.867887607036263,25.46,25.661779512606298,25.84,27.203405150440148,27.7,,,2024-11-26 332 | 331,23.3,23.842776311340984,25.46,25.641667720674892,25.9,27.19560879369619,27.1,,,2024-11-27 333 | 332,21.3,23.815246600232946,25.36,25.621032821233044,25.6,27.189106434097678,26.9,,,2024-11-28 334 | 333,22.7,23.785396379608727,25.36,25.599823921357732,25.939999999999998,27.18328401950008,27.3,,,2024-11-29 335 | 334,23.2,23.752866622826257,25.46,25.578021695888904,26.18,27.17788989148154,27.2,,,2024-11-30 336 | 335,22.9,23.718022852261843,25.62,25.5557624073539,26.1,27.172576899016597,27.9,,,2024-12-01 337 | 336,22.4,23.6821965809016,24.7,25.533226485433175,25.64,27.168072369415334,28.6,,,2024-12-02 338 | 337,20.6,23.647153348124995,25.18,25.510717203785113,25.8,27.16521560020176,27.7,,,2024-12-03 339 | 338,22.8,23.614446968961857,25.0,25.48888960509094,25.82,27.16453660772422,28.6,,,2024-12-04 340 | 339,22.7,23.584964370999252,24.96,25.4684858747666,25.939999999999998,27.167204489930906,27.5,,,2024-12-05 341 | 340,23.4,23.55903950500895,25.060000000000002,25.449932342674515,26.14,27.174324220210988,27.8,,,2024-12-06 342 | 341,22.5,23.536082022227646,24.92,25.433381487416323,26.08,27.186162910103267,27.8,,,2024-12-07 343 | 342,23.0,23.51537186679947,25.12,25.418571963815943,25.88,27.201617513944964,27.5,,,2024-12-08 344 | 343,23.0,23.496590925511313,25.16,25.4048083466794,25.64,27.218349242822416,28.1,,,2024-12-09 345 | 344,22.8,23.479138397589704,25.36,25.391240885556336,25.64,27.233741284925117,28.0,,,2024-12-10 346 | 345,23.1,23.462796612664686,25.2,25.377473163136955,25.64,27.245950648265197,27.2,,,2024-12-11 347 | 346,23.2,23.44731078317868,25.26,25.363609016038012,25.7,27.25458320000807,27.7,,,2024-12-12 348 | 347,22.8,23.431542025066104,24.96,25.349904930646403,26.14,27.260751111517987,27.5,,,2024-12-13 349 | 348,23.1,23.41372250806052,25.02,25.336121139840063,25.66,27.26573569362008,27.6,,,2024-12-14 350 | 349,23.3,23.391712989900707,24.8,25.321281192673485,25.5,27.27049058679047,28.0,,,2024-12-15 351 | 350,22.2,23.36421153591947,24.4,25.304353581154277,25.14,27.274803886233975,28.3,,,2024-12-16 352 | 351,22.8,23.33086797419195,25.12,25.284803793241466,25.64,27.27788735816525,27.4,,,2024-12-17 353 | 352,23.2,23.27859498526925,24.96,25.259995001707118,25.42,27.27772272047782,28.1,,,2024-12-18 354 | 353,22.6,23.227026080444656,25.060000000000002,25.233883061084175,25.6,27.274274516481082,27.4,,,2024-12-19 355 | 354,22.2,23.176547349482497,24.86,25.207593721385912,25.4,27.26937118959851,27.7,,,2024-12-20 356 | 355,22.2,23.126604125990255,25.12,25.181362757214536,25.48,27.263771580011838,27.9,,,2024-12-21 357 | 356,22.4,23.076780846085164,24.86,25.155215039795806,25.4,27.25776334097246,28.2,,,2024-12-22 358 | 357,21.9,23.026785103575012,24.92,25.129124207882843,25.54,27.25145835822907,27.2,,,2024-12-23 359 | 358,22.9,22.97637914139562,24.96,25.103061149050866,26.14,27.244959181564518,28.5,,,2024-12-24 360 | 359,23.0,22.925413305195583,24.8,25.07700326194738,25.34,27.23836096193224,28.7,,,2024-12-25 361 | 360,21.6,22.873724410556427,25.0,25.050901640053656,25.68,27.231689598922348,28.2,,,2024-12-26 362 | 361,22.4,22.821154905317275,24.860000000000003,25.024681130364243,25.58,27.224920442808376,28.0,,,2024-12-27 363 | 362,22.0,22.767607173283626,24.16,24.99824652360992,25.279999999999998,27.217952143963906,27.7,,,2024-12-28 364 | 363,21.4,22.713067379338305,24.82,24.97151604500083,25.48,27.210632259534666,27.2,,,2024-12-29 365 | 364,21.9,22.65762580392415,24.4,24.944451362059148,25.439999999999998,27.202775319284807,27.5,,,2024-12-30 366 | 365,22.6,22.601445359784478,24.76,24.91704874410255,25.74,27.194280948278937,27.8,,,2024-12-31 367 | -------------------------------------------------------------------------------- /sample_data/transformed/temperature_mean-addis-ababa-ethiopia_transformed.csv: -------------------------------------------------------------------------------- 1 | dayofyear,min,p05,p40,mean,p60,p95,max,2024,2024_diff,date 2 | 1,10.2,11.345867806169725,13.1,13.218627284942785,13.639999999999999,14.903534895228171,15.0,14.7,1.4813727150572138,2024-01-01 3 | 2,11.2,11.414713439003753,13.16,13.264068069754737,13.44,14.936174936599095,16.1,15.0,1.735931930245263,2024-01-02 4 | 3,11.3,11.483923906983343,12.96,13.309515509311039,13.4,14.969085240565512,15.4,14.5,1.1904844906889611,2024-01-03 5 | 4,11.5,11.553378956814273,13.16,13.354920916973677,13.6,15.002243557661012,15.0,14.5,1.1450790830263227,2024-01-04 6 | 5,10.9,11.622952543119935,13.14,13.400237528656493,13.94,15.035581123278988,14.8,15.5,2.0997624713435066,2024-01-05 7 | 6,10.3,11.692524121263709,13.06,13.445436083317244,13.74,15.069070999534867,15.8,15.1,1.654563916682756,2024-01-06 8 | 7,11.0,11.761990368747519,13.26,13.490499169118685,13.84,15.1027414025616,16.3,14.6,1.1095008308813146,2024-01-07 9 | 8,11.1,11.831299814668293,13.16,13.535426679834623,13.7,15.136613725528791,15.4,13.9,0.3645733201653769,2024-01-08 10 | 9,10.8,11.90046237143164,13.32,13.580232846414464,13.98,15.170670994022988,15.9,14.0,0.4197671535855356,2024-01-09 11 | 10,11.6,11.969512241637325,13.6,13.624952313008466,13.84,15.20490260942161,16.2,14.6,0.9750476869915339,2024-01-10 12 | 11,10.9,12.038535815553463,13.42,13.669642638636411,13.8,15.239287376480748,16.1,15.0,1.330357361363589,2024-01-11 13 | 12,11.3,12.107674224269664,13.360000000000001,13.714383651997368,14.04,15.27380782528201,15.6,15.0,1.2856163480026321,2024-01-12 14 | 13,11.9,12.177210510793682,13.780000000000001,13.759299741714706,14.2,15.308465903624294,16.1,15.2,1.4407002582852932,2024-01-13 15 | 14,11.9,12.247761411152442,13.48,13.804613578668631,14.2,15.343395574822585,16.9,16.3,2.4953864213313697,2024-01-14 16 | 15,12.6,12.32056945095882,13.66,13.850654211614264,14.2,15.379141890294655,17.9,16.0,2.1493457883857356,2024-01-15 17 | 16,11.7,12.384379939571595,13.92,13.894008403411965,14.24,15.414623767305706,16.0,15.2,1.3059915965880347,2024-01-16 18 | 17,11.9,12.443991662155645,13.52,13.936154108316801,14.44,15.450390951965463,15.7,15.2,1.2638458916831983,2024-01-17 19 | 18,12.1,12.49850817730469,13.66,13.976539876360446,14.5,15.487249859790133,16.0,16.2,2.223460123639553,2024-01-18 20 | 19,12.0,12.547338869963593,13.4,14.014726987453766,13.94,15.526021524000278,15.9,15.8,1.7852730125462344,2024-01-19 21 | 20,11.3,12.590403877024645,13.66,14.050440151627368,14.1,15.567718152666012,16.0,15.1,1.0495598483726312,2024-01-20 22 | 21,12.4,12.62814692665104,13.76,14.08357849884322,14.34,15.61263112491847,15.9,14.7,0.6164215011567791,2024-01-21 23 | 22,12.8,12.661061582212911,13.82,14.114373713715624,14.74,15.659754424179207,16.0,14.2,0.0856262862843753,2024-01-22 24 | 23,11.7,12.689164930500288,14.1,14.143268392250077,14.74,15.707387436014539,16.1,15.2,1.0567316077499225,2024-01-23 25 | 24,12.3,12.712357525219238,13.96,14.17073491062804,14.54,15.7541193735213,16.2,15.7,1.5292650893719593,2024-01-24 26 | 25,12.5,12.731297572738159,13.9,14.19726867725026,14.4,15.798827065372963,16.2,16.6,2.402731322749741,2024-01-25 27 | 26,11.4,12.746487990385523,14.06,14.223280098252554,14.54,15.84094137877162,16.6,17.5,3.2767199017474464,2024-01-26 28 | 27,12.3,12.7587519250097,13.860000000000001,14.249064069387412,14.639999999999999,15.881003389809859,16.3,17.0,2.750935930612588,2024-01-27 29 | 28,12.7,12.76944140312583,14.16,14.274910913363707,14.639999999999999,15.920105478103286,16.4,16.2,1.9250890866362926,2024-01-28 30 | 29,12.9,12.780098744709012,13.98,14.301044837786712,14.319999999999999,15.959021420168705,16.9,15.7,1.3989551622132872,2024-01-29 31 | 30,12.3,12.791201954405803,13.96,14.327338955322444,14.319999999999999,15.997822093142165,16.6,15.8,1.4726610446775563,2024-01-30 32 | 31,10.9,12.802138749155523,13.92,14.353480741025406,14.68,16.03691438969532,16.7,15.2,0.8465192589745936,2024-01-31 33 | 32,10.9,12.811642193602012,14.0,14.379074606954843,14.639999999999999,16.076774250869416,16.8,15.3,0.9209253930451577,2024-02-01 34 | 33,12.0,12.81914650810097,13.96,14.403664005417266,14.6,16.117587914254912,16.8,15.8,1.396335994582735,2024-02-02 35 | 34,11.7,12.824997574748775,14.0,14.427079239159703,14.54,16.15976516419755,17.0,15.1,0.672920760840297,2024-02-03 36 | 35,11.1,12.830692304995043,14.5,14.449771932229716,14.7,16.20352514251656,16.7,14.8,0.3502280677702849,2024-02-04 37 | 36,11.8,12.83813398010471,14.2,14.472222665769072,14.88,16.248083916542093,16.4,14.6,0.1277773342309274,2024-02-05 38 | 37,12.6,12.848622334199996,14.26,14.494674596790487,14.639999999999999,16.291749044925744,16.6,15.6,1.1053254032095126,2024-02-06 39 | 38,13.4,12.862954824764525,14.26,14.51718371619581,14.639999999999999,16.332704819647997,16.5,15.7,1.1828162838041898,2024-02-07 40 | 39,13.2,12.88131128580581,14.56,14.539870538664527,14.84,16.369593391890394,17.0,16.3,1.7601294613354739,2024-02-08 41 | 40,13.3,12.903548541404126,14.42,14.562965473420947,14.959999999999999,16.401826488800502,17.4,16.5,1.937034526579053,2024-02-09 42 | 41,12.4,12.930283049538248,14.42,14.587007421629833,15.34,16.43037627458211,18.0,15.9,1.3129925783701673,2024-02-10 43 | 42,12.4,12.962733025709776,14.62,14.612615931999423,15.04,16.4572912196192,16.8,15.6,0.9873840680005763,2024-02-11 44 | 43,12.5,13.000838695044337,14.46,14.64017471750244,14.84,16.484911435327344,16.1,15.2,0.5598252824975596,2024-02-12 45 | 44,12.0,13.042696119955801,14.360000000000001,14.66945581602462,15.24,16.514989284265976,16.6,15.6,0.9305441839753801,2024-02-13 46 | 45,12.5,13.08601595398119,14.42,14.699926152045645,15.24,16.54843567667416,16.8,15.4,0.7000738479543553,2024-02-14 47 | 46,12.7,13.129645716184754,14.16,14.731182936531699,14.719999999999999,16.58566586331646,17.2,15.5,0.7688170634683011,2024-02-15 48 | 47,12.8,13.1744151366423,14.22,14.763178673777059,14.8,16.626491035813153,17.4,15.3,0.5368213262229418,2024-02-16 49 | 48,13.0,13.22146586249732,14.26,14.796131102533616,14.94,16.669506951343116,18.0,15.3,0.5038688974663845,2024-02-17 50 | 49,11.9,13.271554545932727,14.4,14.83026751634088,14.74,16.7123511968189,16.9,15.7,0.8697324836591189,2024-02-18 51 | 50,11.5,13.324888636052423,14.2,14.8656817781041,15.04,16.752962691430113,16.7,16.1,1.2343182218959008,2024-02-19 52 | 51,12.7,13.380598156265584,14.5,14.902106716324633,15.06,16.790189102339102,16.5,15.5,0.5978932836753668,2024-02-20 53 | 52,13.2,13.436996352625123,14.46,14.938916963743985,15.1,16.823948483684536,16.8,15.4,0.46108303625601543,2024-02-21 54 | 53,13.1,13.493096040641557,14.5,14.97550681405446,14.94,16.854623601642686,17.4,15.0,0.02449318594553951,2024-02-22 55 | 54,13.4,13.548489136387035,14.76,15.011808372152231,15.04,16.883198236699137,17.5,14.8,-0.21180837215223036,2024-02-23 56 | 55,13.5,13.60262010414541,14.66,15.048386824975973,15.0,16.91082780465102,18.0,15.8,0.751613175024028,2024-02-24 57 | 56,13.9,13.654355489704994,14.6,15.08603290909959,14.9,16.937465101241358,18.1,17.4,2.313967090900409,2024-02-25 58 | 57,13.3,13.702271144812809,14.66,15.12520629809883,15.36,16.963332215228377,17.5,18.2,3.0747937019011697,2024-02-26 59 | 58,13.1,13.745512141097556,14.7,15.166064933200014,15.7,16.989878942300145,17.4,18.8,3.633935066799987,2024-02-27 60 | 59,12.7,13.784146086371761,15.1,15.208434549264853,15.44,17.01902405493146,18.0,18.4,3.191565450735146,2024-02-28 61 | 60,12.8,13.818310293135692,14.7,15.251743496871047,15.5,17.052025341232827,18.2,16.8,1.548256503128954,2024-03-01 62 | 61,13.0,13.84797736497401,15.0,15.295079580886753,15.56,17.088515605795923,18.0,16.5,1.2049204191132468,2024-03-02 63 | 62,12.4,13.873124515879068,14.8,15.337131871379784,15.319999999999999,17.126412158836075,18.5,17.2,1.8628681286202156,2024-03-03 64 | 63,12.6,13.893784127064363,14.8,15.376499498723184,15.1,17.162599863122544,17.8,16.3,0.9235005012768163,2024-03-04 65 | 64,13.9,13.91039896034953,14.62,15.412372720570273,15.2,17.195045084264176,17.7,15.9,0.48762727942972717,2024-03-05 66 | 65,14.0,13.924033664123437,14.8,15.444959445647509,15.319999999999999,17.224050731528948,17.8,16.3,0.8550405543524917,2024-03-06 67 | 66,13.9,13.936138428073473,15.12,15.475086823297989,15.639999999999999,17.251070177467227,17.3,15.8,0.3249131767020117,2024-03-07 68 | 67,13.8,13.947701843711634,15.360000000000001,15.503718259572304,15.98,17.277838350558387,17.5,15.9,0.39628174042769615,2024-03-08 69 | 68,13.8,13.95933201986744,14.96,15.531668247621381,16.24,17.306061943832724,18.0,16.3,0.7683317523786197,2024-03-09 70 | 69,13.3,13.971113834261844,15.66,15.559354067684373,16.04,17.33622305327758,18.2,15.2,-0.3593540676843734,2024-03-10 71 | 70,13.5,13.98292337280637,15.52,15.586688527679135,16.0,17.367636510758715,18.9,15.2,-0.3866885276791354,2024-03-11 72 | 71,13.4,13.99418788128748,15.4,15.6132853870657,15.94,17.399694740705968,18.9,15.4,-0.21328538706569944,2024-03-12 73 | 72,12.5,14.004314759886404,15.56,15.638766826147823,16.24,17.43281204590114,19.1,15.6,-0.0387668261478229,2024-03-13 74 | 73,12.0,14.013641872241147,15.72,15.66291990479126,16.24,17.467641389129,18.0,16.0,0.3370800952087407,2024-03-14 75 | 74,12.3,14.02221649226714,15.66,15.685077550534311,16.24,17.504180974632593,17.8,16.0,0.3149224494656888,2024-03-15 76 | 75,12.5,14.029792809920885,15.16,15.704273331522163,15.84,17.54144552861641,18.8,16.3,0.5957266684778375,2024-03-16 77 | 76,12.7,14.036312052448258,15.4,15.719618340206232,15.7,17.57734398618461,19.4,17.0,1.2803816597937683,2024-03-17 78 | 77,13.5,14.042150532147957,15.44,15.730895784108062,15.78,17.609952608434316,19.3,17.3,1.5691042158919384,2024-03-18 79 | 78,14.2,14.047874692502157,15.48,15.738432691122442,15.84,17.638307389633702,18.7,18.3,2.5615673088775583,2024-03-19 80 | 79,14.3,14.053994340027394,15.62,15.742755037141789,15.94,17.662024576917315,18.9,17.3,1.5572449628582117,2024-03-20 81 | 80,13.7,14.061104101864537,15.52,15.744470332934611,16.0,17.68175765824273,18.7,17.5,1.7555296670653888,2024-03-21 82 | 81,13.6,14.0697821763758,15.3,15.744380060387911,15.74,17.698752161457247,18.9,18.6,2.8556199396120903,2024-03-22 83 | 82,13.5,14.080481455059155,15.4,15.743269288548044,15.9,17.714001785024475,17.9,18.3,2.556730711451957,2024-03-23 84 | 83,13.3,14.093502862558049,15.4,15.74143524780666,15.78,17.727125613574493,18.1,18.0,2.2585647521933403,2024-03-24 85 | 84,12.5,14.109215333844915,15.32,15.73890448990665,15.6,17.737484571595566,18.4,15.7,-0.03890448990665085,2024-03-25 86 | 85,12.9,14.127692028094845,15.1,15.735728997830675,15.78,17.744993201800856,18.6,15.9,0.16427100216932544,2024-03-26 87 | 86,14.3,14.148210714641724,15.2,15.731993216264113,16.04,17.749995502583932,18.6,15.8,0.06800678373588731,2024-03-27 88 | 87,14.0,14.169545383526232,15.3,15.727515993397496,16.0,17.7525886009462,18.6,15.5,-0.22751599339749617,2024-03-28 89 | 88,13.4,14.190103448020388,15.4,15.72189239214806,15.74,17.752753947783024,19.1,15.5,-0.22189239214806022,2024-03-29 90 | 89,13.8,14.208793558038387,15.26,15.714845178738845,15.94,17.750693784624723,19.5,15.8,0.08515482126115614,2024-03-30 91 | 90,13.8,14.225614824479699,15.2,15.706663943202772,15.9,17.74729405166418,20.1,15.3,-0.406663943202771,2024-03-31 92 | 91,13.4,14.240939691534644,15.16,15.698045677462526,15.84,17.743202558417696,19.5,15.9,0.2019543225374747,2024-04-01 93 | 92,13.4,14.255403380717844,15.16,15.689769821015837,15.74,17.738913123615546,19.2,15.5,-0.18976982101583673,2024-04-02 94 | 93,14.2,14.26966486337477,15.16,15.68242573368031,15.5,17.734453533022347,19.3,15.1,-0.58242573368031,2024-04-03 95 | 94,14.0,14.283940783662569,15.16,15.676283902025082,15.66,17.72924836923131,19.4,15.4,-0.2762839020250816,2024-04-04 96 | 95,14.3,14.298296473163468,15.26,15.671170562428221,15.639999999999999,17.722035367678465,19.5,15.5,-0.17117056242822137,2024-04-05 97 | 96,14.5,14.313051351897768,15.360000000000001,15.66670819325192,15.8,17.711153065655,19.4,15.2,-0.4667081932519199,2024-04-06 98 | 97,14.4,14.328584805207601,15.06,15.662673817022267,15.639999999999999,17.695493730565943,19.3,15.2,-0.4626738170222673,2024-04-07 99 | 98,14.4,14.345251739980617,15.26,15.659076216078605,15.6,17.67569089067465,19.0,15.7,0.04092378392139473,2024-04-08 100 | 99,13.5,14.362658621558055,15.26,15.656211746385233,15.6,17.65313576037942,19.0,16.4,0.7437882536147651,2024-04-09 101 | 100,14.2,14.380137406503184,15.06,15.654455654724577,15.54,17.629158782204154,18.6,15.8,0.14554434527542348,2024-04-10 102 | 101,13.8,14.39632853033337,15.1,15.65377521004162,15.34,17.60434840944295,19.3,15.7,0.046224789958380086,2024-04-11 103 | 102,13.9,14.409338646535724,15.16,15.653765358196534,15.4,17.579526951650024,18.7,15.5,-0.1537653581965337,2024-04-12 104 | 103,14.1,14.418130810745224,15.12,15.65433683959239,15.6,17.55648527959247,18.5,16.2,0.5456631604076101,2024-04-13 105 | 104,14.0,14.422488917040098,15.3,15.65571372563809,15.5,17.537312392152277,18.3,17.0,1.3442862743619095,2024-04-14 106 | 105,13.9,14.422805554616364,15.4,15.658240688697658,15.9,17.524052867565366,18.0,17.2,1.5417593113023411,2024-04-15 107 | 106,14.5,14.420434345908545,15.32,15.662370121116536,15.88,17.517913141886556,18.3,17.2,1.5376298788834628,2024-04-16 108 | 107,14.3,14.417544361794295,15.4,15.668587291327388,15.74,17.51865617139638,18.6,17.2,1.5314127086726117,2024-04-17 109 | 108,14.3,14.415962641942507,15.26,15.677084661615794,15.7,17.525184512759367,18.2,17.0,1.322915338384206,2024-04-18 110 | 109,14.5,14.416642971180613,15.3,15.687694426454438,15.7,17.53514927773403,17.6,17.1,1.412305573545563,2024-04-19 111 | 110,14.6,14.419500751863106,15.5,15.700067562902374,15.7,17.54677256036708,17.4,17.5,1.7999324370976257,2024-04-20 112 | 111,14.3,14.424028195288082,15.26,15.71384757178227,15.819999999999999,17.559575767614948,17.8,15.8,0.08615242821772995,2024-04-21 113 | 112,14.4,14.429376193747572,15.2,15.728711981073848,15.9,17.57463206272056,19.5,16.5,0.7712880189261515,2024-04-22 114 | 113,14.4,14.4346345550317,15.46,15.744420912187275,15.88,17.593397423284383,19.1,16.3,0.5555790878127258,2024-04-23 115 | 114,13.6,14.438881608930712,15.3,15.760579053511107,15.88,17.617421308157237,18.4,16.1,0.33942094648889487,2024-04-24 116 | 115,13.8,14.441682675029355,15.16,15.776485321816015,15.9,17.647299408454355,18.1,16.4,0.6235146781839838,2024-04-25 117 | 116,13.8,14.442430508479738,15.26,15.791420361002677,15.94,17.68276392454577,18.1,15.2,-0.5914203610026778,2024-04-26 118 | 117,14.0,14.440844764623566,15.360000000000001,15.804959492211779,15.84,17.723056643093805,18.6,15.6,-0.20495949221177945,2024-04-27 119 | 118,13.1,14.437184674312121,15.26,15.816768623281328,16.0,17.766786505002262,19.2,15.6,-0.21676862328132884,2024-04-28 120 | 119,13.7,14.43212748707518,15.5,15.826505188888673,15.92,17.812107998021492,18.6,15.6,-0.2265051888886731,2024-04-29 121 | 120,13.8,14.42675883413361,15.5,15.833959517841583,15.78,17.857087797535538,18.6,16.2,0.3660404821584162,2024-04-30 122 | 121,14.4,14.42245558907318,15.46,15.839090769489893,15.94,17.899684441996822,18.7,15.7,-0.13909076948989352,2024-05-01 123 | 122,13.6,14.420136092988223,15.360000000000001,15.842219257422828,15.84,17.93771164682661,19.4,16.6,0.7577807425771734,2024-05-02 124 | 123,13.7,14.4203355742309,15.5,15.844175340740158,16.12,17.969337637965612,19.0,16.2,0.3558246592598415,2024-05-03 125 | 124,13.7,14.422953542494684,15.66,15.845928167864823,16.24,17.99398454564202,19.3,16.6,0.7540718321351783,2024-05-04 126 | 125,13.5,14.427208352374382,15.34,15.848064941631808,16.12,18.011442024768602,19.4,17.3,1.4519350583681927,2024-05-05 127 | 126,13.9,14.43177571439995,15.32,15.850694953004625,15.94,18.021582233530776,19.5,16.0,0.14930504699537472,2024-05-06 128 | 127,14.0,14.435343057365259,15.360000000000001,15.853624565767326,16.0,18.024801141951205,18.9,16.9,1.0463754342326723,2024-05-07 129 | 128,13.6,14.43686630363543,15.3,15.856303084259023,15.7,18.02229691945843,18.8,16.8,0.9436969157409774,2024-05-08 130 | 129,14.2,14.436077267182537,15.360000000000001,15.85816708177445,15.7,18.01570120811145,18.6,15.9,0.04183291822555013,2024-05-09 131 | 130,14.0,14.433587570474899,15.06,15.85916135570989,15.639999999999999,18.006980198798136,18.8,15.9,0.04083864429011008,2024-05-10 132 | 131,14.0,14.430089380751749,15.2,15.859673659704184,15.639999999999999,17.998040481467054,19.0,16.2,0.34032634029581565,2024-05-11 133 | 132,14.3,14.426168932961122,15.16,15.86024018964968,15.54,17.989992767403212,18.5,16.5,0.6397598103503199,2024-05-12 134 | 133,14.3,14.422450581470782,15.2,15.861491062450492,15.639999999999999,17.983821406684104,18.5,17.4,1.5385089375495067,2024-05-13 135 | 134,14.2,14.419483045087848,15.5,15.8640592550382,15.74,17.979929032305876,18.8,17.5,1.6359407449618004,2024-05-14 136 | 135,14.3,14.417171593276475,15.56,15.868403575604882,16.0,17.978595201846097,18.4,16.7,0.8315964243951175,2024-05-15 137 | 136,14.4,14.415156315076846,15.8,15.874921108794974,16.1,17.980536506919353,18.0,16.8,0.9250788912050272,2024-05-16 138 | 137,14.1,14.413016508357066,15.56,15.883972536976248,16.24,17.986603211094113,18.0,17.0,1.1160274630237517,2024-05-17 139 | 138,14.0,14.410521772351922,15.62,15.895588498716405,16.1,17.997965214202953,17.7,17.2,1.3044115012835942,2024-05-18 140 | 139,14.0,14.407371543597874,15.5,15.909320763670134,16.08,18.015272967274758,18.3,16.5,0.5906792363298656,2024-05-19 141 | 140,13.8,14.402963749371423,15.7,15.924189608952897,16.279999999999998,18.03869328717012,18.8,17.0,1.0758103910471029,2024-05-20 142 | 141,14.1,14.39692935156919,15.32,15.938507789054125,16.24,18.068076898041088,19.0,16.6,0.6614922109458767,2024-05-21 143 | 142,14.3,14.389246291091592,15.26,15.950355988428083,16.14,18.103031795509256,18.6,17.2,1.2496440115719167,2024-05-22 144 | 143,14.2,14.380303741915624,15.3,15.958286787508918,16.14,18.14235371650773,18.8,17.1,1.1417132124910836,2024-05-23 145 | 144,14.3,14.370334009883397,15.360000000000001,15.961502432991715,16.439999999999998,18.18449073897112,19.4,17.9,1.9384975670082838,2024-05-24 146 | 145,14.1,14.359539059273352,15.4,15.959650354180747,16.22,18.227757273782018,19.7,17.6,1.6403496458192546,2024-05-25 147 | 146,14.1,14.3478794781422,15.3,15.952630811388197,16.42,18.27078196025358,19.4,18.5,2.5473691886118033,2024-05-26 148 | 147,14.2,14.335280607344565,15.3,15.940583444222936,16.1,18.31175121897134,19.4,18.0,2.059416555777064,2024-05-27 149 | 148,14.2,14.32154570566995,15.26,15.924020714444229,16.14,18.348443032027333,19.2,18.3,2.375979285555772,2024-05-28 150 | 149,14.6,14.30633122860733,15.46,15.903842324634427,15.639999999999999,18.378793859802574,19.5,18.1,2.1961576753655745,2024-05-29 151 | 150,13.8,14.288933676046517,15.7,15.880355175093397,16.24,18.401125107799373,19.7,18.0,2.1196448249066027,2024-05-30 152 | 151,13.5,14.26868358488454,15.360000000000001,15.853309734894099,16.0,18.414696980801715,19.2,17.9,2.0466902651059,2024-05-31 153 | 152,13.9,14.245020264289275,15.46,15.822113909574675,16.1,18.419502320587803,19.1,,,2024-06-01 154 | 153,14.0,14.217714203767317,15.42,15.786404558721197,16.04,18.416663557706084,19.0,,,2024-06-02 155 | 154,14.2,14.187163824621699,15.26,15.746036333564591,15.5,18.406826554211566,19.6,,,2024-06-03 156 | 155,14.1,14.154002231993088,15.16,15.700942075757553,15.52,18.389914375950966,19.4,,,2024-06-04 157 | 156,14.2,14.11871656334856,15.0,15.651375010446833,15.639999999999999,18.365231475553156,19.2,,,2024-06-05 158 | 157,14.1,14.081628219291312,15.12,15.598011654319015,15.5,18.332207699469745,18.9,,,2024-06-06 159 | 158,13.3,14.042826636975029,15.26,15.541607503488368,15.4,18.290614397163537,19.0,,,2024-06-07 160 | 159,13.3,14.002019967063429,15.06,15.482764176388528,15.3,18.240546312415265,19.0,,,2024-06-08 161 | 160,13.9,13.95912539403266,14.98,15.421981159973331,15.3,18.18330555426319,18.6,,,2024-06-09 162 | 161,13.9,13.914431707540478,14.7,15.359792522604911,15.24,18.121536505832267,18.9,,,2024-06-10 163 | 162,13.7,13.868075226883219,15.12,15.296940189532407,15.44,18.05781948696719,19.0,,,2024-06-11 164 | 163,13.7,13.820189900701552,14.66,15.234428500831667,15.24,17.99489972113756,19.3,,,2024-06-12 165 | 164,13.3,13.771366273355714,14.5,15.172971351393963,14.94,17.936184525223425,18.6,,,2024-06-13 166 | 165,13.4,13.722400229249129,14.62,15.1128973285322,15.0,17.88468572020002,18.6,,,2024-06-14 167 | 166,13.4,13.67425361303527,14.5,15.054224124279521,15.08,17.84228644737404,19.4,,,2024-06-15 168 | 167,13.2,13.627595379414666,14.2,14.996983974753775,14.68,17.80950795723076,18.8,,,2024-06-16 169 | 168,13.5,13.582963799576687,14.22,14.941223991431752,14.8,17.784272351558133,18.3,,,2024-06-17 170 | 169,13.4,13.540755109371112,14.3,14.887277804263737,14.74,17.76300636095851,18.9,,,2024-06-18 171 | 170,13.3,13.501156023825395,14.26,14.83571055233721,14.719999999999999,17.743313567912175,18.7,,,2024-06-19 172 | 171,13.3,13.463589906985302,14.06,14.786892692576869,14.74,17.724886330005585,18.7,,,2024-06-20 173 | 172,13.1,13.427175015672281,14.24,14.740821018302347,14.5,17.708254991662017,18.7,,,2024-06-21 174 | 173,13.1,13.39147998765823,14.06,14.697450872822728,14.34,17.694060219024816,18.4,,,2024-06-22 175 | 174,12.9,13.356654845650999,14.06,14.656979175108154,14.34,17.68346851983808,19.6,,,2024-06-23 176 | 175,12.8,13.323168040276148,14.0,14.619501813226645,14.4,17.675725510160444,19.6,,,2024-06-24 177 | 176,12.8,13.291637042411386,14.0,14.584882915260225,14.4,17.66864632187986,19.5,,,2024-06-25 178 | 177,12.9,13.26290222988917,13.8,14.552903720863045,14.3,17.659694897683902,18.2,,,2024-06-26 179 | 178,13.4,13.237721246590258,13.9,14.52329805110705,14.24,17.64715947658195,18.7,,,2024-06-27 180 | 179,13.1,13.216574408332884,13.76,14.495927900430019,14.2,17.630729370271688,18.2,,,2024-06-28 181 | 180,13.0,13.199467161337093,13.860000000000001,14.470770935222392,14.28,17.611090873861567,19.1,,,2024-06-29 182 | 181,12.9,13.185898212190631,13.76,14.447634413854757,14.2,17.587811257706363,19.2,,,2024-06-30 183 | 182,13.1,13.175279050815105,14.1,14.426055029384136,14.34,17.559027084332,19.2,,,2024-07-01 184 | 183,13.5,13.166476955588358,14.12,14.405650606422531,14.5,17.523289226585486,19.6,,,2024-07-02 185 | 184,12.5,13.158166763797675,13.92,14.386182578196387,14.1,17.47980469116873,19.3,,,2024-07-03 186 | 185,12.7,13.149297693201172,13.9,14.367271115426892,14.1,17.42874312467594,19.7,,,2024-07-04 187 | 186,12.8,13.139129893816573,13.76,14.34844790349865,14.1,17.37125079407131,19.1,,,2024-07-05 188 | 187,12.7,13.127339926710299,13.9,14.329483675081212,14.0,17.309313117971147,18.9,,,2024-07-06 189 | 188,12.3,13.114324287067905,13.9,14.31026818672868,14.139999999999999,17.24481613781903,18.5,,,2024-07-07 190 | 189,12.5,13.100761899133724,13.8,14.290604171982897,14.1,17.17818967756343,19.1,,,2024-07-08 191 | 190,13.4,13.08754161177525,13.9,14.270317690181486,14.1,17.10868135243868,18.8,,,2024-07-09 192 | 191,13.2,13.075690301913964,13.860000000000001,14.249451016608795,14.1,17.03631985226286,18.3,,,2024-07-10 193 | 192,12.8,13.065092676503626,13.860000000000001,14.228045492112058,14.139999999999999,16.963205420308732,17.8,,,2024-07-11 194 | 193,13.2,13.054779694387468,13.8,14.20629073257985,14.18,16.89321302545248,18.0,,,2024-07-12 195 | 194,12.6,13.043480812174828,13.860000000000001,14.184449769696599,14.2,16.829913570144214,17.8,,,2024-07-13 196 | 195,12.3,13.030378704219139,13.66,14.162821932458687,14.0,16.77427112100749,17.6,,,2024-07-14 197 | 196,12.8,13.015501712527495,13.66,14.14184382859606,14.0,16.725497663072282,17.4,,,2024-07-15 198 | 197,12.5,12.999238805211561,13.6,14.121977089762773,13.9,16.682299412939862,17.8,,,2024-07-16 199 | 198,12.8,12.982234189503778,13.8,14.103505172836577,14.1,16.644183011655883,18.7,,,2024-07-17 200 | 199,12.6,12.965295226343683,13.56,14.086640241027732,13.94,16.6111459706644,19.7,,,2024-07-18 201 | 200,12.3,12.949596367047128,13.66,14.071714659026512,14.0,16.58261516934936,19.3,,,2024-07-19 202 | 201,12.5,12.936023594957735,13.66,14.059078376714258,13.94,16.557178750124066,19.5,,,2024-07-20 203 | 202,12.2,12.925074060882604,13.66,14.048889102307557,13.9,16.533493069916343,20.5,,,2024-07-21 204 | 203,12.7,12.917158567376896,13.6,14.04101402855214,13.7,16.510240085372555,20.1,,,2024-07-22 205 | 204,12.9,12.912337740240242,13.66,14.035190637423229,13.9,16.486687885327825,20.3,,,2024-07-23 206 | 205,12.3,12.910332623101258,13.6,14.03108085230374,13.9,16.463270117122956,20.0,,,2024-07-24 207 | 206,12.4,12.910613568630449,13.5,14.028124724593484,13.7,16.44214134465568,19.5,,,2024-07-25 208 | 207,12.0,12.912957277309172,13.56,14.025768180644404,13.78,16.425082374765832,20.3,,,2024-07-26 209 | 208,12.2,12.917289522482234,13.5,14.023674794709377,13.7,16.411386305399343,19.8,,,2024-07-27 210 | 209,12.7,12.92347335766013,13.66,14.021801786699552,13.84,16.3974090588869,19.2,,,2024-07-28 211 | 210,12.6,12.931161428126353,13.7,14.020246223912807,13.9,16.378711825152635,19.2,,,2024-07-29 212 | 211,12.7,12.939458682819913,13.66,14.019058903055095,13.94,16.352146332727255,19.4,,,2024-07-30 213 | 212,12.9,12.947882636234933,13.780000000000001,14.01823305254081,14.1,16.318025814115373,19.1,,,2024-07-31 214 | 213,12.8,12.95637664370529,13.8,14.017609351030167,14.0,16.27987090776229,19.4,,,2024-08-01 215 | 214,12.7,12.964411052470037,13.5,14.0167440499284,14.04,16.240812784391206,18.7,,,2024-08-02 216 | 215,12.8,12.971454348597781,13.7,14.01522686481477,13.8,16.201881130963923,18.6,,,2024-08-03 217 | 216,13.0,12.977228962149715,13.7,14.012724886448737,13.9,16.162527007429887,18.0,,,2024-08-04 218 | 217,11.9,12.98165704203744,13.6,14.009170968481286,13.84,16.121296210355354,17.9,,,2024-08-05 219 | 218,13.0,12.985090897296367,13.72,14.004628210077298,13.94,16.077202071346004,17.2,,,2024-08-06 220 | 219,12.7,12.988095169816727,13.7,13.999387945445292,13.8,16.0305416680485,16.7,,,2024-08-07 221 | 220,13.2,12.991409298756654,13.6,13.993899261954567,13.84,15.98196946518665,16.7,,,2024-08-08 222 | 221,13.0,12.995910120854708,13.66,13.988897920721552,13.84,15.93168673347453,18.1,,,2024-08-09 223 | 222,12.8,13.00215778458997,13.8,13.985187138982173,13.94,15.879954562830395,19.0,,,2024-08-10 224 | 223,12.1,13.010221258361664,13.66,13.983385462017894,13.94,15.828239776195772,18.9,,,2024-08-11 225 | 224,12.4,13.019786279204158,13.7,13.98381410229784,13.8,15.779139019625106,17.6,,,2024-08-12 226 | 225,12.3,13.030290683336593,13.76,13.986469003047302,13.94,15.735076559483115,17.8,,,2024-08-13 227 | 226,12.8,13.041565333960438,13.6,13.991199894968851,13.8,15.697357363300355,17.8,,,2024-08-14 228 | 227,12.1,13.053434993543563,13.66,13.997702484994464,13.84,15.66581632235521,17.6,,,2024-08-15 229 | 228,12.3,13.065846410320685,13.7,14.005479006784565,14.0,15.63912721116382,17.1,,,2024-08-16 230 | 229,12.7,13.078947548454607,13.66,14.014009106785757,13.84,15.615418691226246,16.8,,,2024-08-17 231 | 230,12.8,13.093112574837102,13.72,14.022899631998039,14.0,15.59255599245552,17.0,,,2024-08-18 232 | 231,12.7,13.10905597305624,13.7,14.031938822488785,14.0,15.567727323548752,16.2,,,2024-08-19 233 | 232,12.9,13.127637259547955,13.860000000000001,14.04122428639655,14.139999999999999,15.539064967696758,16.5,,,2024-08-20 234 | 233,13.3,13.149527081922827,13.9,14.0510371471461,14.2,15.505999079864548,17.2,,,2024-08-21 235 | 234,12.9,13.174519342803588,13.96,14.061585170916851,14.2,15.468914897769098,17.0,,,2024-08-22 236 | 235,13.1,13.201629979807837,13.92,14.072892510452032,14.1,15.428818088087022,16.4,,,2024-08-23 237 | 236,13.2,13.229411316228488,13.8,14.084613831352563,14.1,15.38726202115753,17.1,,,2024-08-24 238 | 237,13.1,13.256346093783757,13.7,14.096017359342067,14.0,15.345904588732846,17.9,,,2024-08-25 239 | 238,13.1,13.280863424905341,13.860000000000001,14.106179761775598,14.0,15.306076037235641,17.8,,,2024-08-26 240 | 239,13.0,13.301811763116364,14.0,14.114254644085996,14.18,15.2690262913685,16.7,,,2024-08-27 241 | 240,12.8,13.318399460728147,14.0,14.119630501688198,14.4,15.236184771314347,16.3,,,2024-08-28 242 | 241,13.3,13.330165327476632,13.860000000000001,14.121919030116729,14.1,15.208520424333376,16.5,,,2024-08-29 243 | 242,13.2,13.336765156760606,13.9,14.12087995396015,14.139999999999999,15.185903548312213,16.1,,,2024-08-30 244 | 243,12.7,13.338052811138432,14.0,14.116511376333538,14.2,15.167845678356093,16.3,,,2024-08-31 245 | 244,13.2,13.334127008438871,13.96,14.10905261115241,14.139999999999999,15.154096426622816,17.0,,,2024-09-01 246 | 245,13.4,13.325613633297237,13.96,14.099006912145818,14.24,15.144498574934529,18.6,,,2024-09-02 247 | 246,13.1,13.313685682093222,14.0,14.086964911772398,14.24,15.138536916750388,17.9,,,2024-09-03 248 | 247,13.3,13.29940474849913,14.0,14.073403291380263,14.3,15.135516376976653,17.9,,,2024-09-04 249 | 248,13.2,13.283767943626186,13.9,14.058544985073057,14.08,15.135064290908542,18.0,,,2024-09-05 250 | 249,13.4,13.2674007936527,13.8,14.042462717068124,14.0,15.137540333965608,17.7,,,2024-09-06 251 | 250,13.4,13.250622850721854,13.8,14.025439810534943,13.9,15.14365913325228,18.3,,,2024-09-07 252 | 251,13.1,13.233689882017815,13.76,14.007975745537514,14.139999999999999,15.15300898137059,17.1,,,2024-09-08 253 | 252,13.1,13.216635019838899,13.6,13.990530551322118,13.8,15.164793336344662,17.2,,,2024-09-09 254 | 253,12.8,13.19955345541633,13.76,13.97340050701313,13.84,15.178147296727447,16.9,,,2024-09-10 255 | 254,12.9,13.182490124386373,13.66,13.956727113040339,14.0,15.191648055351868,16.5,,,2024-09-11 256 | 255,12.5,13.165207194882075,13.46,13.940519162731846,13.88,15.20416345666686,16.2,,,2024-09-12 257 | 256,12.8,13.147997987039515,13.56,13.924928918619807,13.74,15.214622046489522,16.6,,,2024-09-13 258 | 257,12.8,13.13152057837521,13.42,13.910250359683438,13.68,15.221927829977492,16.8,,,2024-09-14 259 | 258,12.9,13.116663352491754,13.6,13.89681144732328,13.8,15.224894749055847,16.6,,,2024-09-15 260 | 259,12.9,13.104223865964068,13.52,13.88482261135011,13.84,15.22293513493288,16.4,,,2024-09-16 261 | 260,12.9,13.094380078999004,13.52,13.874316194888314,13.78,15.216038317891257,15.9,,,2024-09-17 262 | 261,12.2,13.086997733890325,13.52,13.865214010385257,13.74,15.20393333270195,16.4,,,2024-09-18 263 | 262,12.3,13.08157960496009,13.56,13.85741444172509,13.74,15.186598887638217,16.5,,,2024-09-19 264 | 263,12.3,13.07748689933833,13.6,13.850745409400497,13.8,15.163871835480656,17.0,,,2024-09-20 265 | 264,13.0,13.074172802320016,13.860000000000001,13.845006325152218,14.1,15.135907041157536,15.7,,,2024-09-21 266 | 265,13.0,13.070518056180035,13.66,13.839848990626828,13.94,15.103045889344966,16.0,,,2024-09-22 267 | 266,12.5,13.064496485914205,13.66,13.834596764960443,13.8,15.066073182190157,16.0,,,2024-09-23 268 | 267,12.5,13.054042743520474,13.66,13.828324184909855,13.9,15.025746007865369,17.1,,,2024-09-24 269 | 268,12.5,13.037352159252569,13.5,13.820105135374893,13.84,14.982444235995306,16.5,,,2024-09-25 270 | 269,13.0,13.013524125654532,13.6,13.809102166887843,13.88,14.936643648654615,17.1,,,2024-09-26 271 | 270,12.9,12.982266708572071,13.66,13.794730018097281,13.8,14.889596281471936,16.8,,,2024-09-27 272 | 271,13.0,12.943801257360166,13.6,13.7767776035261,13.94,14.843150756434182,16.1,,,2024-09-28 273 | 272,12.7,12.8990222592057,13.6,13.755389763319375,13.8,14.79939333108339,16.4,,,2024-09-29 274 | 273,12.6,12.848712344744156,13.46,13.731117110358857,13.9,14.759705251807944,16.3,,,2024-09-30 275 | 274,12.6,12.793561809088308,13.5,13.704896898506423,13.74,14.725125197962383,16.9,,,2024-10-01 276 | 275,12.7,12.734304233986736,13.6,13.677753841685846,13.8,14.69656725850157,16.7,,,2024-10-02 277 | 276,12.6,12.671724018654778,13.46,13.650388657082441,13.8,14.67403882240778,17.2,,,2024-10-03 278 | 277,12.7,12.6069487940785,13.5,13.62328556738943,13.74,14.656668936180674,16.0,,,2024-10-04 279 | 278,12.7,12.540860482084424,13.4,13.596572792470301,13.7,14.642975981900134,16.7,,,2024-10-05 280 | 279,11.1,12.473816585972722,13.5,13.570008252688664,13.7,14.63217209744939,16.8,,,2024-10-06 281 | 280,11.3,12.405615475770196,13.360000000000001,13.543229683965842,13.54,14.624451473537373,15.5,,,2024-10-07 282 | 281,11.7,12.33694179836044,13.16,13.516185816337558,13.6,14.619598967767521,15.3,,,2024-10-08 283 | 282,11.5,12.269142060717941,13.2,13.489175836507593,13.48,14.617661295827192,16.1,,,2024-10-09 284 | 283,11.4,12.203821075012476,13.1,13.462683192817893,13.44,14.619726881020357,16.6,,,2024-10-10 285 | 284,12.0,12.142357358481835,12.860000000000001,13.437305646289666,13.4,14.626889041524771,16.3,,,2024-10-11 286 | 285,12.1,12.085645156050349,12.9,13.413776778165474,13.28,14.640670343135467,17.4,,,2024-10-12 287 | 286,11.7,12.034474952135348,13.0,13.392964287564268,13.52,14.662785711008357,17.4,,,2024-10-13 288 | 287,11.5,11.989508191113936,13.040000000000001,13.375548330473977,13.74,14.693872286011606,17.8,,,2024-10-14 289 | 288,11.7,11.951171611482383,13.16,13.362080296932044,13.7,14.732908276056742,17.5,,,2024-10-15 290 | 289,10.6,11.919328179482045,13.2,13.35283281291418,13.5,14.777849256856115,15.7,,,2024-10-16 291 | 290,11.5,11.893319459458343,13.2,13.34775760024937,13.5,14.8264720341374,15.1,,,2024-10-17 292 | 291,11.7,11.871999935316914,13.26,13.346454249064909,13.5,14.876596219902302,15.1,,,2024-10-18 293 | 292,11.4,11.853893133131129,13.16,13.348178919884914,13.7,14.926178002007308,15.2,,,2024-10-19 294 | 293,11.2,11.837620530186143,13.0,13.352070886248471,13.48,14.973205977043621,16.2,,,2024-10-20 295 | 294,11.8,11.822202121517181,13.0,13.357431550729347,13.7,15.016628918994808,15.6,,,2024-10-21 296 | 295,11.6,11.807907505181461,12.98,13.3639521951104,13.639999999999999,15.056032946781379,15.5,,,2024-10-22 297 | 296,11.8,11.795091405786145,13.02,13.371670121297273,13.38,15.091813147993532,15.1,,,2024-10-23 298 | 297,11.0,11.783419691724697,13.06,13.380751544034652,13.54,15.124474549447212,15.5,,,2024-10-24 299 | 298,11.6,11.772764535557277,12.9,13.391467565117843,13.639999999999999,15.154502145768252,16.3,,,2024-10-25 300 | 299,11.5,11.764212792830952,12.7,13.404282950482,13.74,15.182431242638797,16.4,,,2024-10-26 301 | 300,11.2,11.759180849170313,12.7,13.419614337230541,13.24,15.208657749386195,15.8,,,2024-10-27 302 | 301,11.7,11.758747464243758,13.06,13.437537443656785,13.6,15.233368534045537,15.8,,,2024-10-28 303 | 302,11.4,11.763386826904377,13.1,13.45764858498551,13.44,15.256514801625762,15.8,,,2024-10-29 304 | 303,11.3,11.772507791405403,13.16,13.47911919623865,13.6,15.277419153181736,16.0,,,2024-10-30 305 | 304,11.2,11.785024441184282,13.1,13.500887210848271,13.84,15.294842258189078,16.1,,,2024-10-31 306 | 305,10.6,11.79982474508492,13.4,13.521750549702267,13.74,15.307168647831313,15.8,,,2024-11-01 307 | 306,10.8,11.815241961488743,13.32,13.540515210793021,13.84,15.31252410825325,15.3,,,2024-11-02 308 | 307,11.4,11.830327810592376,13.26,13.556437050445968,13.88,15.30949502253787,15.7,,,2024-11-03 309 | 308,11.5,11.845234460110465,13.3,13.569137676099896,13.88,15.2973387695649,15.5,,,2024-11-04 310 | 309,11.1,11.860534686303918,13.5,13.578677929342843,13.94,15.276334131845177,15.5,,,2024-11-05 311 | 310,10.8,11.876138267808836,13.6,13.585473690658556,13.94,15.248185825525173,15.7,,,2024-11-06 312 | 311,11.1,11.891537536952738,13.66,13.59019650701654,13.94,15.215295839120595,15.5,,,2024-11-07 313 | 312,11.6,11.90663319517264,13.6,13.5934937954664,13.94,15.180084869715325,15.4,,,2024-11-08 314 | 313,11.8,11.921378356908152,13.76,13.595809686972444,14.2,15.144573781507574,15.8,,,2024-11-09 315 | 314,11.8,11.935211407605664,13.7,13.597277573095354,14.08,15.109850559543466,16.0,,,2024-11-10 316 | 315,11.5,11.947084137050354,13.3,13.597785510513699,14.139999999999999,15.076419969477518,16.1,,,2024-11-11 317 | 316,11.8,11.95493417712014,13.46,13.597131477197127,13.74,15.044772263836213,16.6,,,2024-11-12 318 | 317,11.5,11.956711371476905,13.3,13.594727419318145,13.54,15.01479119095992,17.2,,,2024-11-13 319 | 318,11.7,11.951379305270068,13.3,13.589889862323503,13.7,14.985741272547013,16.4,,,2024-11-14 320 | 319,11.6,11.938688782865885,13.32,13.582135239125979,13.74,14.95581693890987,16.2,,,2024-11-15 321 | 320,12.0,11.919106320082328,13.12,13.571284859448344,13.6,14.923762484259608,15.3,,,2024-11-16 322 | 321,11.9,11.892960274872104,13.280000000000001,13.557260865897854,13.7,14.889671114415332,15.3,,,2024-11-17 323 | 322,11.5,11.86105874509787,13.46,13.540053668515363,13.94,14.85466565270179,15.8,,,2024-11-18 324 | 323,11.6,11.824589136516172,13.56,13.519821851743833,14.0,14.819676709345089,15.5,,,2024-11-19 325 | 324,11.4,11.784001620910235,13.46,13.496939034850856,13.92,14.786172713617397,15.1,,,2024-11-20 326 | 325,11.2,11.739199708053508,13.5,13.47210410695108,13.9,14.755907881429705,15.2,,,2024-11-21 327 | 326,11.3,11.690674315643655,13.5,13.446372820734453,14.0,14.731107463831497,15.2,,,2024-11-22 328 | 327,10.8,11.640260431467192,13.66,13.421073001287473,13.94,14.713627327331634,15.3,,,2024-11-23 329 | 328,11.4,11.590546623655017,13.5,13.397434626761523,13.7,14.703674357468717,15.5,,,2024-11-24 330 | 329,11.5,11.544088300563503,13.3,13.376263052666042,13.74,14.700071498591914,16.3,,,2024-11-25 331 | 330,11.1,11.50312490981042,13.4,13.357676506633414,13.7,14.700434485565559,15.8,,,2024-11-26 332 | 331,9.6,11.469027526133182,13.0,13.341252753525533,13.3,14.70239059106044,15.4,,,2024-11-27 333 | 332,9.3,11.441859161007853,12.76,13.326348886546889,13.139999999999999,14.704550363685163,15.2,,,2024-11-28 334 | 333,10.2,11.420227173210925,12.860000000000001,13.31240012848497,13.34,14.706602429694273,15.1,,,2024-11-29 335 | 334,10.7,11.403071304316121,12.6,13.298871320905244,13.459999999999999,14.7087179244279,15.1,,,2024-11-30 336 | 335,11.2,11.39029692962534,12.8,13.285202276500282,13.56,14.710960254872468,15.3,,,2024-12-01 337 | 336,11.2,11.381860311937828,13.22,13.271222240368038,13.7,14.713020460089071,15.1,,,2024-12-02 338 | 337,10.2,11.377764488265221,12.8,13.257221995165038,13.48,14.714468190450194,14.9,,,2024-12-03 339 | 338,9.7,11.377125176456476,12.96,13.243830801304055,13.44,14.715351737196134,15.0,,,2024-12-04 340 | 339,9.9,11.378858581147927,12.66,13.232097086104703,13.5,14.716230216073866,15.6,,,2024-12-05 341 | 340,10.2,11.38250004339652,12.96,13.223308431103595,13.4,14.7179733410225,15.7,,,2024-12-06 342 | 341,10.1,11.388937561971119,13.08,13.218391784431027,13.5,14.72132130729919,14.9,,,2024-12-07 343 | 342,10.8,11.399192792708481,13.4,13.21730412611983,13.8,14.725850076055625,14.9,,,2024-12-08 344 | 343,11.2,11.413361045063628,13.46,13.218978750172887,13.9,14.730490895477828,14.9,,,2024-12-09 345 | 344,10.9,11.430914635951563,13.3,13.22189632281183,13.84,14.734070007566306,14.8,,,2024-12-10 346 | 345,10.8,11.450572593661766,13.0,13.22494827534628,13.92,14.736007085306468,15.2,,,2024-12-11 347 | 346,11.0,11.47137361129704,13.42,13.2279630836518,13.92,14.736606834130187,15.4,,,2024-12-12 348 | 347,10.8,11.493046960168998,13.06,13.23138023503523,13.5,14.736491268728944,15.2,,,2024-12-13 349 | 348,10.4,11.51539420488824,12.76,13.235378449014789,13.319999999999999,14.73642792862513,15.4,,,2024-12-14 350 | 349,10.2,11.537882822566095,13.06,13.239944044584323,13.34,14.736949620761134,14.9,,,2024-12-15 351 | 350,10.8,11.560008086694598,12.96,13.245207270086654,13.54,14.738392902537758,14.7,,,2024-12-16 352 | 351,10.1,11.58144655063202,13.14,13.251225576731182,13.54,14.74149827758236,15.7,,,2024-12-17 353 | 352,9.6,11.603230573159527,13.14,13.263464952829379,13.54,14.754245426865007,15.2,,,2024-12-18 354 | 353,10.6,11.623714386175303,13.0,13.275235873609844,13.54,14.76829646961934,15.5,,,2024-12-19 355 | 354,11.2,11.643385524280504,12.780000000000001,13.286288948299918,13.54,14.782837110873363,15.9,,,2024-12-20 356 | 355,11.6,11.662718039266391,12.66,13.296964804001922,13.639999999999999,14.79769077354831,14.8,,,2024-12-21 357 | 356,11.6,11.681993059141078,12.96,13.307567131679454,13.52,14.812782777203852,14.5,,,2024-12-22 358 | 357,11.1,11.701346399430754,12.8,13.318325153104414,13.639999999999999,14.828127871093283,15.0,,,2024-12-23 359 | 358,10.4,11.72084480748282,12.96,13.329413125077108,13.52,14.843797514183581,15.5,,,2024-12-24 360 | 359,10.7,11.740496001354392,13.02,13.340965143645224,13.719999999999999,14.859863537014268,15.4,,,2024-12-25 361 | 360,11.2,11.760329097605695,13.4,13.353077070244932,13.94,14.876353355908037,15.2,,,2024-12-26 362 | 361,11.6,11.78038533631914,13.7,13.365802185982274,14.0,14.893274252605424,15.6,,,2024-12-27 363 | 362,11.8,11.800679540848954,13.360000000000001,13.379142110090438,13.8,14.91062843749329,15.0,,,2024-12-28 364 | 363,11.5,11.821164986794006,13.26,13.393053967887086,13.88,14.92842852762563,16.3,,,2024-12-29 365 | 364,11.6,11.841757264513658,12.9,13.407456334262498,13.38,14.946701690060483,15.7,,,2024-12-30 366 | 365,11.2,11.862368445224284,13.0,13.422242002008597,13.74,14.965457694562406,15.4,,,2024-12-31 367 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Lato:wght@200&display=swap"); 2 | 3 | html { 4 | font-family: "Lato, Arial, sans-serif"; 5 | font-size: 1.2rem; 6 | } 7 | 8 | /* Widget labels */ 9 | .css-16idsys p, 10 | .e1y5xkzn3 p, 11 | .e1nzilvr4 p, 12 | .e121c1cl0 p { 13 | font-size: 1.1rem; 14 | } 15 | 16 | /* Margin below widget labels */ 17 | .css-81oif8, 18 | .e1jram340 { 19 | margin-bottom: 0.1rem; 20 | } 21 | 22 | /* Main container */ 23 | .css-z5fcl4 { 24 | padding-top: 1.5rem; 25 | } 26 | 27 | div.css-j5r0tf { 28 | min-width: 285px; 29 | } 30 | 31 | /* Make Folium map responsive */ 32 | [title~="st.iframe"] { 33 | min-height: 500px; 34 | width: 100%; 35 | } 36 | 37 | /* Remove blank space at top and bottom */ 38 | .block-container { 39 | padding-top: 0rem; 40 | padding-bottom: 0rem; 41 | } 42 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yotkadata/meteo_hist/d03b3b6d3887b85886685b69a07a5972adf8e068/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit tests (pytest) for the MeteoHist class. 3 | """ 4 | 5 | import datetime as dt 6 | from pathlib import Path 7 | 8 | import pandas as pd 9 | import pytest 10 | import requests 11 | 12 | from meteo_hist.base import MeteoHist 13 | 14 | 15 | @pytest.fixture(name="meteohist_instance_default") 16 | def fixture_meteohist_instance_default(): 17 | """ 18 | Fixture to create an instance of MeteoHist for testing. 19 | """ 20 | coords = (52.5170365, 13.3888599) # Berlin coordinates 21 | 22 | # Construct the path to the sample data file relative to this test file 23 | sample_data_path = ( 24 | Path(__file__).parent.parent 25 | / "sample_data/raw/temperature_mean-berlin-germany.csv" 26 | ) 27 | 28 | # Use sample data to prevent network requests and API rate limits 29 | data = pd.read_csv(sample_data_path, parse_dates=["date"]) 30 | 31 | return MeteoHist(coords, data=data) 32 | 33 | 34 | def test_get_data(): 35 | """ 36 | Test if data is fetched correctly on initialization. 37 | """ 38 | coords = (52.5170365, 13.3888599) # Berlin coordinates 39 | 40 | class_instance = MeteoHist(coords) 41 | 42 | assert isinstance( 43 | class_instance.data_raw, pd.DataFrame 44 | ), f"data_raw attribute should be a Pandas DataFrame, but is {type(class_instance.data_raw)}" 45 | assert isinstance( 46 | class_instance.data, pd.DataFrame 47 | ), f"data attribute should be a Pandas DataFrame, but is {type(class_instance.data)}" 48 | 49 | 50 | def test_year_attr(meteohist_instance_default): 51 | """ 52 | Test the if MeteoHist with default parameters correctly sets year attribute. 53 | """ 54 | year = meteohist_instance_default.year 55 | current_year = dt.datetime.now().year 56 | 57 | assert ( 58 | year == current_year 59 | ), f"Year attribute should be current year {current_year}, {year} found." 60 | 61 | 62 | def test_ref_period_attr(meteohist_instance_default): 63 | """ 64 | Test the if MeteoHist with default parameters correctly sets reference_period attribute. 65 | """ 66 | reference_period = meteohist_instance_default.reference_period 67 | 68 | assert isinstance( 69 | reference_period, tuple 70 | ), f"reference_period attribute should be a tuple, {type(reference_period)} found." 71 | 72 | assert isinstance(reference_period[0], int) and isinstance( 73 | reference_period[1], int 74 | ), ( 75 | "reference_period attribute should be a tuple of two integers, " 76 | f"{type(reference_period[0])} and {type(reference_period[1])} found." 77 | ) 78 | 79 | assert len(reference_period) == 2, ( 80 | "reference_period attribute should be a tuple of two integers, " 81 | f"{len(reference_period)} found." 82 | ) 83 | 84 | assert ( 85 | reference_period[1] - reference_period[0] == 29 86 | ), "reference_period should be 30 years long." 87 | 88 | valid_start_years = [1941, 1951, 1961, 1971, 1981, 1991] 89 | assert reference_period[0] in valid_start_years, ( 90 | "reference_period should start in one of the following years: " 91 | f"{valid_start_years}, {reference_period[0]} found." 92 | ) 93 | 94 | valid_end_years = [1970, 1980, 1990, 2000, 2010, 2020] 95 | assert reference_period[1] in valid_end_years, ( 96 | "reference_period should end in one of the following years: " 97 | f"{valid_end_years}, {reference_period[1]} found." 98 | ) 99 | 100 | 101 | def test_transform_df_dates(meteohist_instance_default): 102 | """ 103 | Test the if MeteoHist with default parameters correctly transforms dates. 104 | """ 105 | 106 | assert isinstance(meteohist_instance_default.data, pd.DataFrame), ( 107 | "data attribute should be a Pandas DataFrame, " 108 | f"{type(meteohist_instance_default.data)} found." 109 | ) 110 | 111 | 112 | def test_transform_df_values(): 113 | """ 114 | Test the if MeteoHist correctly keeps values in line. 115 | """ 116 | 117 | coords = (52.5170365, 13.3888599) # Berlin coordinates 118 | instance = MeteoHist(coords, year=2020) 119 | 120 | # Get raw data 121 | df_raw = instance.data_raw 122 | 123 | # Transformed data 124 | df_t = instance.data 125 | 126 | # Pick some dates 127 | dates = ["2020-02-28", "2020-01-01", "2020-07-06", "2020-09-16", "2020-12-31"] 128 | 129 | # Compare values for each date 130 | for date in dates: 131 | value_r = df_raw[df_raw["date"] == date]["value"].values[0] 132 | value_t = df_t[df_t["date"] == date]["2020"].values[0] 133 | 134 | assert ( 135 | value_r == value_t 136 | ), f"Value for {date} should be {value_r}, {value_t} found." 137 | 138 | 139 | def test_get_location(meteohist_instance_default): 140 | """ 141 | Test the get_location method for valid coordinates. 142 | """ 143 | location = meteohist_instance_default.get_location((52.5170365, 13.3888599)) 144 | assert location is not None, "Location should not be None for valid coordinates." 145 | 146 | 147 | def test_get_data_with_invalid_coords(meteohist_instance_default): 148 | """ 149 | Test get_data method with invalid coordinates. 150 | """ 151 | with pytest.raises(requests.exceptions.RequestException): 152 | meteohist_instance_default.get_data(coords=(999, 999)) 153 | 154 | 155 | def test_dayofyear_to_date(meteohist_instance_default): 156 | """ 157 | Test dayofyear_to_date method. 158 | """ 159 | date = meteohist_instance_default.dayofyear_to_date(2020, 60) 160 | assert date == dt.datetime( 161 | 2020, 2, 29 162 | ), "Date should be Feb 29, 2020 for day 60 of a leap year." 163 | 164 | 165 | def test_dayofyear_to_date_non_leap(meteohist_instance_default): 166 | """ 167 | Test dayofyear_to_date method for non-leap year. 168 | """ 169 | date = meteohist_instance_default.dayofyear_to_date(2021, 60) 170 | assert date == dt.datetime( 171 | 2021, 3, 1 172 | ), "Date should be Mar 1, 2021 for day 60 of a non-leap year." 173 | 174 | 175 | def test_get_stats_for_year(meteohist_instance_default): 176 | """ 177 | Test get_stats_for_year method. 178 | """ 179 | stats = meteohist_instance_default.get_stats_for_year(2020) 180 | assert isinstance(stats, dict), "Stats should be returned as a dictionary." 181 | 182 | 183 | def test_get_stats_for_period(meteohist_instance_default): 184 | """ 185 | Test get_stats_for_period method. 186 | """ 187 | stats = meteohist_instance_default.get_stats_for_period((2010, 2020)) 188 | assert isinstance(stats, pd.DataFrame), "Stats should be returned as a DataFrame." 189 | 190 | 191 | def test_remove_leap_days(meteohist_instance_default): 192 | """ 193 | Test remove_leap_days method. 194 | """ 195 | data_with_leap = pd.DataFrame( 196 | { 197 | "date": pd.to_datetime(["2020-02-28", "2020-02-29", "2020-03-01"]), 198 | "value": [1, 2, 3], 199 | } 200 | ) 201 | data_without_leap = meteohist_instance_default.remove_leap_days(data_with_leap) 202 | assert len(data_without_leap) == 2, "Data should not contain leap day." 203 | 204 | 205 | def test_transform_data_smooth(meteohist_instance_default): 206 | """ 207 | Test transform_data method with smoothing. 208 | """ 209 | data = meteohist_instance_default.data 210 | assert "mean" in data.columns, "Transformed data should include mean column." 211 | assert "p05" in data.columns, "Transformed data should include p05 column." 212 | assert "p95" in data.columns, "Transformed data should include p95 column." 213 | 214 | 215 | def test_get_y_limits(meteohist_instance_default): 216 | """ 217 | Test get_y_limits method. 218 | """ 219 | min_y, max_y = meteohist_instance_default.get_y_limits() 220 | assert isinstance(min_y, float) and isinstance( 221 | max_y, float 222 | ), "Y limits should be floats." 223 | 224 | 225 | def test_get_min_max(meteohist_instance_default): 226 | """ 227 | Test get_min_max method. 228 | """ 229 | min_val = meteohist_instance_default.get_min_max((1, 365), "min") 230 | max_val = meteohist_instance_default.get_min_max((1, 365), "max") 231 | assert isinstance(min_val, float) and isinstance( 232 | max_val, float 233 | ), "Min and max values should be floats." 234 | --------------------------------------------------------------------------------