diff --git a/.gitignore b/.gitignore index 161ffb07..6ff575f7 100644 --- a/.gitignore +++ b/.gitignore @@ -187,4 +187,4 @@ static/ AGENTS.md !rdagent/**/AGENTS.md -scripts/ \ No newline at end of file +scripts/ diff --git a/Makefile b/Makefile index f6611574..0820eeec 100644 --- a/Makefile +++ b/Makefile @@ -83,11 +83,11 @@ constraints: deepclean # Check lint with black. black: - $(PIPRUN) python -m black --check --diff . --extend-exclude "(test/scripts|test/notebook/testfiles|git_ignore_folder)" -l 120 + $(PIPRUN) python -m black --check --diff . --extend-exclude "(test/scripts|test/notebook/testfiles|git_ignore_folder|web)" -l 120 # Check lint with isort. isort: - $(PIPRUN) python -m isort --check . -s git_ignore_folder -s test/scripts -s test/notebook/testfiles + $(PIPRUN) python -m isort --check . -s git_ignore_folder -s test/scripts -s test/notebook/testfiles -s web # Check lint with mypy. # First deal with the core folder, and then gradually increase the scope of detection, @@ -120,11 +120,11 @@ pre-commit: # Auto lint with black. auto-black: - $(PIPRUN) python -m black . --extend-exclude "(test/scripts|test/notebook/testfiles|git_ignore_folder|.venv)" -l 120 + $(PIPRUN) python -m black . --extend-exclude "(test/scripts|test/notebook/testfiles|git_ignore_folder|.venv|web)" -l 120 # Auto lint with isort. auto-isort: - $(PIPRUN) python -m isort . -s git_ignore_folder -s test/scripts -s test/notebook/testfiles -s .venv + $(PIPRUN) python -m isort . -s git_ignore_folder -s test/scripts -s test/notebook/testfiles -s .venv -s web # Auto lint with toml-sort. auto-toml-sort: diff --git a/README.md b/README.md index df72a820..aed48bd4 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ # 📰 News | 🗞️ News | 📝 Description | | -- | ------ | +| Web UI Release | We release a new frontend that can be built and served by `rdagent server_ui` for real-time interaction and trace viewing, currently excluding the `data_science` scenario. | | NeurIPS 2025 Acceptance | We are thrilled to announce that our paper [R&D-Agent-Quant](https://arxiv.org/abs/2505.15155) has been accepted to NeurIPS 2025 | | [Technical Report Release](#overall-technical-report) | Overall framework description and results on MLE-bench | | [R&D-Agent-Quant Release](#deep-application-in-diverse-scenarios) | Apply R&D-Agent to quant trading | @@ -337,21 +338,53 @@ The **[🖥️ Live Demo](https://rdagent.azurewebsites.net/)** is implemented b ``` ### 🖥️ Monitor the Application Results -- You can run the following command for our demo program to see the run logs. +#### Streamlit UI - ```sh - rdagent ui --port 19899 --log-dir --data-science - ``` +Use the Streamlit UI to view run logs, especially for the `data_science` scenario. -- About the `data_science` parameter: If you want to see the logs of the data science scenario, set the `data_science` parameter to `True`; otherwise set it to `False`. - -- Although port 19899 is not commonly used, but before you run this demo, you need to check if port 19899 is occupied. If it is, please change it to another port that is not occupied. +```sh +rdagent ui --port 19899 --log-dir --data-science +``` - You can check if a port is occupied by running the following command. +About the `data_science` parameter: If you want to see the logs of the data science scenario, set the `data_science` parameter to `True`; otherwise set it to `False`. - ```sh - rdagent health_check --no-check-env --no-check-docker - ``` +#### Web UI + +We also provide a separate web frontend in `web/` for the Flask backend started by `server_ui`. + +**NOTE:** This web UI is different from `rdagent ui`. The current web UI does not support the `data_science` scenario yet. For the `data_science` scenario, please continue to use `rdagent ui --data-science`. + +```sh +cd web +npm install +``` + +To build the frontend for the Flask backend, generate the static assets into the default directory used by `server_ui`: + +```sh +cd web +npm run build:flask +``` + +By default, `server_ui` serves static files from `./git_ignore_folder/static`. If you need a different location, set the `UI_STATIC_PATH` environment variable before starting the backend. + +Start the Flask backend and serve the built frontend together with the real-time APIs: + +```sh +rdagent server_ui --port 19899 +``` + +After that, open `http://127.0.0.1:19899` in your browser. + +#### Common Notes + +Port `19899` is used in the examples above. Before starting either UI, check whether this port is already occupied. If it is, please change it to another available port. + +You can check whether the port is occupied by running: + +```sh +rdagent health_check --no-check-env --no-check-docker +``` # 🏭 Scenarios diff --git a/rdagent/app/cli.py b/rdagent/app/cli.py index dad1920f..07937172 100644 --- a/rdagent/app/cli.py +++ b/rdagent/app/cli.py @@ -16,10 +16,13 @@ load_dotenv(".env") import subprocess from importlib.resources import path as rpath +from typing import Optional import typer +from typing_extensions import Annotated from rdagent.app.data_science.loop import main as data_science +from rdagent.app.finetune.llm.loop import main as llm_finetune from rdagent.app.general_model.general_model import ( extract_models_and_implement as general_model, ) @@ -33,6 +36,11 @@ from rdagent.log.mle_summary import grade_summary as grade_summary app = typer.Typer() +CheckoutOption = Annotated[bool, typer.Option("--checkout/--no-checkout", "-c/-C")] +CheckEnvOption = Annotated[bool, typer.Option("--check-env/--no-check-env", "-e/-E")] +CheckDockerOption = Annotated[bool, typer.Option("--check-docker/--no-check-docker", "-d/-D")] +CheckPortsOption = Annotated[bool, typer.Option("--check-ports/--no-check-ports", "-p/-P")] + def ui(port=19899, log_dir="", debug: bool = False, data_science: bool = False): """ @@ -56,9 +64,11 @@ def ui(port=19899, log_dir="", debug: bool = False, data_science: bool = False): def server_ui(port=19899): """ - start web app to show the log traces in real time + start the Flask log server in real time """ - subprocess.run(["python", "rdagent/log/server/app.py", f"--port={port}"]) + from rdagent.log.server.app import main as log_server_main + + log_server_main(port=port) def ds_user_interact(port=19900): @@ -69,17 +79,123 @@ def ds_user_interact(port=19900): subprocess.run(commands) -app.command(name="fin_factor")(fin_factor) -app.command(name="fin_model")(fin_model) -app.command(name="fin_quant")(fin_quant) -app.command(name="fin_factor_report")(fin_factor_report) -app.command(name="general_model")(general_model) -app.command(name="data_science")(data_science) -app.command(name="grade_summary")(grade_summary) +@app.command(name="fin_factor") +def fin_factor_cli( + path: Optional[str] = None, + step_n: Optional[int] = None, + loop_n: Optional[int] = None, + all_duration: Optional[str] = None, + checkout: CheckoutOption = True, +): + fin_factor(path=path, step_n=step_n, loop_n=loop_n, all_duration=all_duration, checkout=checkout) + + +@app.command(name="fin_model") +def fin_model_cli( + path: Optional[str] = None, + step_n: Optional[int] = None, + loop_n: Optional[int] = None, + all_duration: Optional[str] = None, + checkout: CheckoutOption = True, +): + fin_model(path=path, step_n=step_n, loop_n=loop_n, all_duration=all_duration, checkout=checkout) + + +@app.command(name="fin_quant") +def fin_quant_cli( + path: Optional[str] = None, + step_n: Optional[int] = None, + loop_n: Optional[int] = None, + all_duration: Optional[str] = None, + checkout: CheckoutOption = True, +): + fin_quant(path=path, step_n=step_n, loop_n=loop_n, all_duration=all_duration, checkout=checkout) + + +@app.command(name="fin_factor_report") +def fin_factor_report_cli( + report_folder: Optional[str] = None, + path: Optional[str] = None, + all_duration: Optional[str] = None, + checkout: CheckoutOption = True, +): + fin_factor_report(report_folder=report_folder, path=path, all_duration=all_duration, checkout=checkout) + + +@app.command(name="general_model") +def general_model_cli(report_file_path: str): + general_model(report_file_path) + + +@app.command(name="data_science") +def data_science_cli( + path: Optional[str] = None, + checkout: CheckoutOption = True, + step_n: Optional[int] = None, + loop_n: Optional[int] = None, + timeout: Optional[str] = None, + competition: Optional[str] = None, +): + data_science( + path=path, + checkout=checkout, + step_n=step_n, + loop_n=loop_n, + timeout=timeout, + competition=competition, + ) + + +@app.command(name="llm_finetune") +def llm_finetune_cli( + path: Optional[str] = None, + checkout: CheckoutOption = True, + benchmark: Optional[str] = None, + benchmark_description: Optional[str] = None, + dataset: Optional[str] = None, + base_model: Optional[str] = None, + upper_data_size_limit: Optional[int] = None, + step_n: Optional[int] = None, + loop_n: Optional[int] = None, + timeout: Optional[str] = None, +): + llm_finetune( + path=path, + checkout=checkout, + benchmark=benchmark, + benchmark_description=benchmark_description, + dataset=dataset, + base_model=base_model, + upper_data_size_limit=upper_data_size_limit, + step_n=step_n, + loop_n=loop_n, + timeout=timeout, + ) + + +@app.command(name="grade_summary") +def grade_summary_cli(log_folder: str): + grade_summary(log_folder) + + app.command(name="ui")(ui) app.command(name="server_ui")(server_ui) -app.command(name="health_check")(health_check) -app.command(name="collect_info")(collect_info) + + +@app.command(name="health_check") +def health_check_cli( + check_env: CheckEnvOption = True, + check_docker: CheckDockerOption = True, + check_ports: CheckPortsOption = True, +): + health_check(check_env=check_env, check_docker=check_docker, check_ports=check_ports) + + +@app.command(name="collect_info") +def collect_info_cli(): + collect_info() + + app.command(name="ds_user_interact")(ds_user_interact) diff --git a/rdagent/app/data_science/loop.py b/rdagent/app/data_science/loop.py index 5d68429c..b2d08abf 100644 --- a/rdagent/app/data_science/loop.py +++ b/rdagent/app/data_science/loop.py @@ -3,8 +3,6 @@ from pathlib import Path from typing import Optional import fire -import typer -from typing_extensions import Annotated from rdagent.app.data_science.conf import DS_RD_SETTING from rdagent.core.utils import import_class @@ -14,7 +12,7 @@ from rdagent.scenarios.data_science.loop import DataScienceRDLoop def main( path: Optional[str] = None, - checkout: Annotated[bool, typer.Option("--checkout/--no-checkout", "-c/-C")] = True, + checkout: bool = True, checkout_path: Optional[str] = None, step_n: Optional[int] = None, loop_n: Optional[int] = None, diff --git a/rdagent/app/finetune/llm/loop.py b/rdagent/app/finetune/llm/loop.py index 1839020a..9f2bea1e 100644 --- a/rdagent/app/finetune/llm/loop.py +++ b/rdagent/app/finetune/llm/loop.py @@ -7,8 +7,7 @@ Standard RDLoop entry point for LLM fine-tuning, consistent with data science im import asyncio from typing import Optional, cast -import typer -from typing_extensions import Annotated +import fire from rdagent.app.finetune.llm.conf import FT_RD_SETTING from rdagent.log import rdagent_logger as logger @@ -17,7 +16,7 @@ from rdagent.scenarios.finetune.loop import LLMFinetuneRDLoop def main( path: Optional[str] = None, - checkout: Annotated[bool, typer.Option("--checkout/--no-checkout", "-c/-C")] = True, + checkout: bool = True, user_target_scenario: Optional[str] = None, benchmark: Optional[str] = None, benchmark_description: Optional[str] = None, @@ -98,4 +97,4 @@ def main( if __name__ == "__main__": - typer.run(main) + fire.Fire(main) diff --git a/rdagent/app/qlib_rd_loop/conf.py b/rdagent/app/qlib_rd_loop/conf.py index 891157ed..13dade7c 100644 --- a/rdagent/app/qlib_rd_loop/conf.py +++ b/rdagent/app/qlib_rd_loop/conf.py @@ -45,7 +45,7 @@ class ModelBasePropSetting(BasePropSetting): test_start: str = "2017-01-01" """Start date of the test / backtest segment""" - test_end: Optional[str] = None + test_end: Optional[str] = "2020-08-01" """End date of the test / backtest segment""" @@ -89,7 +89,7 @@ class FactorBasePropSetting(BasePropSetting): test_start: str = "2017-01-01" """Start date of the test / backtest segment""" - test_end: Optional[str] = None + test_end: Optional[str] = "2020-08-01" """End date of the test / backtest segment""" @@ -102,10 +102,10 @@ class FactorFromReportPropSetting(FactorBasePropSetting): report_result_json_file_path: str = "git_ignore_folder/report_list.json" """Path to the JSON file listing research reports for factor extraction""" - max_factors_per_exp: int = 10000 + max_factors_per_exp: int = 6 """Maximum number of factors implemented per experiment""" - report_limit: int = 10000 + report_limit: int = 20 """Maximum number of reports to process""" @@ -166,7 +166,7 @@ class QuantBasePropSetting(BasePropSetting): test_start: str = "2017-01-01" """Start date of the test / backtest segment""" - test_end: Optional[str] = None + test_end: Optional[str] = "2020-08-01" """End date of the test / backtest segment""" diff --git a/rdagent/app/qlib_rd_loop/factor.py b/rdagent/app/qlib_rd_loop/factor.py index c43f5123..7f7736a4 100755 --- a/rdagent/app/qlib_rd_loop/factor.py +++ b/rdagent/app/qlib_rd_loop/factor.py @@ -7,8 +7,6 @@ from pathlib import Path from typing import Any, Optional import fire -import typer -from typing_extensions import Annotated from rdagent.app.qlib_rd_loop.conf import FACTOR_PROP_SETTING from rdagent.components.workflow.rd_loop import RDLoop @@ -34,8 +32,10 @@ def main( step_n: Optional[int] = None, loop_n: Optional[int] = None, all_duration: str | None = None, - checkout: Annotated[bool, typer.Option("--checkout/--no-checkout", "-c/-C")] = True, + checkout: bool = True, checkout_path: Optional[str] = None, + base_features_path: Optional[str] = None, + **kwargs, ): """ Auto R&D Evolving loop for fintech factors. @@ -51,10 +51,15 @@ def main( checkout = Path(checkout_path) if path is None: - model_loop = FactorRDLoop(FACTOR_PROP_SETTING) + factor_loop = FactorRDLoop(FACTOR_PROP_SETTING) else: - model_loop = FactorRDLoop.load(path, checkout=checkout) - asyncio.run(model_loop.run(step_n=step_n, loop_n=loop_n, all_duration=all_duration)) + factor_loop = FactorRDLoop.load(path, checkout=checkout) + + factor_loop._init_base_features(base_features_path) + if "user_interaction_queues" in kwargs and kwargs["user_interaction_queues"] is not None: + factor_loop._set_interactor(*kwargs["user_interaction_queues"]) + factor_loop._interact_init_params() + asyncio.run(factor_loop.run(step_n=step_n, loop_n=loop_n, all_duration=all_duration)) if __name__ == "__main__": diff --git a/rdagent/app/qlib_rd_loop/factor_from_report.py b/rdagent/app/qlib_rd_loop/factor_from_report.py index 60293fc3..0759f1f7 100644 --- a/rdagent/app/qlib_rd_loop/factor_from_report.py +++ b/rdagent/app/qlib_rd_loop/factor_from_report.py @@ -126,6 +126,9 @@ class FactorReportLoop(FactorRDLoop, metaclass=LoopMeta): ] exp.sub_workspace_list = exp.sub_workspace_list[: FACTOR_FROM_REPORT_PROP_SETTING.max_factors_per_exp] exp.sub_tasks = exp.sub_tasks[: FACTOR_FROM_REPORT_PROP_SETTING.max_factors_per_exp] + exp.base_features = self.plan["features"] + if exp.based_experiments: + exp.based_experiments[-1].base_features = self.plan["features"] logger.log_object(exp.hypothesis, tag="hypothesis generation") logger.log_object(exp.sub_tasks, tag="experiment generation") return exp diff --git a/rdagent/app/qlib_rd_loop/model.py b/rdagent/app/qlib_rd_loop/model.py index c2e4ed39..d821f249 100644 --- a/rdagent/app/qlib_rd_loop/model.py +++ b/rdagent/app/qlib_rd_loop/model.py @@ -21,6 +21,8 @@ def main( loop_n: int | None = None, all_duration: str | None = None, checkout: bool = True, + base_features_path: str | None = None, + **kwargs, ): """ Auto R&D Evolving loop for fintech models @@ -36,6 +38,10 @@ def main( model_loop = ModelRDLoop(MODEL_PROP_SETTING) else: model_loop = ModelRDLoop.load(path, checkout=checkout) + model_loop._init_base_features(base_features_path) + if "user_interaction_queues" in kwargs and kwargs["user_interaction_queues"] is not None: + model_loop._set_interactor(*kwargs["user_interaction_queues"]) + model_loop._interact_init_params() asyncio.run(model_loop.run(step_n=step_n, loop_n=loop_n, all_duration=all_duration)) diff --git a/rdagent/app/qlib_rd_loop/quant.py b/rdagent/app/qlib_rd_loop/quant.py index 43772c53..e26972ef 100644 --- a/rdagent/app/qlib_rd_loop/quant.py +++ b/rdagent/app/qlib_rd_loop/quant.py @@ -15,6 +15,7 @@ from rdagent.core.developer import Developer from rdagent.core.exception import FactorEmptyError, ModelEmptyError from rdagent.core.proposal import ( Experiment2Feedback, + ExperimentPlan, Hypothesis2Experiment, HypothesisFeedback, HypothesisGen, @@ -23,6 +24,7 @@ from rdagent.core.scenario import Scenario from rdagent.core.utils import import_class from rdagent.log import rdagent_logger as logger from rdagent.scenarios.qlib.proposal.quant_proposal import QuantTrace +from rdagent.utils.qlib import ALPHA20 class QuantRDLoop(RDLoop): @@ -62,6 +64,10 @@ class QuantRDLoop(RDLoop): self.model_summarizer: Experiment2Feedback = import_class(PROP_SETTING.model_summarizer)(scen) logger.log_object(self.model_summarizer, tag="model summarizer") + self.plan: ExperimentPlan = { + "features": ALPHA20, + "feature_codes": {}, + } # for user interaction self.trace = QuantTrace(scen=scen) super(RDLoop, self).__init__() @@ -75,6 +81,11 @@ class QuantRDLoop(RDLoop): else: exp = self.model_hypothesis2experiment.convert(hypo, self.trace) logger.log_object(exp.sub_tasks, tag="experiment generation") + exp.base_features = self.plan["features"] + exp.base_feature_codes = self.plan["feature_codes"] + if exp.based_experiments: + exp.based_experiments[-1].base_features = self.plan["features"] + exp.based_experiments[-1].base_feature_codes = self.plan["feature_codes"] return {"propose": hypo, "exp_gen": exp} await asyncio.sleep(1) @@ -112,6 +123,7 @@ class QuantRDLoop(RDLoop): feedback = self.factor_summarizer.generate_feedback(prev_out["running"], self.trace) elif prev_out["direct_exp_gen"]["propose"].action == "model": feedback = self.model_summarizer.generate_feedback(prev_out["running"], self.trace) + feedback = self._interact_feedback(feedback) logger.log_object(feedback, tag="feedback") return feedback @@ -122,6 +134,8 @@ def main( loop_n: int | None = None, all_duration: str | None = None, checkout: bool = True, + base_features_path: str | None = None, + **kwargs, ): """ Auto R&D Evolving loop for fintech factors. @@ -133,6 +147,10 @@ def main( quant_loop = QuantRDLoop(QUANT_PROP_SETTING) else: quant_loop = QuantRDLoop.load(path, checkout=checkout) + quant_loop._init_base_features(base_features_path) + if "user_interaction_queues" in kwargs and kwargs["user_interaction_queues"] is not None: + quant_loop._set_interactor(*kwargs["user_interaction_queues"]) + quant_loop._interact_init_params() asyncio.run(quant_loop.run(step_n=step_n, loop_n=loop_n, all_duration=all_duration)) diff --git a/rdagent/app/utils/health_check.py b/rdagent/app/utils/health_check.py index d0b2892b..95597b25 100644 --- a/rdagent/app/utils/health_check.py +++ b/rdagent/app/utils/health_check.py @@ -4,10 +4,8 @@ import socket import docker import fire import litellm -import typer from litellm import completion, embedding from litellm.utils import ModelResponse -from typing_extensions import Annotated from rdagent.log import rdagent_logger as logger from rdagent.utils.env import cleanup_container @@ -44,10 +42,10 @@ def check_and_list_free_ports(start_port=19899, max_ports=10) -> None: if not is_port_in_use(port): free_ports.append(port) logger.warning( - f"Port 19899 is occupied, please replace it with an available port when running the `rdagent ui` command. Available ports: {free_ports}" + f"Port 19899 is occupied, please replace it with an available port when running the `rdagent ui/server_ui` command. Available ports: {free_ports}" ) else: - logger.info(f"Port 19899 is not occupied, you can run the `rdagent ui` command") + logger.info(f"Port 19899 is not occupied, you can run the `rdagent ui/server_ui` command") def test_chat(chat_model, chat_api_key, chat_api_base): @@ -135,9 +133,9 @@ def env_check(): def health_check( - check_env: Annotated[bool, typer.Option("--check-env/--no-check-env", "-e/-E")] = True, - check_docker: Annotated[bool, typer.Option("--check-docker/--no-check-docker", "-d/-D")] = True, - check_ports: Annotated[bool, typer.Option("--check-ports/--no-check-ports", "-p/-P")] = True, + check_env: bool = True, + check_docker: bool = True, + check_ports: bool = True, ): """ Run the RD-Agent health check: @@ -167,4 +165,4 @@ def health_check( if __name__ == "__main__": - typer.run(health_check) + fire.Fire(health_check) diff --git a/rdagent/components/proposal/__init__.py b/rdagent/components/proposal/__init__.py index 6eda9fb8..dec620d5 100644 --- a/rdagent/components/proposal/__init__.py +++ b/rdagent/components/proposal/__init__.py @@ -42,6 +42,7 @@ class LLMHypothesisGen(HypothesisGen): ), hypothesis_output_format=context_dict["hypothesis_output_format"], hypothesis_specification=context_dict["hypothesis_specification"], + user_instruction=plan.get("user_instruction", None) if plan is not None else None, ) user_prompt = T(".prompts:hypothesis_gen.user_prompt").r( targets=self.targets, diff --git a/rdagent/components/proposal/prompts.yaml b/rdagent/components/proposal/prompts.yaml index bb9ca77d..9d2ced50 100644 --- a/rdagent/components/proposal/prompts.yaml +++ b/rdagent/components/proposal/prompts.yaml @@ -3,10 +3,17 @@ hypothesis_gen: The user is working on generating new hypotheses for the {{ targets }} in a data-driven research and development process. The {{ targets }} are used in the following scenario: {{ scenario }} + + {% if user_instruction %} + **User's overall instruction:** + {{ user_instruction }} + {% endif %} + The user has already proposed several hypotheses and conducted evaluations on them. This information will be provided to you. Your task is to analyze previous experiments, reflect on the decision made in each experiment, and consider why experiments with a decision of true were successful while those with a decision of false failed. Then, think about how to improve further — either by refining the existing approach or by exploring an entirely new direction. If one exists and you agree with it, feel free to use it. If you disagree, please generate an improved version. {% if hypothesis_specification %} - To assist you in formulating new hypotheses, the user has provided some additional information: {{ hypothesis_specification }}. + To assist you in formulating new hypotheses, the user has provided some additional information: + {{ hypothesis_specification }} **Important:** If the hypothesis_specification outlines the next steps you need to follow, ensure you adhere to those instructions. {% endif %} Please generate the output using the following format and specifications: diff --git a/rdagent/components/workflow/rd_loop.py b/rdagent/components/workflow/rd_loop.py index 4d2768da..f93c51f9 100644 --- a/rdagent/components/workflow/rd_loop.py +++ b/rdagent/components/workflow/rd_loop.py @@ -4,6 +4,9 @@ It is from `rdagent/app/qlib_rd_loop/model.py` and try to replace `rdagent/app/q """ import asyncio +import json +from multiprocessing import Queue +from pathlib import Path from typing import Any from rdagent.components.workflow.conf import BasePropSetting @@ -11,6 +14,7 @@ from rdagent.core.conf import RD_AGENT_SETTINGS from rdagent.core.developer import Developer from rdagent.core.proposal import ( Experiment2Feedback, + ExperimentPlan, Hypothesis, Hypothesis2Experiment, HypothesisFeedback, @@ -20,6 +24,7 @@ from rdagent.core.proposal import ( from rdagent.core.scenario import Scenario from rdagent.core.utils import import_class from rdagent.log import rdagent_logger as logger +from rdagent.utils.qlib import ALPHA20, validate_qlib_features from rdagent.utils.workflow import LoopBase, LoopMeta @@ -36,6 +41,11 @@ class RDLoop(LoopBase, metaclass=LoopMeta): else None ) + self.plan: ExperimentPlan = { + "features": ALPHA20, + "feature_codes": {}, + } # for user interaction + self.hypothesis2experiment: Hypothesis2Experiment = ( import_class(PROP_SETTING.hypothesis2experiment)() if hasattr(PROP_SETTING, "hypothesis2experiment") and PROP_SETTING.hypothesis2experiment @@ -58,8 +68,125 @@ class RDLoop(LoopBase, metaclass=LoopMeta): super().__init__() # excluded steps + def _set_interactor(self, user_request_q: Queue, user_response_q: Queue): + self.user_request_q = user_request_q + self.user_response_q = user_response_q + + def _init_base_features(self, base_features_path: str | None): + if base_features_path is not None: + try: + base_dir = Path(base_features_path) + base_factors_file = base_dir / "base_factors.json" + + feature_codes: dict[str, str] = {} + for py_file in sorted(base_dir.glob("*.py")): + feature_codes[py_file.name] = py_file.read_text() + self.plan["feature_codes"] = feature_codes + + if not base_factors_file.exists(): + logger.info(f"No base_factors.json found under {base_dir}. Keeping default base features.") + logger.info(f"{len(feature_codes)} feature code files loaded from {base_dir}.") + else: + with base_factors_file.open("r") as f: + features = json.load(f) + + if not isinstance(features, dict): + raise ValueError( + "`base_factors.json` must contain a JSON object of feature_name -> expression." + ) + + if validate_qlib_features(list(features.values())): + self.plan["features"] = features + logger.info( + f"Loaded base features from {base_factors_file}. {len(features)} features loaded and {len(feature_codes)} feature code files loaded." + ) + else: + logger.warning( + f"Base feature validation failed for features loaded from {base_factors_file}. Using default features." + ) + except Exception as e: + logger.warning(f"Failed to load base features from {base_features_path}: {e}. Using default features.") + else: + logger.info("No base features path provided. Using default features.") + + def _interact_init_params(self) -> None: + if not (hasattr(self, "user_request_q") and hasattr(self, "user_response_q")): + return + + logger.info("Waiting for user interaction on initial parameters...") + try: + self.user_request_q.put( + { + "user_instruction": None, + } + ) + res_dict = self.user_response_q.get() + logger.info("Received user instruction response.") + self.plan.update(res_dict) + + if "feature_codes" not in self.plan: + self.plan[ + "user_instruction" + ] += f"\n\n{str(list(self.plan['feature_codes'].keys()))} has been configured as the base factor; do not generate duplicate factors." + fea_valid_msg = "" + while True: + logger.info("Requesting base feature configuration from user.") + self.user_request_q.put( + { + "features": self.plan["features"], + "feature_validation_msg": fea_valid_msg, + } + ) + self.plan["features"] = self.user_response_q.get() + logger.info("Received base feature configuration response.") + if validate_qlib_features(list(self.plan["features"].values())): + logger.info(f"Base feature validation passed. {len(self.plan['features'])} features selected.") + break + else: + logger.info("Base feature validation failed. Asking user to revise.") + fea_valid_msg = "Some features are invalid, please revise." + + except (EOFError, OSError): + logger.info("User interaction failed, using default initial parameters.") + return + logger.info("Received user interaction on initial parameters.") + + def _interact_hypo(self, hypo: Hypothesis) -> Hypothesis: + if not (hasattr(self, "user_request_q") and hasattr(self, "user_response_q")): + return hypo + + logger.info("Waiting for user interaction on hypothesis...") + try: + self.user_request_q.put(hypo.__dict__) + res_dict = self.user_response_q.get() + modified_hypo = type(hypo)(**res_dict) + except (EOFError, OSError, TypeError): + logger.info("User interaction failed, using original hypothesis.") + return hypo + logger.info("Received user interaction on hypothesis.") + return modified_hypo + + def _interact_feedback(self, feedback: HypothesisFeedback) -> HypothesisFeedback: + if not (hasattr(self, "user_request_q") and hasattr(self, "user_response_q")): + return feedback + + logger.info("Waiting for user interaction on feedback...") + try: + self.user_request_q.put(feedback.__dict__) + res_dict = self.user_response_q.get() + modified_feedback = HypothesisFeedback(**res_dict) + except (EOFError, OSError, TypeError): + logger.info("User interaction failed, using original feedback.") + return feedback + logger.info("Received user interaction on feedback.") + return modified_feedback + def _propose(self): - hypothesis = self.hypothesis_gen.gen(self.trace) + hypothesis = self.hypothesis_gen.gen(self.trace, self.plan) + + # user can change the hypothesis here + hypothesis = self._interact_hypo(hypothesis) + logger.log_object(hypothesis, tag="hypothesis generation") return hypothesis @@ -74,6 +201,11 @@ class RDLoop(LoopBase, metaclass=LoopMeta): if self.get_unfinished_loop_cnt(self.loop_idx) < RD_AGENT_SETTINGS.get_max_parallel(): hypo = self._propose() exp = self._exp_gen(hypo) + exp.base_features = self.plan["features"] + exp.base_feature_codes = self.plan["feature_codes"] + if exp.based_experiments: + exp.based_experiments[-1].base_features = self.plan["features"] + exp.based_experiments[-1].base_feature_codes = self.plan["feature_codes"] return {"propose": hypo, "exp_gen": exp} await asyncio.sleep(1) @@ -99,6 +231,7 @@ class RDLoop(LoopBase, metaclass=LoopMeta): ) else: feedback = self.summarizer.generate_feedback(prev_out["running"], self.trace) + feedback = self._interact_feedback(feedback) logger.log_object(feedback, tag="feedback") return feedback diff --git a/rdagent/core/proposal.py b/rdagent/core/proposal.py index 9625cdae..39cd0523 100644 --- a/rdagent/core/proposal.py +++ b/rdagent/core/proposal.py @@ -105,12 +105,14 @@ class HypothesisFeedback(ExperimentFeedback): new_hypothesis: str | None = None, eda_improvement: str | None = None, acceptable: bool | None = None, + exception: Exception | None = None, ) -> None: super().__init__( reason, decision=decision, code_change_summary=code_change_summary, eda_improvement=eda_improvement, + exception=exception, ) self.observations = observations self.hypothesis_evaluation = hypothesis_evaluation diff --git a/rdagent/log/conf.py b/rdagent/log/conf.py index f8f81686..a6fdfc3d 100644 --- a/rdagent/log/conf.py +++ b/rdagent/log/conf.py @@ -19,9 +19,16 @@ class LogSettings(ExtendedBaseSettings): storages: dict[str, list[int | str]] = {} + def set_ui_server_port(self, port: int | None) -> None: + self.ui_server_port = port + if port is None: + self.storages.pop("rdagent.log.ui.storage.WebStorage", None) + return + + self.storages["rdagent.log.ui.storage.WebStorage"] = [port, self.trace_path] + def model_post_init(self, _context: Any, /) -> None: - if self.ui_server_port is not None: - self.storages["rdagent.log.ui.storage.WebStorage"] = [self.ui_server_port, self.trace_path] + self.set_ui_server_port(self.ui_server_port) LOG_SETTINGS = LogSettings() diff --git a/rdagent/log/logger.py b/rdagent/log/logger.py index a3d7df5c..faade38b 100644 --- a/rdagent/log/logger.py +++ b/rdagent/log/logger.py @@ -7,18 +7,12 @@ from pathlib import Path from typing import Generator from loguru import logger - -from .conf import LOG_SETTINGS - -if LOG_SETTINGS.format_console is not None: - logger.remove() - logger.add(sys.stdout, format=LOG_SETTINGS.format_console) - from psutil import Process from rdagent.core.utils import SingletonBaseClass, import_class from .base import Storage +from .conf import LOG_SETTINGS from .storage import FileStorage from .utils import get_caller_info @@ -48,6 +42,18 @@ class RDAgentLog(SingletonBaseClass): # Thread-/coroutine-local tag; In Linux forked subprocess, it will be copied to the subprocess. _tag_ctx: ContextVar[str] = ContextVar("_tag_ctx", default="") + _raw_log_key = "_rdagent_raw" + + @classmethod + def _configure_console_sinks(cls) -> None: + raw_filter = lambda record: bool(record["extra"].get(cls._raw_log_key, False)) + normal_filter = lambda record: not raw_filter(record) + + if LOG_SETTINGS.format_console is not None: + logger.add(sys.stdout, format=LOG_SETTINGS.format_console, filter=normal_filter) + else: + logger.add(sys.stdout, filter=normal_filter) + logger.add(sys.stdout, format="{message}", filter=raw_filter) @property def _tag(self) -> str: # Get current tag @@ -58,13 +64,29 @@ class RDAgentLog(SingletonBaseClass): self._tag_ctx.set(value) def __init__(self) -> None: + logger.remove() + self._configure_console_sinks() + self.storage = FileStorage(LOG_SETTINGS.trace_path) self.other_storages: list[Storage] = [] + self.refresh_storages_from_settings() + + self.main_pid = os.getpid() + + def refresh_storages_from_settings(self) -> None: + self.other_storages = [] for storage, args in LOG_SETTINGS.storages.items(): storage_cls = import_class(storage) self.other_storages.append(storage_cls(*args)) - self.main_pid = os.getpid() + def rebind_console_to_current_streams(self) -> None: + """Rebind loguru sinks to the current stdio objects. + + This is needed in forked/spawned subprocesses after stdout/stderr have been + redirected, because loguru keeps references to the original stream objects. + """ + logger.remove() + self._configure_console_sinks() @contextmanager def tag(self, tag: str) -> Generator[None, None, None]: @@ -82,6 +104,8 @@ class RDAgentLog(SingletonBaseClass): self._tag_ctx.reset(token) def set_storages_path(self, path: str | Path) -> None: + if isinstance(path, str): + path = Path(path) for storage in [self.storage] + self.other_storages: if hasattr(storage, "path"): storage.path = path @@ -115,17 +139,10 @@ class RDAgentLog(SingletonBaseClass): caller_info = get_caller_info(level=3) tag = f"{self._tag}.{tag}.{self.get_pids()}".strip(".") - if raw: - logger.remove() - logger.add(sys.stderr, format=lambda r: "{message}") - - log_func = getattr(logger.patch(lambda r: r.update(caller_info)), level) + patched_logger = logger.patch(lambda r: r.update(caller_info)).bind(**{self._raw_log_key: raw}).opt(raw=raw) + log_func = getattr(patched_logger, level) log_func(msg) - if raw: - logger.remove() - logger.add(sys.stderr) - def info(self, msg: str, *, tag: str = "", raw: bool = False) -> None: self._log("info", msg, tag=tag, raw=raw) diff --git a/rdagent/log/server/app.py b/rdagent/log/server/app.py index 308175ce..575f14bc 100644 --- a/rdagent/log/server/app.py +++ b/rdagent/log/server/app.py @@ -1,69 +1,287 @@ +import logging import os import random -import signal -import subprocess +import traceback from collections import defaultdict +from contextlib import redirect_stderr, redirect_stdout from datetime import datetime, timezone +from multiprocessing import Process, Queue from pathlib import Path +from queue import Empty import randomname import typer -from flask import Flask, jsonify, request, send_from_directory +from flask import Flask, jsonify, request, send_file, send_from_directory from flask_cors import CORS from werkzeug.utils import secure_filename from rdagent.log.storage import FileStorage from rdagent.log.ui.conf import UI_SETTING from rdagent.log.ui.storage import WebStorage -from rdagent.log.utils import is_valid_session -app = Flask(__name__, static_folder=UI_SETTING.static_path) +app = Flask(__name__, static_folder=str(Path(UI_SETTING.static_path).resolve())) CORS(app) +app.config["UI_SERVER_PORT"] = 19899 -rdagent_processes = defaultdict() -server_port = 19899 +_YELLOW = "\033[33m" +_RESET = "\033[0m" + + +class _YellowWarningFormatter(logging.Formatter): + def format(self, record: logging.LogRecord) -> str: + if record.levelno == logging.WARNING: + record.levelname = f"{_YELLOW}{record.levelname}{_RESET}" + return super().format(record) + + +def _configure_app_logger() -> None: + formatter = _YellowWarningFormatter( + fmt="[%(asctime)s] %(levelname)s in %(module)s: %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + for handler in app.logger.handlers: + handler.setFormatter(formatter) + + +_configure_app_logger() + + +_TARGETS_WITHOUT_USER_INTERACTION = {"general_model", "fin_factor_report"} + + +class RDAgentTask: + def __init__( + self, + target_name: str, + kwargs: dict, + stdout_path: str, + log_trace_path: str, + scenario: str, + trace_name: str, + ui_server_port: int | None = None, + create_process: bool = True, + ) -> None: + self.target_name = target_name + self.kwargs = kwargs + self.stdout_path = stdout_path + self.log_trace_path = log_trace_path + self.scenario = scenario + self.trace_name = trace_name + self.ui_server_port = ui_server_port + self.process: Process | None = None + + # Two IPC queues for user interaction. + # - `user_request_q`: rdagent subprocess -> server (dicts to render on frontend) + # - `user_response_q`: server -> rdagent subprocess (user input dicts) + # NOTE: Use multiprocessing.Queue because rdagent is started as a separate process. + self.user_request_q: Queue = Queue(maxsize=1024) + self.user_response_q: Queue = Queue(maxsize=1024) + + if create_process: + self.process = Process( + target=self._run, + name=f"rdagent:{self.scenario}:{self.trace_name}", + ) + self.messages: list[dict] = [] + self.pointers: defaultdict[str, int] = defaultdict(int) + + def start(self) -> None: + if self.process is not None: + self.process.start() + + def is_alive(self) -> bool: + return self.process is not None and self.process.is_alive() + + def get_end_code(self) -> int: + if self.process is None or self.process.exitcode is None: + return 0 + return self.process.exitcode + + def stop(self) -> None: + if self.process is not None and self.process.is_alive(): + self.process.terminate() + self.process.join() + + # Best-effort cleanup for IPC queues. + for q in (self.user_request_q, self.user_response_q): + try: + q.cancel_join_thread() + except Exception: + pass + try: + q.close() + except Exception: + pass + + def _run(self) -> None: + from rdagent.log.conf import LOG_SETTINGS + + LOG_SETTINGS.set_ui_server_port(self.ui_server_port) + + from rdagent.log import rdagent_logger + + rdagent_logger.refresh_storages_from_settings() + rdagent_logger.set_storages_path(self.log_trace_path) + Path(self.stdout_path).parent.mkdir(parents=True, exist_ok=True) + with open(self.stdout_path, "w") as log_file: + with redirect_stdout(log_file), redirect_stderr(log_file): + rdagent_logger.rebind_console_to_current_streams() + try: + # Only interactive targets should receive IPC queues. + if self.target_name not in _TARGETS_WITHOUT_USER_INTERACTION: + self.kwargs.setdefault( + "user_interaction_queues", + (self.user_request_q, self.user_response_q), + ) + + if self.target_name == "data_science": + from rdagent.app.data_science.loop import main as data_science + + data_science(**self.kwargs) + elif self.target_name == "general_model": + from rdagent.app.general_model.general_model import ( + extract_models_and_implement as general_model, + ) + + general_model(**self.kwargs) + elif self.target_name == "fin_factor": + from rdagent.app.qlib_rd_loop.factor import main as fin_factor + + fin_factor(**self.kwargs) + elif self.target_name == "fin_factor_report": + from rdagent.app.qlib_rd_loop.factor_from_report import ( + main as fin_factor_report, + ) + + fin_factor_report(**self.kwargs) + elif self.target_name == "fin_model": + from rdagent.app.qlib_rd_loop.model import main as fin_model + + fin_model(**self.kwargs) + elif self.target_name == "fin_quant": + from rdagent.app.qlib_rd_loop.quant import main as fin_quant + + fin_quant(**self.kwargs) + else: + raise ValueError(f"Unknown target: {self.target_name}") + except Exception: + traceback.print_exc() + + +rdagent_processes: dict[str, RDAgentTask] = {} log_folder_path = Path(UI_SETTING.trace_folder).absolute() +def _drain_user_requests_into_messages(task: RDAgentTask) -> None: + """Move a single pending user-interaction request into `task.messages`. + + Assumption: each rdagent process only has one active request at a time. + """ + + try: + req = task.user_request_q.get_nowait() + except Empty: + return + except Exception: + return + + # Standardize the message shape for the frontend. + # The agent can send either a full message dict, or a raw content dict. + if isinstance(req, dict) and {"tag", "timestamp", "content"}.issubset(req.keys()): + msg = req + else: + msg = { + "tag": "user_interaction.request", + "timestamp": datetime.now(timezone.utc).isoformat(), + "content": req, + } + task.messages.append(msg) + + @app.route("/favicon.ico") def favicon(): return send_from_directory(app.static_folder, "favicon.ico", mimetype="image/vnd.microsoft.icon") -msgs_for_frontend = defaultdict(list) -pointers = defaultdict(lambda: defaultdict(int)) # pointers[trace_id][user_ip] +def _normalize_static_request_path(fn: str) -> str: + static_prefix = UI_SETTING.static_path.strip("./") + if static_prefix and fn.startswith(f"{static_prefix}/"): + return fn[len(static_prefix) + 1 :] + return fn + + +def _get_or_create_task(trace_id: str) -> RDAgentTask: + task = rdagent_processes.get(trace_id) + if task is None: + task = RDAgentTask( + target_name="", + kwargs={}, + stdout_path="", + log_trace_path=trace_id, + scenario="", + trace_name="", + ui_server_port=None, + create_process=False, + ) + rdagent_processes[trace_id] = task + return task + + +def _resolve_stdout_path(trace_id: str) -> Path | None: + normalized_trace_id = str(trace_id or "").strip() + if not normalized_trace_id: + return None + + task = rdagent_processes.get(str(log_folder_path / normalized_trace_id)) + if task is None or not task.stdout_path: + return None + + stdout_path = Path(task.stdout_path).resolve() + + try: + if os.path.commonpath([str(stdout_path), str(log_folder_path)]) != str(log_folder_path): + return None + except ValueError: + return None + + return stdout_path def read_trace(log_path: Path, id: str = "") -> None: fs = FileStorage(log_path) ws = WebStorage(port=1, path=log_path) - msgs_for_frontend[id] = [] + task = _get_or_create_task(id) + task.messages = [] last_timestamp = None for msg in fs.iter_msg(): data = ws._obj_to_json(obj=msg.content, tag=msg.tag, id=id, timestamp=msg.timestamp.isoformat()) if data: if isinstance(data, list): for d in data: - msgs_for_frontend[id].append(d["msg"]) + task.messages.append(d["msg"]) last_timestamp = msg.timestamp else: - msgs_for_frontend[id].append(data["msg"]) + task.messages.append(data["msg"]) last_timestamp = msg.timestamp now = datetime.now(timezone.utc) if last_timestamp and (now - last_timestamp).total_seconds() > 1800: - msgs_for_frontend[id].append({"tag": "END", "timestamp": now.isoformat(), "content": {}}) + task.messages.append( + { + "tag": "END", + "timestamp": now.isoformat(), + "content": {"error_msg": "Trace session has ended.", "end_code": 0}, + } + ) # load all traces from the log folder -for p in log_folder_path.glob("*/*/"): - if is_valid_session(p): - read_trace(p, id=str(p)) +# for p in log_folder_path.glob("*/*/"): +# read_trace(p, id=str(p)) @app.route("/trace", methods=["POST"]) def update_trace(): - global pointers, msgs_for_frontend data = request.get_json() trace_id = data.get("id") return_all = data.get("all") @@ -75,28 +293,64 @@ def update_trace(): return jsonify({"error": "Trace ID is required"}), 400 trace_id = str(log_folder_path / trace_id) + task = _get_or_create_task(trace_id) + + # Make sure any pending user-interaction requests are visible to the frontend. + _drain_user_requests_into_messages(task) + + if task.process is not None and not task.is_alive(): + if not task.messages or task.messages[-1].get("tag") != "END": + task.messages.append( + { + "tag": "END", + "timestamp": datetime.now(timezone.utc).isoformat(), + "content": { + "error_msg": "RD-Agent process has completed.", + "end_code": task.get_end_code(), + }, + } + ) + app.logger.warning(f"Process for {trace_id} has ended.") + user_ip = request.remote_addr if reset: - pointers[trace_id][user_ip] = 0 + task.pointers[user_ip] = 0 - start_pointer = pointers[trace_id][user_ip] + start_pointer = task.pointers[user_ip] end_pointer = start_pointer + msg_num - if end_pointer > len(msgs_for_frontend[trace_id]) or return_all: - end_pointer = len(msgs_for_frontend[trace_id]) + if end_pointer > len(task.messages) or return_all: + end_pointer = len(task.messages) - returned_msgs = msgs_for_frontend[trace_id][start_pointer:end_pointer] - - pointers[trace_id][user_ip] = end_pointer + returned_msgs = task.messages[start_pointer:end_pointer] + task.pointers[user_ip] = end_pointer if returned_msgs: app.logger.info([msg["tag"] for msg in returned_msgs]) return jsonify(returned_msgs), 200 +@app.route("/stdout", methods=["GET"]) +def download_stdout_file(): + trace_id = request.args.get("id", "") + stdout_path = _resolve_stdout_path(trace_id) + + if stdout_path is None: + return jsonify({"error": "Trace ID is required or invalid"}), 400 + if not stdout_path.exists() or not stdout_path.is_file(): + return jsonify({"error": "Stdout file not found"}), 404 + + return send_file( + stdout_path, + as_attachment=True, + download_name=stdout_path.name, + mimetype="text/plain", + ) + + @app.route("/upload", methods=["POST"]) def upload_file(): # 获取请求体中的字段 - global rdagent_processes, server_port + global rdagent_processes scenario = request.form.get("scenario") files = request.files.getlist("files") competition = request.form.get("competition") @@ -109,21 +363,19 @@ def upload_file(): trace_name = f"{competition}-{randomname.get_name()}" else: trace_name = randomname.get_name() - trace_files_path = log_folder_path / scenario / "uploads" / trace_name + trace_files_path = log_folder_path / "uploads" / scenario / trace_name log_trace_path = (log_folder_path / scenario / trace_name).absolute() - stdout_path = log_folder_path / scenario / f"{trace_name}.stdout" + stdout_path = log_folder_path / scenario / f"{trace_name}.log" if not stdout_path.exists(): stdout_path.parent.mkdir(parents=True, exist_ok=True) # save files for file in files: if file: - p = (log_folder_path / scenario / "uploads" / trace_name).resolve() + p = (log_folder_path / "uploads" / scenario / trace_name).resolve() sanitized_filename = secure_filename(file.filename) # Sanitize filename target_path = (p / sanitized_filename).resolve() # Normalize target path - if not sanitized_filename.lower().endswith(".pdf"): - return jsonify({"error": "Invalid file type"}), 400 # Ensure target_path is within the allowed base directory if os.path.commonpath([str(target_path), str(p)]) == str(p) and target_path.is_file() == False: if not p.exists(): @@ -132,42 +384,62 @@ def upload_file(): else: return jsonify({"error": "Invalid file path"}), 400 + target_name = None + kwargs = {} + loop_n_val = int(loop_n) if loop_n else None + all_duration_val = f"{all_duration}h" if all_duration else None + if scenario == "Finance Data Building": - cmds = ["rdagent", "fin_factor"] - if scenario == "Finance Data Building (Reports)": - cmds = ["rdagent", "fin_factor_report", "--report_folder", str(trace_files_path)] + target_name = "fin_factor" + kwargs = { + "loop_n": loop_n_val, + "all_duration": all_duration_val, + "base_features_path": str(trace_files_path), + } if scenario == "Finance Model Implementation": - cmds = ["rdagent", "fin_model"] + target_name = "fin_model" + kwargs = { + "loop_n": loop_n_val, + "all_duration": all_duration_val, + "base_features_path": str(trace_files_path), + } + if scenario == "Finance Whole Pipeline": + target_name = "fin_quant" + kwargs = { + "loop_n": loop_n_val, + "all_duration": all_duration_val, + "base_features_path": str(trace_files_path), + } + if scenario == "Finance Data Building (Reports)": + target_name = "fin_factor_report" + kwargs = {"report_folder": str(trace_files_path), "all_duration": all_duration_val} if scenario == "General Model Implementation": if len(files) == 0: # files is one link rfp = request.form.get("files")[0] else: # one file is uploaded rfp = str(trace_files_path / files[0].filename) - cmds = ["rdagent", "general_model", "--report_file_path", rfp] - if scenario == "Finance Whole Pipeline": - cmds = ["rdagent", "fin_quant"] + target_name = "general_model" + kwargs = {"report_file_path": rfp} if scenario == "Data Science": - cmds = ["rdagent", "data_science", "--competition", competition] + target_name = "data_science" + kwargs = {"competition": competition, "loop_n": loop_n_val, "timeout": all_duration_val} - # time control parameters - if scenario != "Finance Data Building (Reports)": - if loop_n: - cmds += ["--loop_n", loop_n] - if all_duration: - cmds += ["--timeout", f"{all_duration}h"] + if target_name is None: + return jsonify({"error": "Unknown scenario"}), 400 - app.logger.info(f"Started process for {log_trace_path} with parameters: {cmds}") - with stdout_path.open("w") as log_file: - rdagent_processes[str(log_trace_path)] = subprocess.Popen( - cmds, - stdout=log_file, - stderr=log_file, - env={ - **os.environ, - "LOG_TRACE_PATH": str(log_trace_path), - "LOG_UI_SERVER_PORT": str(server_port), - }, - ) + app.logger.info(f"Started process for {log_trace_path} with target: {target_name}, kwargs: {kwargs}") + task = RDAgentTask( + target_name=target_name, + kwargs=kwargs, + stdout_path=str(stdout_path), + log_trace_path=str(log_trace_path), + scenario=scenario, + trace_name=trace_name, + ui_server_port=app.config["UI_SERVER_PORT"], + ) + task.start() + app.logger.warning(f"Task {log_trace_path} started.") + rdagent_processes[str(log_trace_path)] = task return ( jsonify( { @@ -182,7 +454,6 @@ def upload_file(): def receive_msgs(): try: data = request.get_json() - # app.logger.info(data["msg"]["tag"]) if not data: return jsonify({"error": "No JSON data received"}), 400 except Exception as e: @@ -190,16 +461,41 @@ def receive_msgs(): if isinstance(data, list): for d in data: - msgs_for_frontend[d["id"]].append(d["msg"]) + task = _get_or_create_task(d["id"]) + task.messages.append(d["msg"]) else: - msgs_for_frontend[data["id"]].append(data["msg"]) + task = _get_or_create_task(data["id"]) + task.messages.append(data["msg"]) + + return jsonify({"status": "success"}), 200 + + +@app.route("/user_interaction/submit", methods=["POST"]) +def submit_user_interaction_response(): + """Frontend submits a user response; server forwards it to the rdagent subprocess via IPC queue.""" + data = request.get_json(silent=True) or {} + trace_id = data.get("id") + payload = data.get("payload") + + if not trace_id: + return jsonify({"error": "Trace ID is required"}), 400 + if payload is None: + return jsonify({"error": "Missing 'payload'"}), 400 + + trace_id = str(log_folder_path / trace_id) + task = _get_or_create_task(trace_id) + + try: + task.user_response_q.put(payload, block=False) + except Exception as e: + return jsonify({"error": f"Failed to enqueue user response: {e}"}), 500 return jsonify({"status": "success"}), 200 @app.route("/control", methods=["POST"]) def control_process(): - global rdagent_processes, msgs_for_frontend + global rdagent_processes data = request.get_json() app.logger.info(data) if not data or "id" not in data or "action" not in data: @@ -208,32 +504,31 @@ def control_process(): id = str(log_folder_path / data["id"]) action = data["action"] + if action != "stop": + return jsonify({"error": "Only 'stop' action is supported"}), 400 + if id not in rdagent_processes or rdagent_processes[id] is None: return jsonify({"error": "No running process for given id"}), 400 - process = rdagent_processes[id] + task = rdagent_processes[id] - if process.poll() is not None: - msgs_for_frontend[id].append({"tag": "END", "timestamp": datetime.now(timezone.utc).isoformat(), "content": {}}) - return jsonify({"error": "Process has already terminated"}), 400 + if task.process is None: + return jsonify({"error": "No running process for given id"}), 400 try: - if action == "pause": - os.kill(process.pid, signal.SIGSTOP) - return jsonify({"status": "paused"}), 200 - elif action == "resume": - os.kill(process.pid, signal.SIGCONT) - return jsonify({"status": "resumed"}), 200 - elif action == "stop": - process.terminate() - process.wait() - del rdagent_processes[id] - msgs_for_frontend[id].append( - {"tag": "END", "timestamp": datetime.now(timezone.utc).isoformat(), "content": {}} + if task.is_alive(): + task.stop() + + if not task.messages or task.messages[-1].get("tag") != "END": + task.messages.append( + { + "tag": "END", + "timestamp": datetime.now(timezone.utc).isoformat(), + "content": {"error_msg": "RD-Agent process was stopped by user.", "end_code": -1}, + } ) - return jsonify({"status": "stopped"}), 200 - else: - return jsonify({"error": "Unknown action"}), 400 + app.logger.warning(f"Process for {id} has been stopped.") + return jsonify({"status": "stopped"}), 200 except Exception as e: return jsonify({"error": f"Failed to {action} process, {e}"}), 500 @@ -241,9 +536,8 @@ def control_process(): @app.route("/test", methods=["GET"]) def test(): # return 'Hello, World!' - global msgs_for_frontend, pointers - msgs = {k: [i["tag"] for i in v] for k, v in msgs_for_frontend.items()} - pointers = pointers + msgs = {k: [i["tag"] for i in task.messages] for k, task in rdagent_processes.items()} + pointers = {k: dict(task.pointers) for k, task in rdagent_processes.items()} return jsonify({"msgs": msgs, "pointers": pointers}), 200 @@ -256,12 +550,11 @@ def index(): @app.route("/", methods=["GET"]) def server_static_files(fn): - return send_from_directory(app.static_folder, fn) + return send_from_directory(app.static_folder, _normalize_static_request_path(fn)) def main(port: int = 19899): - global server_port - server_port = port + app.config["UI_SERVER_PORT"] = port app.run(debug=False, host="0.0.0.0", port=port) diff --git a/rdagent/log/ui/conf.py b/rdagent/log/ui/conf.py index d689d856..9590b935 100644 --- a/rdagent/log/ui/conf.py +++ b/rdagent/log/ui/conf.py @@ -16,7 +16,7 @@ class UIBasePropSetting(ExtendedBaseSettings): static_path: str = "./git_ignore_folder/static" - trace_folder: str = "./traces" + trace_folder: str = "./git_ignore_folder/traces" enable_cache: bool = True diff --git a/rdagent/log/ui/storage.py b/rdagent/log/ui/storage.py index de652fd6..96366eae 100644 --- a/rdagent/log/ui/storage.py +++ b/rdagent/log/ui/storage.py @@ -33,10 +33,11 @@ class WebStorage(Storage): def log(self, obj: object, tag: str, timestamp: datetime | None = None, **kwargs: Any) -> str | Path: timestamp = gen_datetime(timestamp) if "pdf_image" in tag or "load_pdf_screenshot" in tag: - obj.save(f"{UI_SETTING.static_path}/{timestamp.isoformat()}.jpg") + Path(f"{UI_SETTING.static_path}/pdf_images").mkdir(parents=True, exist_ok=True) + obj.save(f"{UI_SETTING.static_path}/pdf_images/{timestamp.isoformat()}.jpg") try: - data = self._obj_to_json(obj=obj, tag=tag, id=self.path, timestamp=timestamp.isoformat()) + data = self._obj_to_json(obj=obj, tag=tag, id=str(self.path), timestamp=timestamp.isoformat()) if not data: return "Normal log, skipped" if isinstance(data, list): @@ -48,7 +49,7 @@ class WebStorage(Storage): resp = requests.post(f"{self.url}/receive", json=data, headers=headers, timeout=1) return f"{resp.status_code} {resp.text}" except (requests.ConnectionError, requests.Timeout) as e: - pass + print(f"Failed to connect to the web storage server at {self.url}: {e}") def truncate(self, time: datetime) -> None: self.msgs = [m for m in self.msgs if datetime.fromisoformat(m["msg"]["timestamp"]) <= time] @@ -100,7 +101,7 @@ class WebStorage(Storage): "tag": "research.pdf_image", "timestamp": timestamp, "loop_id": li, - "content": {"image": f"{timestamp}.jpg"}, + "content": {"image": f"pdf_images/{timestamp}.jpg"}, }, } elif "experiment generation" in tag or "load_experiment" in tag: diff --git a/rdagent/log/utils/__init__.py b/rdagent/log/utils/__init__.py index 51bbe610..44edb67e 100644 --- a/rdagent/log/utils/__init__.py +++ b/rdagent/log/utils/__init__.py @@ -92,7 +92,7 @@ def extract_loopid_func_name(tag: str) -> tuple[str, str] | tuple[None, None]: def extract_evoid(tag: str) -> str | None: """extract evo id from the tag in Message""" - match = re.search(r"\.evo_loop_(\d+)\.", tag) + match = re.search(r"evo_loop_(\d+)\.", tag) return cast(str, match.group(1)) if match else None diff --git a/rdagent/scenarios/qlib/developer/factor_runner.py b/rdagent/scenarios/qlib/developer/factor_runner.py index 00304da2..c6b58d35 100644 --- a/rdagent/scenarios/qlib/developer/factor_runner.py +++ b/rdagent/scenarios/qlib/developer/factor_runner.py @@ -19,17 +19,6 @@ from rdagent.scenarios.qlib.experiment.model_experiment import QlibModelExperime DIRNAME = Path(__file__).absolute().resolve().parent DIRNAME_local = Path.cwd() -# class QlibFactorExpWorkspace: - -# def prepare(): -# # create a folder; -# # copy template -# # place data inside the folder `combined_factors` -# # -# def execute(): -# de = DockerEnv() -# de.run(local_path=self.ws_path, entry="qrun conf_baseline.yaml") - # TODO: supporting multiprocessing and keep previous results @@ -89,6 +78,8 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): "valid_start": fbps.valid_start, "valid_end": fbps.valid_end, "test_start": fbps.test_start, + "feature_names": str(list(exp.base_features.keys())), + "feature_expressions": str(list(exp.base_features.values())), } if fbps.test_end is not None: env_to_use.update({"test_end": fbps.test_end}) @@ -103,8 +94,8 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): logger.info(f"SOTA factor processing ...") SOTA_factor = process_factor_data(sota_factor_experiments_list) - logger.info(f"New factor processing ...") # Process the new factors data + logger.info(f"New factor processing ...") new_factors = process_factor_data(exp) if new_factors.empty: @@ -126,9 +117,10 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): combined_factors = combined_factors.loc[:, ~combined_factors.columns.duplicated(keep="last")] new_columns = pd.MultiIndex.from_product([["feature"], combined_factors.columns]) combined_factors.columns = new_columns - num_features = RD_AGENT_SETTINGS.initial_fator_library_size + len(combined_factors.columns) logger.info(f"Factor data processing completed.") + num_features = len(exp.base_features) + len(combined_factors.columns) + # Due to the rdagent and qlib docker image in the numpy version of the difference, # the `combined_factors_df.pkl` file could not be loaded correctly in qlib dokcer, # so we changed the file type of `combined_factors_df` from pkl to parquet. @@ -175,19 +167,30 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]): else: # LGBM + combined factors result, stdout = exp.experiment_workspace.execute( - qlib_config_name=( - f"conf_baseline.yaml" if len(exp.based_experiments) == 0 else "conf_combined_factors.yaml" - ), + qlib_config_name="conf_combined_factors.yaml", run_env=env_to_use, ) else: logger.info(f"Experiment execution ...") - result, stdout = exp.experiment_workspace.execute( - qlib_config_name=( - f"conf_baseline.yaml" if len(exp.based_experiments) == 0 else "conf_combined_factors.yaml" - ), - run_env=env_to_use, - ) + if exp.base_feature_codes: + factors = process_factor_data(exp) + factors = factors.sort_index() + factors = factors.loc[:, ~factors.columns.duplicated(keep="last")] + new_columns = pd.MultiIndex.from_product([["feature"], factors.columns]) + factors.columns = new_columns + target_path = exp.experiment_workspace.workspace_path / "combined_factors_df.parquet" + # Save the combined factors to the workspace + factors.to_parquet(target_path, engine="pyarrow") + logger.info(f"Factor data processing completed.") + result, stdout = exp.experiment_workspace.execute( + qlib_config_name="conf_combined_factors.yaml", + run_env=env_to_use, + ) + else: + result, stdout = exp.experiment_workspace.execute( + qlib_config_name="conf_baseline.yaml", + run_env=env_to_use, + ) if result is None: logger.error(f"Failed to run this experiment, because {stdout}") diff --git a/rdagent/scenarios/qlib/developer/model_runner.py b/rdagent/scenarios/qlib/developer/model_runner.py index 9e8e4e49..7f909420 100644 --- a/rdagent/scenarios/qlib/developer/model_runner.py +++ b/rdagent/scenarios/qlib/developer/model_runner.py @@ -47,7 +47,7 @@ class QlibModelRunner(CachedRunner[QlibModelExperiment]): combined_factors = combined_factors.loc[:, ~combined_factors.columns.duplicated(keep="last")] new_columns = pd.MultiIndex.from_product([["feature"], combined_factors.columns]) combined_factors.columns = new_columns - num_features = str(RD_AGENT_SETTINGS.initial_fator_library_size + len(combined_factors.columns)) + num_features = str(len(exp.base_features) + len(combined_factors.columns)) target_path = exp.experiment_workspace.workspace_path / "combined_factors_df.parquet" @@ -67,6 +67,8 @@ class QlibModelRunner(CachedRunner[QlibModelExperiment]): "valid_start": mbps.valid_start, "valid_end": mbps.valid_end, "test_start": mbps.test_start, + "feature_names": str(list(exp.base_features.keys())), + "feature_expressions": str(list(exp.base_features.values())), } if mbps.test_end is not None: env_to_use.update({"test_end": mbps.test_end}) diff --git a/rdagent/scenarios/qlib/developer/utils.py b/rdagent/scenarios/qlib/developer/utils.py index ea541890..cd4abef3 100644 --- a/rdagent/scenarios/qlib/developer/utils.py +++ b/rdagent/scenarios/qlib/developer/utils.py @@ -3,6 +3,7 @@ from typing import List import pandas as pd from rdagent.components.coder.CoSTEER.evaluators import CoSTEERMultiFeedback +from rdagent.components.coder.factor_coder.factor import FactorFBWorkspace, FactorTask from rdagent.core.conf import RD_AGENT_SETTINGS from rdagent.core.exception import FactorEmptyError from rdagent.core.utils import multiprocessing_wrapper @@ -10,6 +11,123 @@ from rdagent.log import rdagent_logger as logger from rdagent.scenarios.qlib.experiment.factor_experiment import QlibFactorExperiment +def _build_base_feature_workspaces(exp: QlibFactorExperiment) -> list[FactorFBWorkspace]: + workspaces: list[FactorFBWorkspace] = [] + for file_name, code in exp.base_feature_codes.items(): + workspace = FactorFBWorkspace( + target_task=FactorTask( + factor_name=file_name, + factor_description=f"Base feature from {file_name}", + factor_formulation="", + ) + ) + workspace.inject_files(**{"factor.py": code}) + workspaces.append(workspace) + return workspaces + + +def _build_execute_calls(exp: QlibFactorExperiment, base_feature_workspaces: list[FactorFBWorkspace]) -> list[tuple]: + execute_calls = [] + + if exp.sub_tasks: + assert isinstance(exp.prop_dev_feedback, CoSTEERMultiFeedback) + execute_calls.extend( + (implementation.execute, ("All",)) + for implementation, feedback in zip(exp.sub_workspace_list, exp.prop_dev_feedback) + if implementation and feedback + ) + + execute_calls.extend((workspace.execute, ("All",)) for workspace in base_feature_workspaces) + return execute_calls + + +def _resolve_index_level_values(df: pd.DataFrame, level_name: str) -> pd.Index | None: + matching_levels = [idx for idx, name in enumerate(df.index.names) if name == level_name] + if not matching_levels: + return None + + if len(matching_levels) == 1: + return df.index.get_level_values(matching_levels[0]) + + candidate_values = [df.index.get_level_values(idx) for idx in matching_levels] + first_values = candidate_values[0] + if all(first_values.equals(values) for values in candidate_values[1:]): + logger.warning( + f"Factor dataframe has duplicated '{level_name}' index levels at positions {matching_levels}; " + "their values are identical, so the first one is used." + ) + return first_values + + logger.warning( + f"Skip factor dataframe because index has ambiguous duplicated '{level_name}' levels at positions " + f"{matching_levels}. index names={list(df.index.names)}" + ) + return None + + +def _normalize_factor_index(df: pd.DataFrame) -> pd.DataFrame | None: + """Normalize factor index to a 2-level MultiIndex: (datetime, instrument).""" + if df is None or df.empty: + return None + + index_names = list(df.index.names) + if "datetime" not in index_names: + return None + + if "instrument" not in index_names: + logger.warning(f"Skip factor dataframe because index misses 'instrument'. index names={index_names}") + return None + + datetime_values = _resolve_index_level_values(df, "datetime") + instrument_values = _resolve_index_level_values(df, "instrument") + if datetime_values is None or instrument_values is None: + return None + + normalized = df.copy() + normalized.index = pd.MultiIndex.from_arrays( + [datetime_values, instrument_values], + names=["datetime", "instrument"], + ) + return normalized + + +def _format_index_info(df: pd.DataFrame | None) -> str: + if df is None: + return "df is None" + return f"index_type={type(df.index).__name__}, nlevels={df.index.nlevels}, names={list(df.index.names)}" + + +def _process_message_and_df( + source_name: str, + message: str, + df: pd.DataFrame | None, + factor_dfs: list[pd.DataFrame], + error_message: str, +) -> str: + index_info = _format_index_info(df) + if df is None or "datetime" not in df.index.names: + logger.warning(f"Factor data from {source_name} has invalid execution output or index: {index_info}") + logger.warning(f"Factor data from {source_name} is not generated because of {message}") + return ( + f"{error_message}Factor data from {source_name} is not generated because of {message}. " + f"index_info={index_info}. " + ) + + normalized_df = _normalize_factor_index(df) + if normalized_df is None: + logger.warning(f"Factor data from {source_name} is skipped due to invalid index structure: {index_info}") + return f"{error_message}Factor data from {source_name} is skipped due to invalid index: {index_info}. " + + time_diff = df.index.get_level_values("datetime").to_series().diff().dropna().unique() + if pd.Timedelta(minutes=1) in time_diff: + logger.warning(f"Factor data from {source_name} is not generated.") + return error_message + + factor_dfs.append(normalized_df) + logger.info(f"Factor data from {source_name} is successfully generated.") + return error_message + + def process_factor_data(exp_or_list: List[QlibFactorExperiment] | QlibFactorExperiment) -> pd.DataFrame: """ Process and combine factor data from experiment implementations. @@ -23,44 +141,36 @@ def process_factor_data(exp_or_list: List[QlibFactorExperiment] | QlibFactorExpe if isinstance(exp_or_list, QlibFactorExperiment): exp_or_list = [exp_or_list] factor_dfs = [] + error_message = "" # Collect all exp's dataframes for exp in exp_or_list: - if isinstance(exp, QlibFactorExperiment): - if len(exp.sub_tasks) > 0: - # if it has no sub_tasks, the experiment is results from template project. - # otherwise, it is developed with designed task. So it should have feedback. - assert isinstance(exp.prop_dev_feedback, CoSTEERMultiFeedback) - # Iterate over sub-implementations and execute them to get each factor data - message_and_df_list = multiprocessing_wrapper( - [ - (implementation.execute, ("All",)) - for implementation, fb in zip(exp.sub_workspace_list, exp.prop_dev_feedback) - if implementation and fb - ], # only execute successfully feedback - n=RD_AGENT_SETTINGS.multi_proc_n, - ) - error_message = "" - for message, df in message_and_df_list: - # Check if factor generation was successful - if df is not None and "datetime" in df.index.names: - time_diff = df.index.get_level_values("datetime").to_series().diff().dropna().unique() - if pd.Timedelta(minutes=1) not in time_diff: - factor_dfs.append(df) - logger.info( - f"Factor data from {exp.hypothesis.concise_justification} is successfully generated." - ) - else: - logger.warning(f"Factor data from {exp.hypothesis.concise_justification} is not generated.") - else: - error_message += f"Factor data from {exp.hypothesis.concise_justification} is not generated because of {message}" - logger.warning( - f"Factor data from {exp.hypothesis.concise_justification} is not generated because of {message}" - ) + if not isinstance(exp, QlibFactorExperiment): + continue + + source_name = exp.hypothesis.concise_justification if exp.hypothesis else "BASE factor files" + base_feature_workspaces = _build_base_feature_workspaces(exp) + execute_calls = _build_execute_calls(exp, base_feature_workspaces) + if not execute_calls: + continue + + message_and_df_list = multiprocessing_wrapper(execute_calls, n=RD_AGENT_SETTINGS.multi_proc_n) + for message, df in message_and_df_list: + error_message = _process_message_and_df(source_name, message, df, factor_dfs, error_message) # Combine all successful factor data if factor_dfs: - return pd.concat(factor_dfs, axis=1) + try: + return pd.concat(factor_dfs, axis=1) + except Exception as concat_error: + concat_index_info = " | ".join([f"df#{i}: {_format_index_info(df)}" for i, df in enumerate(factor_dfs)]) + logger.warning( + f"Failed to concat factor data due to index misalignment. concat_error={concat_error}; collected_index_info={concat_index_info}" + ) + raise FactorEmptyError( + "Failed to concat factor data due to index misalignment or incompatible index structure. " + f"concat_error={concat_error}; collected_index_info={concat_index_info}; details={error_message}" + ) from concat_error else: raise FactorEmptyError( f"No valid factor data found to merge (in process_factor_data) because of {error_message}." diff --git a/rdagent/scenarios/qlib/experiment/factor_experiment.py b/rdagent/scenarios/qlib/experiment/factor_experiment.py index 1cf6c685..3fecf60e 100644 --- a/rdagent/scenarios/qlib/experiment/factor_experiment.py +++ b/rdagent/scenarios/qlib/experiment/factor_experiment.py @@ -21,6 +21,10 @@ class QlibFactorExperiment(FactorExperiment[FactorTask, QlibFBWorkspace, FactorF super().__init__(*args, **kwargs) self.experiment_workspace = QlibFBWorkspace(template_folder_path=Path(__file__).parent / "factor_template") self.stdout = "" + self.base_features: dict[str, str] = ( + {} + ) # Qlib features in operator form, e.g., "RESI5": "Resi($close, 5)/$close" + self.base_feature_codes: dict[str, str] = {} # Qlib features in code form class QlibFactorScenario(Scenario): diff --git a/rdagent/scenarios/qlib/experiment/factor_template/conf_baseline.yaml b/rdagent/scenarios/qlib/experiment/factor_template/conf_baseline.yaml index 9ba27ff8..4583dc3e 100644 --- a/rdagent/scenarios/qlib/experiment/factor_template/conf_baseline.yaml +++ b/rdagent/scenarios/qlib/experiment/factor_template/conf_baseline.yaml @@ -8,30 +8,36 @@ benchmark: &benchmark SH000300 data_handler_config: &data_handler_config start_time: {{ train_start | default("2008-01-01", true) }} end_time: {{ test_end | default("null", true) }} - fit_start_time: {{ train_start | default("2008-01-01", true) }} - fit_end_time: {{ train_end | default("2014-12-31", true) }} instruments: *market + data_loader: + class: NestedDataLoader + kwargs: + dataloader_l: + - class: qlib.contrib.data.loader.Alpha158DL + kwargs: + config: + label: + - ["Ref($close, -2)/Ref($close, -1) - 1"] + - ["LABEL0"] + feature: + - {{ feature_expressions }} + - {{ feature_names }} + infer_processors: - - class: FilterCol - kwargs: - fields_group: feature - col_list: ["RESI5", "WVMA5", "RSQR5", "KLEN", "RSQR10", "CORR5", "CORD5", "CORR10", - "ROC60", "RESI10", "VSTD5", "RSQR60", "CORR60", "WVMA60", "STD5", - "RSQR20", "CORD60", "CORD10", "CORR20", "KLOW" - ] - class: RobustZScoreNorm kwargs: fields_group: feature clip_outlier: true + fit_start_time: {{ train_start | default("2008-01-01", true) }} + fit_end_time: {{ train_end | default("2014-12-31", true) }} - class: Fillna kwargs: fields_group: feature learn_processors: - class: DropnaLabel - - class: CSRankNorm + - class: CSZScoreNorm kwargs: fields_group: label - label: ["Ref($close, -2) / Ref($close, -1) - 1"] port_analysis_config: &port_analysis_config strategy: @@ -52,6 +58,7 @@ port_analysis_config: &port_analysis_config open_cost: 0.0005 close_cost: 0.0015 min_cost: 5 + task: model: class: LGBModel @@ -71,7 +78,7 @@ task: module_path: qlib.data.dataset kwargs: handler: - class: Alpha158 + class: DataHandlerLP module_path: qlib.contrib.data.handler kwargs: *data_handler_config segments: diff --git a/rdagent/scenarios/qlib/experiment/factor_template/conf_combined_factors.yaml b/rdagent/scenarios/qlib/experiment/factor_template/conf_combined_factors.yaml index 5849a793..df535694 100644 --- a/rdagent/scenarios/qlib/experiment/factor_template/conf_combined_factors.yaml +++ b/rdagent/scenarios/qlib/experiment/factor_template/conf_combined_factors.yaml @@ -20,17 +20,8 @@ data_handler_config: &data_handler_config - ["Ref($close, -2)/Ref($close, -1) - 1"] - ["LABEL0"] feature: - - ["Resi($close, 5)/$close", "Std(Abs($close/Ref($close, 1)-1)*$volume, 5)/(Mean(Abs($close/Ref($close, 1)-1)*$volume, 5)+1e-12)", - "Rsquare($close, 5)", "($high-$low)/$open", "Rsquare($close, 10)", "Corr($close, Log($volume+1), 5)", - "Corr($close/Ref($close,1), Log($volume/Ref($volume, 1)+1), 5)", "Corr($close, Log($volume+1), 10)", - "Ref($close, 60)/$close", "Resi($close, 10)/$close", "Std($volume, 5)/($volume+1e-12)", - "Rsquare($close, 60)", "Corr($close, Log($volume+1), 60)", "Std(Abs($close/Ref($close, 1)-1)*$volume, 60)/(Mean(Abs($close/Ref($close, 1)-1)*$volume, 60)+1e-12)", - "Std($close, 5)/$close", "Rsquare($close, 20)", "Corr($close/Ref($close,1), Log($volume/Ref($volume, 1)+1), 60)", - "Corr($close/Ref($close,1), Log($volume/Ref($volume, 1)+1), 10)", "Corr($close, Log($volume+1), 20)", - "(Less($open, $close)-$low)/$open"] - - ["RESI5", "WVMA5", "RSQR5", "KLEN", "RSQR10", "CORR5", "CORD5", "CORR10", - "ROC60", "RESI10", "VSTD5", "RSQR60", "CORR60", "WVMA60", "STD5", - "RSQR20", "CORD60", "CORD10", "CORR20", "KLOW"] + - {{ feature_expressions }} + - {{ feature_names }} - class: qlib.data.dataset.loader.StaticDataLoader kwargs: config: "combined_factors_df.parquet" diff --git a/rdagent/scenarios/qlib/experiment/factor_template/conf_combined_factors_sota_model.yaml b/rdagent/scenarios/qlib/experiment/factor_template/conf_combined_factors_sota_model.yaml index e2b7da69..f47941e0 100644 --- a/rdagent/scenarios/qlib/experiment/factor_template/conf_combined_factors_sota_model.yaml +++ b/rdagent/scenarios/qlib/experiment/factor_template/conf_combined_factors_sota_model.yaml @@ -20,17 +20,8 @@ data_handler_config: &data_handler_config - ["Ref($close, -2)/Ref($close, -1) - 1"] - ["LABEL0"] feature: - - ["Resi($close, 5)/$close", "Std(Abs($close/Ref($close, 1)-1)*$volume, 5)/(Mean(Abs($close/Ref($close, 1)-1)*$volume, 5)+1e-12)", - "Rsquare($close, 5)", "($high-$low)/$open", "Rsquare($close, 10)", "Corr($close, Log($volume+1), 5)", - "Corr($close/Ref($close,1), Log($volume/Ref($volume, 1)+1), 5)", "Corr($close, Log($volume+1), 10)", - "Ref($close, 60)/$close", "Resi($close, 10)/$close", "Std($volume, 5)/($volume+1e-12)", - "Rsquare($close, 60)", "Corr($close, Log($volume+1), 60)", "Std(Abs($close/Ref($close, 1)-1)*$volume, 60)/(Mean(Abs($close/Ref($close, 1)-1)*$volume, 60)+1e-12)", - "Std($close, 5)/$close", "Rsquare($close, 20)", "Corr($close/Ref($close,1), Log($volume/Ref($volume, 1)+1), 60)", - "Corr($close/Ref($close,1), Log($volume/Ref($volume, 1)+1), 10)", "Corr($close, Log($volume+1), 20)", - "(Less($open, $close)-$low)/$open"] - - ["RESI5", "WVMA5", "RSQR5", "KLEN", "RSQR10", "CORR5", "CORD5", "CORR10", - "ROC60", "RESI10", "VSTD5", "RSQR60", "CORR60", "WVMA60", "STD5", - "RSQR20", "CORD60", "CORD10", "CORR20", "KLOW"] + - {{ feature_expressions }} + - {{ feature_names }} - class: qlib.data.dataset.loader.StaticDataLoader kwargs: config: "combined_factors_df.parquet" diff --git a/rdagent/scenarios/qlib/experiment/model_experiment.py b/rdagent/scenarios/qlib/experiment/model_experiment.py index 947de99f..76c9d8b7 100644 --- a/rdagent/scenarios/qlib/experiment/model_experiment.py +++ b/rdagent/scenarios/qlib/experiment/model_experiment.py @@ -20,6 +20,7 @@ class QlibModelExperiment(ModelExperiment[ModelTask, QlibFBWorkspace, ModelFBWor super().__init__(*args, **kwargs) self.experiment_workspace = QlibFBWorkspace(template_folder_path=Path(__file__).parent / "model_template") self.stdout = "" + self.base_features: dict[str, str] = {} class QlibModelScenario(Scenario): diff --git a/rdagent/scenarios/qlib/experiment/model_template/conf_baseline_factors_model.yaml b/rdagent/scenarios/qlib/experiment/model_template/conf_baseline_factors_model.yaml index 1012d590..ef6cb0a9 100644 --- a/rdagent/scenarios/qlib/experiment/model_template/conf_baseline_factors_model.yaml +++ b/rdagent/scenarios/qlib/experiment/model_template/conf_baseline_factors_model.yaml @@ -3,33 +3,40 @@ qlib_init: region: cn market: &market csi300 benchmark: &benchmark SH000300 + data_handler_config: &data_handler_config start_time: {{ train_start | default("2008-01-01", true) }} end_time: {{ test_end | default("null", true) }} - fit_start_time: {{ train_start | default("2008-01-01", true) }} - fit_end_time: {{ train_end | default("2014-12-31", true) }} instruments: *market + data_loader: + class: NestedDataLoader + kwargs: + dataloader_l: + - class: qlib.contrib.data.loader.Alpha158DL + kwargs: + config: + label: + - ["Ref($close, -2)/Ref($close, -1) - 1"] + - ["LABEL0"] + feature: + - {{ feature_expressions }} + - {{ feature_names }} + infer_processors: - - class: FilterCol - kwargs: - fields_group: feature - col_list: ["RESI5", "WVMA5", "RSQR5", "KLEN", "RSQR10", "CORR5", "CORD5", "CORR10", - "ROC60", "RESI10", "VSTD5", "RSQR60", "CORR60", "WVMA60", "STD5", - "RSQR20", "CORD60", "CORD10", "CORR20", "KLOW" - ] - class: RobustZScoreNorm kwargs: fields_group: feature clip_outlier: true + fit_start_time: {{ train_start | default("2008-01-01", true) }} + fit_end_time: {{ train_end | default("2014-12-31", true) }} - class: Fillna kwargs: fields_group: feature learn_processors: - class: DropnaLabel - - class: CSRankNorm + - class: CSZScoreNorm kwargs: fields_group: label - label: ["Ref($close, -2) / Ref($close, -1) - 1"] port_analysis_config: &port_analysis_config strategy: @@ -74,7 +81,7 @@ task: module_path: qlib.data.dataset kwargs: handler: - class: Alpha158 + class: DataHandlerLP module_path: qlib.contrib.data.handler kwargs: *data_handler_config segments: diff --git a/rdagent/scenarios/qlib/experiment/model_template/conf_sota_factors_model.yaml b/rdagent/scenarios/qlib/experiment/model_template/conf_sota_factors_model.yaml index e2b7da69..f47941e0 100644 --- a/rdagent/scenarios/qlib/experiment/model_template/conf_sota_factors_model.yaml +++ b/rdagent/scenarios/qlib/experiment/model_template/conf_sota_factors_model.yaml @@ -20,17 +20,8 @@ data_handler_config: &data_handler_config - ["Ref($close, -2)/Ref($close, -1) - 1"] - ["LABEL0"] feature: - - ["Resi($close, 5)/$close", "Std(Abs($close/Ref($close, 1)-1)*$volume, 5)/(Mean(Abs($close/Ref($close, 1)-1)*$volume, 5)+1e-12)", - "Rsquare($close, 5)", "($high-$low)/$open", "Rsquare($close, 10)", "Corr($close, Log($volume+1), 5)", - "Corr($close/Ref($close,1), Log($volume/Ref($volume, 1)+1), 5)", "Corr($close, Log($volume+1), 10)", - "Ref($close, 60)/$close", "Resi($close, 10)/$close", "Std($volume, 5)/($volume+1e-12)", - "Rsquare($close, 60)", "Corr($close, Log($volume+1), 60)", "Std(Abs($close/Ref($close, 1)-1)*$volume, 60)/(Mean(Abs($close/Ref($close, 1)-1)*$volume, 60)+1e-12)", - "Std($close, 5)/$close", "Rsquare($close, 20)", "Corr($close/Ref($close,1), Log($volume/Ref($volume, 1)+1), 60)", - "Corr($close/Ref($close,1), Log($volume/Ref($volume, 1)+1), 10)", "Corr($close, Log($volume+1), 20)", - "(Less($open, $close)-$low)/$open"] - - ["RESI5", "WVMA5", "RSQR5", "KLEN", "RSQR10", "CORR5", "CORD5", "CORR10", - "ROC60", "RESI10", "VSTD5", "RSQR60", "CORR60", "WVMA60", "STD5", - "RSQR20", "CORD60", "CORD10", "CORR20", "KLOW"] + - {{ feature_expressions }} + - {{ feature_names }} - class: qlib.data.dataset.loader.StaticDataLoader kwargs: config: "combined_factors_df.parquet" diff --git a/rdagent/utils/qlib.py b/rdagent/utils/qlib.py new file mode 100644 index 00000000..5a56844a --- /dev/null +++ b/rdagent/utils/qlib.py @@ -0,0 +1,208 @@ +from rdagent.core.experiment import FBWorkspace +from rdagent.utils.env import QlibCondaConf, QlibCondaEnv + +ALPHA20 = { + "RESI5": "Resi($close, 5)/$close", + "WVMA5": "Std(Abs($close/Ref($close, 1)-1)*$volume, 5)/(Mean(Abs($close/Ref($close, 1)-1)*$volume, 5)+1e-12)", + "RSQR5": "Rsquare($close, 5)", + "KLEN": "($high-$low)/$open", + "RSQR10": "Rsquare($close, 10)", + "CORR5": "Corr($close, Log($volume+1), 5)", + "CORD5": "Corr($close/Ref($close,1), Log($volume/Ref($volume, 1)+1), 5)", + "CORR10": "Corr($close, Log($volume+1), 10)", + "ROC60": "Ref($close, 60)/$close", + "RESI10": "Resi($close, 10)/$close", + "VSTD5": "Std($volume, 5)/($volume+1e-12)", + "RSQR60": "Rsquare($close, 60)", + "CORR60": "Corr($close, Log($volume+1), 60)", + "WVMA60": "Std(Abs($close/Ref($close, 1)-1)*$volume, 60)/(Mean(Abs($close/Ref($close, 1)-1)*$volume, 60)+1e-12)", + "STD5": "Std($close, 5)/$close", + "RSQR20": "Rsquare($close, 20)", + "CORD60": "Corr($close/Ref($close,1), Log($volume/Ref($volume, 1)+1), 60)", + "CORD10": "Corr($close/Ref($close,1), Log($volume/Ref($volume, 1)+1), 10)", + "CORR20": "Corr($close, Log($volume+1), 20)", + "KLOW": "(Less($open, $close)-$low)/$open", +} + +ALPHA158 = { + "KMID": "($close-$open)/$open", + "KLEN": "($high-$low)/$open", + "KMID2": "($close-$open)/($high-$low+1e-12)", + "KUP": "($high-Greater($open, $close))/$open", + "KUP2": "($high-Greater($open, $close))/($high-$low+1e-12)", + "KLOW": "(Less($open, $close)-$low)/$open", + "KLOW2": "(Less($open, $close)-$low)/($high-$low+1e-12)", + "KSFT": "(2*$close-$high-$low)/$open", + "KSFT2": "(2*$close-$high-$low)/($high-$low+1e-12)", + "OPEN0": "$open/$close", + "HIGH0": "$high/$close", + "LOW0": "$low/$close", + "VWAP0": "$vwap/$close", + "ROC5": "Ref($close, 5)/$close", + "ROC10": "Ref($close, 10)/$close", + "ROC20": "Ref($close, 20)/$close", + "ROC30": "Ref($close, 30)/$close", + "ROC60": "Ref($close, 60)/$close", + "MA5": "Mean($close, 5)/$close", + "MA10": "Mean($close, 10)/$close", + "MA20": "Mean($close, 20)/$close", + "MA30": "Mean($close, 30)/$close", + "MA60": "Mean($close, 60)/$close", + "STD5": "Std($close, 5)/$close", + "STD10": "Std($close, 10)/$close", + "STD20": "Std($close, 20)/$close", + "STD30": "Std($close, 30)/$close", + "STD60": "Std($close, 60)/$close", + "BETA5": "Slope($close, 5)/$close", + "BETA10": "Slope($close, 10)/$close", + "BETA20": "Slope($close, 20)/$close", + "BETA30": "Slope($close, 30)/$close", + "BETA60": "Slope($close, 60)/$close", + "RSQR5": "Rsquare($close, 5)", + "RSQR10": "Rsquare($close, 10)", + "RSQR20": "Rsquare($close, 20)", + "RSQR30": "Rsquare($close, 30)", + "RSQR60": "Rsquare($close, 60)", + "RESI5": "Resi($close, 5)/$close", + "RESI10": "Resi($close, 10)/$close", + "RESI20": "Resi($close, 20)/$close", + "RESI30": "Resi($close, 30)/$close", + "RESI60": "Resi($close, 60)/$close", + "MAX5": "Max($high, 5)/$close", + "MAX10": "Max($high, 10)/$close", + "MAX20": "Max($high, 20)/$close", + "MAX30": "Max($high, 30)/$close", + "MAX60": "Max($high, 60)/$close", + "MIN5": "Min($low, 5)/$close", + "MIN10": "Min($low, 10)/$close", + "MIN20": "Min($low, 20)/$close", + "MIN30": "Min($low, 30)/$close", + "MIN60": "Min($low, 60)/$close", + "QTLU5": "Quantile($close, 5, 0.8)/$close", + "QTLU10": "Quantile($close, 10, 0.8)/$close", + "QTLU20": "Quantile($close, 20, 0.8)/$close", + "QTLU30": "Quantile($close, 30, 0.8)/$close", + "QTLU60": "Quantile($close, 60, 0.8)/$close", + "QTLD5": "Quantile($close, 5, 0.2)/$close", + "QTLD10": "Quantile($close, 10, 0.2)/$close", + "QTLD20": "Quantile($close, 20, 0.2)/$close", + "QTLD30": "Quantile($close, 30, 0.2)/$close", + "QTLD60": "Quantile($close, 60, 0.2)/$close", + "RANK5": "Rank($close, 5)", + "RANK10": "Rank($close, 10)", + "RANK20": "Rank($close, 20)", + "RANK30": "Rank($close, 30)", + "RANK60": "Rank($close, 60)", + "RSV5": "($close-Min($low, 5))/(Max($high, 5)-Min($low, 5)+1e-12)", + "RSV10": "($close-Min($low, 10))/(Max($high, 10)-Min($low, 10)+1e-12)", + "RSV20": "($close-Min($low, 20))/(Max($high, 20)-Min($low, 20)+1e-12)", + "RSV30": "($close-Min($low, 30))/(Max($high, 30)-Min($low, 30)+1e-12)", + "RSV60": "($close-Min($low, 60))/(Max($high, 60)-Min($low, 60)+1e-12)", + "IMAX5": "IdxMax($high, 5)/5", + "IMAX10": "IdxMax($high, 10)/10", + "IMAX20": "IdxMax($high, 20)/20", + "IMAX30": "IdxMax($high, 30)/30", + "IMAX60": "IdxMax($high, 60)/60", + "IMIN5": "IdxMin($low, 5)/5", + "IMIN10": "IdxMin($low, 10)/10", + "IMIN20": "IdxMin($low, 20)/20", + "IMIN30": "IdxMin($low, 30)/30", + "IMIN60": "IdxMin($low, 60)/60", + "IMXD5": "(IdxMax($high, 5)-IdxMin($low, 5))/5", + "IMXD10": "(IdxMax($high, 10)-IdxMin($low, 10))/10", + "IMXD20": "(IdxMax($high, 20)-IdxMin($low, 20))/20", + "IMXD30": "(IdxMax($high, 30)-IdxMin($low, 30))/30", + "IMXD60": "(IdxMax($high, 60)-IdxMin($low, 60))/60", + "CORR5": "Corr($close, Log($volume+1), 5)", + "CORR10": "Corr($close, Log($volume+1), 10)", + "CORR20": "Corr($close, Log($volume+1), 20)", + "CORR30": "Corr($close, Log($volume+1), 30)", + "CORR60": "Corr($close, Log($volume+1), 60)", + "CORD5": "Corr($close/Ref($close,1), Log($volume/Ref($volume, 1)+1), 5)", + "CORD10": "Corr($close/Ref($close,1), Log($volume/Ref($volume, 1)+1), 10)", + "CORD20": "Corr($close/Ref($close,1), Log($volume/Ref($volume, 1)+1), 20)", + "CORD30": "Corr($close/Ref($close,1), Log($volume/Ref($volume, 1)+1), 30)", + "CORD60": "Corr($close/Ref($close,1), Log($volume/Ref($volume, 1)+1), 60)", + "CNTP5": "Mean($close>Ref($close, 1), 5)", + "CNTP10": "Mean($close>Ref($close, 1), 10)", + "CNTP20": "Mean($close>Ref($close, 1), 20)", + "CNTP30": "Mean($close>Ref($close, 1), 30)", + "CNTP60": "Mean($close>Ref($close, 1), 60)", + "CNTN5": "Mean($closeRef($close, 1), 5)-Mean($closeRef($close, 1), 10)-Mean($closeRef($close, 1), 20)-Mean($closeRef($close, 1), 30)-Mean($closeRef($close, 1), 60)-Mean($close bool: + _TFW.inject_files(**{"test_fea.py": TEST_FEATURE_CODE.format(experessions=str(expressions))}) + + qlib_env = QlibCondaEnv(conf=QlibCondaConf()) + qlib_env.prepare() + res = _TFW.run( + env=qlib_env, + entry="python test_fea.py", + ) + return res.exit_code == 0 diff --git a/rdagent/utils/workflow/loop.py b/rdagent/utils/workflow/loop.py index c5548529..fe54d986 100644 --- a/rdagent/utils/workflow/loop.py +++ b/rdagent/utils/workflow/loop.py @@ -11,6 +11,7 @@ Postscripts: import asyncio import concurrent.futures import copy +import multiprocessing.queues import os import pickle from collections import defaultdict @@ -528,8 +529,11 @@ class LoopBase: def __getstate__(self) -> dict[str, Any]: res = {} for k, v in self.__dict__.items(): - if k not in ["queue", "semaphores", "_pbar"]: - res[k] = v + if k in ["queue", "semaphores", "_pbar"]: + continue + if isinstance(v, multiprocessing.queues.Queue): # interaction queues are not picklable + continue + res[k] = v return res def __setstate__(self, state: dict[str, Any]) -> None: diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 00000000..4843aa18 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,23 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* +dist +node_modules +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/web/README.md b/web/README.md new file mode 100644 index 00000000..227f9c4d --- /dev/null +++ b/web/README.md @@ -0,0 +1,33 @@ +# R&D-Agent + +## Project setup + +``` +npm install +``` + +### Compiles and hot-reloads for development + +``` +npm run dev +``` + +### Compiles and minifies for production + +``` +npm run build +``` + +### API URL behavior after build + +This project uses the current page origin as the API base URL. + +If the built frontend is served by the same Flask server that also exposes `/upload`, `/trace`, `/control` and other APIs, no extra frontend configuration is needed. The frontend will automatically call the same host and port that served the page. + +This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` + + + R&D-Agent + + + +
+ + + + \ No newline at end of file diff --git a/web/src/App.vue b/web/src/App.vue new file mode 100644 index 00000000..36cbedca --- /dev/null +++ b/web/src/App.vue @@ -0,0 +1,55 @@ + + + + diff --git a/web/src/assets/icon/Graph-Dot.svg b/web/src/assets/icon/Graph-Dot.svg new file mode 100644 index 00000000..31dc1059 --- /dev/null +++ b/web/src/assets/icon/Graph-Dot.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/web/src/assets/icon/Piggy-Bank.svg b/web/src/assets/icon/Piggy-Bank.svg new file mode 100644 index 00000000..c0051718 --- /dev/null +++ b/web/src/assets/icon/Piggy-Bank.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/web/src/assets/icon/Tablet-Capsule.svg b/web/src/assets/icon/Tablet-Capsule.svg new file mode 100644 index 00000000..7ff47bf3 --- /dev/null +++ b/web/src/assets/icon/Tablet-Capsule.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/web/src/assets/icon/Web-Streamline.svg b/web/src/assets/icon/Web-Streamline.svg new file mode 100644 index 00000000..fa238f65 --- /dev/null +++ b/web/src/assets/icon/Web-Streamline.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/web/src/assets/icon/arrow-right.svg b/web/src/assets/icon/arrow-right.svg new file mode 100644 index 00000000..166bb088 --- /dev/null +++ b/web/src/assets/icon/arrow-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/src/assets/icon/arrows-mark.svg b/web/src/assets/icon/arrows-mark.svg new file mode 100644 index 00000000..076dd40e --- /dev/null +++ b/web/src/assets/icon/arrows-mark.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/src/assets/icon/copy.svg b/web/src/assets/icon/copy.svg new file mode 100644 index 00000000..ccf50713 --- /dev/null +++ b/web/src/assets/icon/copy.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/src/assets/icon/delete.svg b/web/src/assets/icon/delete.svg new file mode 100644 index 00000000..dbd2b128 --- /dev/null +++ b/web/src/assets/icon/delete.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/web/src/assets/icon/edit.svg b/web/src/assets/icon/edit.svg new file mode 100644 index 00000000..9717ce2e --- /dev/null +++ b/web/src/assets/icon/edit.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/web/src/assets/icon/fullscreen.svg b/web/src/assets/icon/fullscreen.svg new file mode 100644 index 00000000..9b9d6bb1 --- /dev/null +++ b/web/src/assets/icon/fullscreen.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/src/assets/icon/loops.svg b/web/src/assets/icon/loops.svg new file mode 100644 index 00000000..0982e734 --- /dev/null +++ b/web/src/assets/icon/loops.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/src/assets/icon/pause.svg b/web/src/assets/icon/pause.svg new file mode 100644 index 00000000..83e80892 --- /dev/null +++ b/web/src/assets/icon/pause.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/web/src/assets/icon/pg-process.svg b/web/src/assets/icon/pg-process.svg new file mode 100644 index 00000000..582395eb --- /dev/null +++ b/web/src/assets/icon/pg-process.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/web/src/assets/icon/pg-result.svg b/web/src/assets/icon/pg-result.svg new file mode 100644 index 00000000..a87afb64 --- /dev/null +++ b/web/src/assets/icon/pg-result.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/src/assets/icon/prime_upload.svg b/web/src/assets/icon/prime_upload.svg new file mode 100644 index 00000000..6236f53a --- /dev/null +++ b/web/src/assets/icon/prime_upload.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/web/src/assets/icon/right-arrow.svg b/web/src/assets/icon/right-arrow.svg new file mode 100644 index 00000000..d06bd037 --- /dev/null +++ b/web/src/assets/icon/right-arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/src/assets/images/Data-Driven.png b/web/src/assets/images/Data-Driven.png new file mode 100644 index 00000000..0bd78996 Binary files /dev/null and b/web/src/assets/images/Data-Driven.png differ diff --git a/web/src/assets/images/Framework.png b/web/src/assets/images/Framework.png new file mode 100644 index 00000000..8dbc17a5 Binary files /dev/null and b/web/src/assets/images/Framework.png differ diff --git a/web/src/assets/images/Microsoft-logo.png b/web/src/assets/images/Microsoft-logo.png new file mode 100644 index 00000000..73fad16b Binary files /dev/null and b/web/src/assets/images/Microsoft-logo.png differ diff --git a/web/src/assets/images/RDAgent-logo-old.png b/web/src/assets/images/RDAgent-logo-old.png new file mode 100644 index 00000000..a72746c0 Binary files /dev/null and b/web/src/assets/images/RDAgent-logo-old.png differ diff --git a/web/src/assets/images/RDAgent-logo.png b/web/src/assets/images/RDAgent-logo.png new file mode 100644 index 00000000..717ec194 Binary files /dev/null and b/web/src/assets/images/RDAgent-logo.png differ diff --git a/web/src/assets/images/bg-1.png b/web/src/assets/images/bg-1.png new file mode 100644 index 00000000..090ca591 Binary files /dev/null and b/web/src/assets/images/bg-1.png differ diff --git a/web/src/assets/images/big-bg-active.png b/web/src/assets/images/big-bg-active.png new file mode 100644 index 00000000..26f33d2f Binary files /dev/null and b/web/src/assets/images/big-bg-active.png differ diff --git a/web/src/assets/images/big-bg.png b/web/src/assets/images/big-bg.png new file mode 100644 index 00000000..3ecc1efc Binary files /dev/null and b/web/src/assets/images/big-bg.png differ diff --git a/web/src/assets/images/continue-img.png b/web/src/assets/images/continue-img.png new file mode 100644 index 00000000..9c2b497c Binary files /dev/null and b/web/src/assets/images/continue-img.png differ diff --git a/web/src/assets/images/down-arrow.svg b/web/src/assets/images/down-arrow.svg new file mode 100644 index 00000000..8c514d87 --- /dev/null +++ b/web/src/assets/images/down-arrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/assets/images/file-upload-active.svg b/web/src/assets/images/file-upload-active.svg new file mode 100644 index 00000000..5d30652b --- /dev/null +++ b/web/src/assets/images/file-upload-active.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/web/src/assets/images/file-upload.svg b/web/src/assets/images/file-upload.svg new file mode 100644 index 00000000..ef5bf582 --- /dev/null +++ b/web/src/assets/images/file-upload.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/web/src/assets/images/first-time-img.png b/web/src/assets/images/first-time-img.png new file mode 100644 index 00000000..8777d428 Binary files /dev/null and b/web/src/assets/images/first-time-img.png differ diff --git a/web/src/assets/images/footer-bg.png b/web/src/assets/images/footer-bg.png new file mode 100644 index 00000000..2cb86600 Binary files /dev/null and b/web/src/assets/images/footer-bg.png differ diff --git a/web/src/assets/images/rd_icon.png b/web/src/assets/images/rd_icon.png new file mode 100644 index 00000000..c597a571 Binary files /dev/null and b/web/src/assets/images/rd_icon.png differ diff --git a/web/src/assets/images/small-bg-active.png b/web/src/assets/images/small-bg-active.png new file mode 100644 index 00000000..47c5c7bb Binary files /dev/null and b/web/src/assets/images/small-bg-active.png differ diff --git a/web/src/assets/images/small-bg.png b/web/src/assets/images/small-bg.png new file mode 100644 index 00000000..48c9054e Binary files /dev/null and b/web/src/assets/images/small-bg.png differ diff --git a/web/src/assets/images/upload-active.svg b/web/src/assets/images/upload-active.svg new file mode 100644 index 00000000..8f2ef15a --- /dev/null +++ b/web/src/assets/images/upload-active.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/web/src/assets/images/upload.svg b/web/src/assets/images/upload.svg new file mode 100644 index 00000000..09a82d44 --- /dev/null +++ b/web/src/assets/images/upload.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/web/src/assets/playground-images/Conclusion-expand.svg b/web/src/assets/playground-images/Conclusion-expand.svg new file mode 100644 index 00000000..fce6fc41 --- /dev/null +++ b/web/src/assets/playground-images/Conclusion-expand.svg @@ -0,0 +1,4 @@ + + + + diff --git a/web/src/assets/playground-images/Hypothesis-expand.svg b/web/src/assets/playground-images/Hypothesis-expand.svg new file mode 100644 index 00000000..a1dded9d --- /dev/null +++ b/web/src/assets/playground-images/Hypothesis-expand.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/web/src/assets/playground-images/Observation-expand.svg b/web/src/assets/playground-images/Observation-expand.svg new file mode 100644 index 00000000..45665421 --- /dev/null +++ b/web/src/assets/playground-images/Observation-expand.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/web/src/assets/playground-images/Reason-expand.svg b/web/src/assets/playground-images/Reason-expand.svg new file mode 100644 index 00000000..ab74bc3e --- /dev/null +++ b/web/src/assets/playground-images/Reason-expand.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/web/src/assets/playground-images/add.svg b/web/src/assets/playground-images/add.svg new file mode 100644 index 00000000..b43f3a4d --- /dev/null +++ b/web/src/assets/playground-images/add.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/web/src/assets/playground-images/close.svg b/web/src/assets/playground-images/close.svg new file mode 100644 index 00000000..8e7fac94 --- /dev/null +++ b/web/src/assets/playground-images/close.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/src/assets/playground-images/download.svg b/web/src/assets/playground-images/download.svg new file mode 100644 index 00000000..85a71524 --- /dev/null +++ b/web/src/assets/playground-images/download.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/web/src/assets/playground-images/fullscreen.svg b/web/src/assets/playground-images/fullscreen.svg new file mode 100644 index 00000000..64183ad6 --- /dev/null +++ b/web/src/assets/playground-images/fullscreen.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/src/assets/playground-images/loading-tab.gif b/web/src/assets/playground-images/loading-tab.gif new file mode 100644 index 00000000..6b7a01a1 Binary files /dev/null and b/web/src/assets/playground-images/loading-tab.gif differ diff --git a/web/src/assets/playground-images/loop-Sucess.svg b/web/src/assets/playground-images/loop-Sucess.svg new file mode 100644 index 00000000..ae26e080 --- /dev/null +++ b/web/src/assets/playground-images/loop-Sucess.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/web/src/assets/playground-images/loop-default.svg b/web/src/assets/playground-images/loop-default.svg new file mode 100644 index 00000000..edc046bf --- /dev/null +++ b/web/src/assets/playground-images/loop-default.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/web/src/assets/playground-images/loop-error.svg b/web/src/assets/playground-images/loop-error.svg new file mode 100644 index 00000000..d1b82c4d --- /dev/null +++ b/web/src/assets/playground-images/loop-error.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/web/src/assets/playground-images/loop-loading.gif b/web/src/assets/playground-images/loop-loading.gif new file mode 100644 index 00000000..8c00434d Binary files /dev/null and b/web/src/assets/playground-images/loop-loading.gif differ diff --git a/web/src/assets/playground-images/loop-loading.svg b/web/src/assets/playground-images/loop-loading.svg new file mode 100644 index 00000000..5ed6f192 --- /dev/null +++ b/web/src/assets/playground-images/loop-loading.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/web/src/assets/playground-images/process-checked.svg b/web/src/assets/playground-images/process-checked.svg new file mode 100644 index 00000000..953839c9 --- /dev/null +++ b/web/src/assets/playground-images/process-checked.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/web/src/assets/playground-images/process-fail-checked.svg b/web/src/assets/playground-images/process-fail-checked.svg new file mode 100644 index 00000000..64e69c47 --- /dev/null +++ b/web/src/assets/playground-images/process-fail-checked.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/web/src/assets/playground-images/process-fail.svg b/web/src/assets/playground-images/process-fail.svg new file mode 100644 index 00000000..10f6f832 --- /dev/null +++ b/web/src/assets/playground-images/process-fail.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/web/src/assets/playground-images/process-success.svg b/web/src/assets/playground-images/process-success.svg new file mode 100644 index 00000000..f83b6d77 --- /dev/null +++ b/web/src/assets/playground-images/process-success.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/web/src/assets/playground-images/zoom.svg b/web/src/assets/playground-images/zoom.svg new file mode 100644 index 00000000..01f05b24 --- /dev/null +++ b/web/src/assets/playground-images/zoom.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/web/src/assets/vue.svg b/web/src/assets/vue.svg new file mode 100644 index 00000000..770e9d33 --- /dev/null +++ b/web/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/common/code-theme.css b/web/src/common/code-theme.css new file mode 100644 index 00000000..027a1f09 --- /dev/null +++ b/web/src/common/code-theme.css @@ -0,0 +1,163 @@ +/** + * prism.js default theme for JavaScript, CSS and HTML + * Based on dabblet (http://dabblet.com) + * @author Lea Verou + */ + +code[class*="language-"], +pre[class*="language-"] { + color: black; + background: none; + text-shadow: 0 1px white; + font-family: "Source Code Pro", monospace; + font-size: 1em; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + word-wrap: normal; + line-height: 1.5; + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; +} + +pre[class*="language-"]::-moz-selection, +pre[class*="language-"] ::-moz-selection, +code[class*="language-"]::-moz-selection, +code[class*="language-"] ::-moz-selection { + text-shadow: none; + background: #b3d4fc; +} + +pre[class*="language-"]::selection, +pre[class*="language-"] ::selection, +code[class*="language-"]::selection, +code[class*="language-"] ::selection { + text-shadow: none; + background: #b3d4fc; +} + +@media print { + code[class*="language-"], + pre[class*="language-"] { + text-shadow: none; + } +} + + +/* Code blocks */ + +pre[class*="language-"] { + padding: 1em; + margin: .5em 0; + overflow: auto; +} + +:not(pre)>code[class*="language-"], +pre[class*="language-"] { + background: #f5f2f0; +} + + +/* Inline code */ + +:not(pre)>code[class*="language-"] { + padding: .1em; + border-radius: .3em; + white-space: normal; +} + +.token { + /* background: transparent; + border: 0px; + color: inherit; + display: inline; + font-size: 1em; + line-height: inherit; + margin: 0px; + overflow-x: auto; + padding: 0px; + white-space: pre; + word-break: normal; + overflow-wrap: normal; */ + font-family: "consolas", monospace; +} + +.token.comment, +.token.prolog, +.token.doctype, +.token.cdata { + color: rgb(128, 132, 149); + font-style: italic; +} + +.token.punctuation { + color: #999; +} + +.token.namespace { + opacity: .7; +} + +.token.property, +.token.tag, +.token.boolean, +.token.number, +.token.constant, +.token.symbol, +.token.deleted { + color: #905; +} + +.token.selector, +.token.attr-name, +.token.string, +.token.char, +.token.builtin, +.token.inserted { + color: rgb(9, 171, 59); +} + +.token.operator, +.token.entity, +.token.url, +.language-css .token.string, +.style .token.string { + color: rgb(237, 111, 19); +} + +.token.atrule, +.token.attr-value, +.token.keyword { + color: rgb(28, 131, 225); +} + +.token.function, +.token.class-name { + color: rgb(28, 131, 225); + font-weight: 700; +} + +.token.regex, +.token.important, +.token.variable { + color: #e90; +} + +.token.important, +.token.bold { + font-weight: bold; +} + +.token.italic { + font-style: italic; +} + +.token.entity { + cursor: help; +} \ No newline at end of file diff --git a/web/src/common/py-theme.css b/web/src/common/py-theme.css new file mode 100644 index 00000000..3dfb83b0 --- /dev/null +++ b/web/src/common/py-theme.css @@ -0,0 +1,137 @@ +/** + * prism.js tomorrow night eighties for JavaScript, CoffeeScript, CSS and HTML + * Based on https://github.com/chriskempson/tomorrow-theme + * @author Rose Pritchard + */ + +code[class*="language-"], +pre[class*="language-"] { + color: #333; + /* 更改为深灰色,便于白色背景下的阅读 */ + background: none; + font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; + font-size: 1em; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + word-wrap: normal; + line-height: 1.5; + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; + font-weight: 500; +} + + +/* Code blocks */ + +pre[class*="language-"] { + padding: 1em; + margin: .5em 0; + overflow: auto; +} + +:not(pre)>code[class*="language-"], +pre[class*="language-"] { + background: #ffffff; + /* 修改背景为白色 */ +} + + +/* Inline code */ + +:not(pre)>code[class*="language-"] { + padding: .1em; + border-radius: .3em; + white-space: normal; +} + +.token.comment, +.token.block-comment, +.token.prolog, +.token.doctype, +.token.cdata { + color: #6a737d; + /* 调整注释为更深的灰色,增加可读性 */ +} + +.token.punctuation { + color: #555; + /* 调整标点符号为更深的灰色 */ +} + +.token.tag, +.token.attr-name, +.token.namespace, +.token.deleted { + color: #e2777a; + /* 仍保留删除标签的红色 */ +} + +.token.function-name { + color: #3d5a80; + /* 修改函数名为蓝色系 */ +} + +.token.boolean, +.token.number, +.token.function { + color: #f08d49; + /* 保持橙色用于布尔值和数字 */ +} + +.token.property, +.token.class-name, +.token.constant, +.token.symbol { + color: #f8c555; + /* 修改为金黄色系,以便突出 */ +} + +.token.selector, +.token.important, +.token.atrule, +.token.keyword, +.token.builtin { + color: #6fa3f2; + /* 将关键词、选择器等修改为浅蓝色 */ +} + +.token.string, +.token.char, +.token.attr-value, +.token.regex, +.token.variable { + color: #7ec699; + /* 修改字符串为绿色 */ +} + +.token.operator, +.token.entity, +.token.url { + color: #67cdcc; + /* 将运算符和URL改为青绿色 */ +} + +.token.important, +.token.bold { + font-weight: bold; +} + +.token.italic { + font-style: italic; +} + +.token.entity { + cursor: help; +} + +.token.inserted { + color: #28a745; + /* 插入内容为绿色 */ +} \ No newline at end of file diff --git a/web/src/common/reset.css b/web/src/common/reset.css new file mode 100644 index 00000000..81a24d2b --- /dev/null +++ b/web/src/common/reset.css @@ -0,0 +1,197 @@ +html, +body, +div, +span, +applet, +object, +iframe, +h1, +h2, +h3, +h4, +h5, +h6, +p, +blockquote, +pre, +a, +abbr, +acronym, +address, +big, +cite, +del, +dfn, +em, +img, +ins, +kbd, +q, +s, +samp, +small, +strike, +strong, +sub, +sup, +tt, +var, +b, +u, +i, +center, +dl, +dt, +dd, +ol, +ul, +li, +fieldset, +form, +label, +legend, +table, +caption, +tbody, +tfoot, +thead, +tr, +th, +td, +article, +aside, +canvas, +details, +embed, +figure, +figcaption, +footer, +header, +menu, +nav, +output, +ruby, +section, +summary, +time, +mark, +audio, +video, +textarea, +input { + margin: 0; + padding: 0; + border: 0; +} + + +/* HTML5 display-role reset for older browsers */ + +article, +aside, +details, +figcaption, +figure, +footer, +header, +menu, +nav, +section { + display: block; +} + +body { + line-height: 1; +} + +blockquote, +q { + quotes: none; +} + +blockquote::before, +blockquote::after, +q::before, +q::after { + content: none; +} + +table { + border-collapse: collapse; + border-spacing: 0; +} + + +/* custom */ + +a { + color: #7e8c8d; + text-decoration: none; + backface-visibility: hidden; +} + +li { + list-style: none; +} + +::-webkit-scrollbar { + width: 5px; + height: 5px; + /* background-color: rgba(211, 208, 255, 0); */ + opacity: 0; +} + +:hover::-webkit-scrollbar { + opacity: 1; +} + +::-webkit-scrollbar-track { + background-color: rgba(211, 208, 255, 0); + /* 滚动条轨道颜色 */ +} + +::-webkit-scrollbar-track-piece { + /* 滚动条背景色 */ + background-color: rgba(211, 208, 255, 0); + border-radius: 6px; +} + +::-webkit-scrollbar-thumb:vertical { + height: 5px; + /* 滚动条颜色 */ + background-color: #E4E7FF; + /* background: linear-gradient( to bottom, #8D9AFF, #9D42FF); */ + border-radius: 6px; +} + +::-webkit-scrollbar-thumb:horizontal { + width: 5px; + background-color: #E4E7FF; + border-radius: 6px; +} + +::-webkit-scrollbar-corner { + /* background-color: rgba(211, 208, 255, 0); */ + /* color: rgba(211, 208, 255, 0); */ +} + +::-webkit-scrollbar-button { + /* background-color: rgba(211, 208, 255, 0); */ + /* color: rgba(211, 208, 255, 0); */ +} + +html, +body { + width: 100%; + height: 100%; +} + +body { + /* -webkit-user-select: none; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + -webkit-touch-callout: none; */ +} + +* { + -webkit-text-size-adjust: none; +} \ No newline at end of file diff --git a/web/src/components/chartBox.vue b/web/src/components/chartBox.vue new file mode 100644 index 00000000..ab6606a2 --- /dev/null +++ b/web/src/components/chartBox.vue @@ -0,0 +1,153 @@ + + + + + diff --git a/web/src/components/code.vue b/web/src/components/code.vue new file mode 100644 index 00000000..5ef06766 --- /dev/null +++ b/web/src/components/code.vue @@ -0,0 +1,183 @@ + + + + diff --git a/web/src/components/development.vue b/web/src/components/development.vue new file mode 100644 index 00000000..91ce9cdf --- /dev/null +++ b/web/src/components/development.vue @@ -0,0 +1,529 @@ + + + + diff --git a/web/src/components/dialog.vue b/web/src/components/dialog.vue new file mode 100644 index 00000000..7a0e01e4 --- /dev/null +++ b/web/src/components/dialog.vue @@ -0,0 +1,145 @@ + + + + diff --git a/web/src/components/feedback.vue b/web/src/components/feedback.vue new file mode 100644 index 00000000..37da8fd8 --- /dev/null +++ b/web/src/components/feedback.vue @@ -0,0 +1,330 @@ + + + + diff --git a/web/src/components/footer.vue b/web/src/components/footer.vue new file mode 100644 index 00000000..e0f320ec --- /dev/null +++ b/web/src/components/footer.vue @@ -0,0 +1,94 @@ + + + diff --git a/web/src/components/kateX.vue b/web/src/components/kateX.vue new file mode 100644 index 00000000..7e9a6fa4 --- /dev/null +++ b/web/src/components/kateX.vue @@ -0,0 +1,36 @@ + + + + diff --git a/web/src/components/lineChart.vue b/web/src/components/lineChart.vue new file mode 100644 index 00000000..0101e892 --- /dev/null +++ b/web/src/components/lineChart.vue @@ -0,0 +1,130 @@ + + + + + diff --git a/web/src/components/lineChartOne.vue b/web/src/components/lineChartOne.vue new file mode 100644 index 00000000..2316368d --- /dev/null +++ b/web/src/components/lineChartOne.vue @@ -0,0 +1,338 @@ + + + + + diff --git a/web/src/components/loading-dot.vue b/web/src/components/loading-dot.vue new file mode 100644 index 00000000..ee939e1b --- /dev/null +++ b/web/src/components/loading-dot.vue @@ -0,0 +1,23 @@ + + + diff --git a/web/src/components/loading.vue b/web/src/components/loading.vue new file mode 100644 index 00000000..6f3b063d --- /dev/null +++ b/web/src/components/loading.vue @@ -0,0 +1,35 @@ + + + diff --git a/web/src/components/loop-component.vue b/web/src/components/loop-component.vue new file mode 100644 index 00000000..606a162b --- /dev/null +++ b/web/src/components/loop-component.vue @@ -0,0 +1,499 @@ + + + + diff --git a/web/src/components/markdown.vue b/web/src/components/markdown.vue new file mode 100644 index 00000000..916ac408 --- /dev/null +++ b/web/src/components/markdown.vue @@ -0,0 +1,240 @@ + + + + + diff --git a/web/src/components/markdownToHtml.vue b/web/src/components/markdownToHtml.vue new file mode 100644 index 00000000..25ed8966 --- /dev/null +++ b/web/src/components/markdownToHtml.vue @@ -0,0 +1,51 @@ + + + + diff --git a/web/src/components/navBar.vue b/web/src/components/navBar.vue new file mode 100644 index 00000000..0d0220c3 --- /dev/null +++ b/web/src/components/navBar.vue @@ -0,0 +1,73 @@ + + + diff --git a/web/src/components/research.vue b/web/src/components/research.vue new file mode 100644 index 00000000..eb5beee5 --- /dev/null +++ b/web/src/components/research.vue @@ -0,0 +1,648 @@ + + + + diff --git a/web/src/components/saveImage.vue b/web/src/components/saveImage.vue new file mode 100644 index 00000000..691080c3 --- /dev/null +++ b/web/src/components/saveImage.vue @@ -0,0 +1,39 @@ + + + diff --git a/web/src/components/select-component.vue b/web/src/components/select-component.vue new file mode 100644 index 00000000..617d479f --- /dev/null +++ b/web/src/components/select-component.vue @@ -0,0 +1,271 @@ + + + + diff --git a/web/src/components/sm-select-component.vue b/web/src/components/sm-select-component.vue new file mode 100644 index 00000000..e8a1e46c --- /dev/null +++ b/web/src/components/sm-select-component.vue @@ -0,0 +1,250 @@ + + + + diff --git a/web/src/components/step-component.vue b/web/src/components/step-component.vue new file mode 100644 index 00000000..03b7d1aa --- /dev/null +++ b/web/src/components/step-component.vue @@ -0,0 +1,87 @@ + + + + diff --git a/web/src/components/svgIcon.vue b/web/src/components/svgIcon.vue new file mode 100644 index 00000000..04e8d2d5 --- /dev/null +++ b/web/src/components/svgIcon.vue @@ -0,0 +1,39 @@ + + + + diff --git a/web/src/components/swiper.vue b/web/src/components/swiper.vue new file mode 100644 index 00000000..b1239c43 --- /dev/null +++ b/web/src/components/swiper.vue @@ -0,0 +1,146 @@ + + + diff --git a/web/src/components/upload-progress.vue b/web/src/components/upload-progress.vue new file mode 100644 index 00000000..b02d35b6 --- /dev/null +++ b/web/src/components/upload-progress.vue @@ -0,0 +1,436 @@ + + + + diff --git a/web/src/constants/mle-competitions.js b/web/src/constants/mle-competitions.js new file mode 100644 index 00000000..2d2d399d --- /dev/null +++ b/web/src/constants/mle-competitions.js @@ -0,0 +1,77 @@ +export const kaggleCompetitions = [ + { name: "MLE-Bench:new-york-city-taxi-fare-prediction" }, + { name: "MLE-Bench:3d-object-detection-for-autonomous-vehicles" }, + { name: "MLE-Bench:aerial-cactus-identification" }, + { name: "MLE-Bench:AI4Code" }, + { name: "MLE-Bench:alaska2-image-steganalysis" }, + { name: "MLE-Bench:aptos2019-blindness-detection" }, + { name: "MLE-Bench:billion-word-imputation" }, + { name: "MLE-Bench:bms-molecular-translation" }, + { name: "MLE-Bench:cassava-leaf-disease-classification" }, + { name: "MLE-Bench:cdiscount-image-classification-challenge" }, + { name: "MLE-Bench:chaii-hindi-and-tamil-question-answering" }, + { name: "MLE-Bench:champs-scalar-coupling" }, + { name: "MLE-Bench:denoising-dirty-documents" }, + { name: "MLE-Bench:detecting-insults-in-social-commentary" }, + { name: "MLE-Bench:dog-breed-identification" }, + { name: "MLE-Bench:dogs-vs-cats-redux-kernels-edition" }, + { name: "MLE-Bench:facebook-recruiting-iii-keyword-extraction" }, + { name: "MLE-Bench:freesound-audio-tagging-2019" }, + { name: "MLE-Bench:google-quest-challenge" }, + { name: "MLE-Bench:google-research-identify-contrails-reduce-global-warming" }, + { name: "MLE-Bench:h-and-m-personalized-fashion-recommendations" }, + { name: "MLE-Bench:herbarium-2020-fgvc7" }, + { name: "MLE-Bench:herbarium-2021-fgvc8" }, + { name: "MLE-Bench:herbarium-2022-fgvc9" }, + { name: "MLE-Bench:histopathologic-cancer-detection" }, + { name: "MLE-Bench:hms-harmful-brain-activity-classification" }, + { name: "MLE-Bench:hotel-id-2021-fgvc8" }, + { name: "MLE-Bench:hubmap-kidney-segmentation" }, + { name: "MLE-Bench:icecube-neutrinos-in-deep-ice" }, + { name: "MLE-Bench:imet-2020-fgvc7" }, + { name: "MLE-Bench:inaturalist-2019-fgvc6" }, + { name: "MLE-Bench:iwildcam-2019-fgvc6" }, + { name: "MLE-Bench:iwildcam-2020-fgvc7" }, + { name: "MLE-Bench:jigsaw-toxic-comment-classification-challenge" }, + { name: "MLE-Bench:jigsaw-unintended-bias-in-toxicity-classification" }, + { name: "MLE-Bench:kuzushiji-recognition" }, + { name: "MLE-Bench:leaf-classification" }, + { name: "MLE-Bench:learning-agency-lab-automated-essay-scoring-2" }, + { name: "MLE-Bench:lmsys-chatbot-arena" }, + { name: "MLE-Bench:mlsp-2013-birds" }, + { name: "MLE-Bench:multi-modal-gesture-recognition" }, + { name: "MLE-Bench:nfl-player-contact-detection" }, + { name: "MLE-Bench:nomad2018-predict-transparent-conductors" }, + { name: "MLE-Bench:osic-pulmonary-fibrosis-progression" }, + { name: "MLE-Bench:petfinder-pawpularity-score" }, + { name: "MLE-Bench:plant-pathology-2020-fgvc7" }, + { name: "MLE-Bench:plant-pathology-2021-fgvc8" }, + { name: "MLE-Bench:predict-volcanic-eruptions-ingv-oe" }, + { name: "MLE-Bench:random-acts-of-pizza" }, + { name: "MLE-Bench:ranzcr-clip-catheter-line-classification" }, + { name: "MLE-Bench:rsna-2022-cervical-spine-fracture-detection" }, + { name: "MLE-Bench:rsna-breast-cancer-detection" }, + { name: "MLE-Bench:rsna-miccai-brain-tumor-radiogenomic-classification" }, + { name: "MLE-Bench:seti-breakthrough-listen" }, + { name: "MLE-Bench:siim-covid19-detection" }, + { name: "MLE-Bench:siim-isic-melanoma-classification" }, + { name: "MLE-Bench:smartphone-decimeter-2022" }, + { name: "MLE-Bench:spooky-author-identification" }, + { name: "MLE-Bench:stanford-covid-vaccine" }, + { name: "MLE-Bench:statoil-iceberg-classifier-challenge" }, + { name: "MLE-Bench:tabular-playground-series-dec-2021" }, + { name: "MLE-Bench:tabular-playground-series-may-2022" }, + { name: "MLE-Bench:tensorflow2-question-answering" }, + { name: "MLE-Bench:tensorflow-speech-recognition-challenge" }, + { name: "MLE-Bench:text-normalization-challenge-english-language" }, + { name: "MLE-Bench:text-normalization-challenge-russian-language" }, + { name: "MLE-Bench:tgs-salt-identification-challenge" }, + { name: "MLE-Bench:the-icml-2013-whale-challenge-right-whale-redux" }, + { name: "MLE-Bench:tweet-sentiment-extraction" }, + { name: "MLE-Bench:us-patent-phrase-to-phrase-matching" }, + { name: "MLE-Bench:uw-madison-gi-tract-image-segmentation" }, + { name: "MLE-Bench:ventilator-pressure-prediction" }, + { name: "MLE-Bench:vesuvius-challenge-ink-detection" }, + { name: "MLE-Bench:vinbigdata-chest-xray-abnormalities-detection" }, + { name: "MLE-Bench:whale-categorization-playground" }, +]; diff --git a/web/src/constants/qlib.js b/web/src/constants/qlib.js new file mode 100644 index 00000000..b4d71177 --- /dev/null +++ b/web/src/constants/qlib.js @@ -0,0 +1,162 @@ +const ALPHA158 = { + "KMID": "($close-$open)/$open", + "KLEN": "($high-$low)/$open", + "KMID2": "($close-$open)/($high-$low+1e-12)", + "KUP": "($high-Greater($open, $close))/$open", + "KUP2": "($high-Greater($open, $close))/($high-$low+1e-12)", + "KLOW": "(Less($open, $close)-$low)/$open", + "KLOW2": "(Less($open, $close)-$low)/($high-$low+1e-12)", + "KSFT": "(2*$close-$high-$low)/$open", + "KSFT2": "(2*$close-$high-$low)/($high-$low+1e-12)", + "OPEN0": "$open/$close", + "HIGH0": "$high/$close", + "LOW0": "$low/$close", + "VWAP0": "$vwap/$close", + "ROC5": "Ref($close, 5)/$close", + "ROC10": "Ref($close, 10)/$close", + "ROC20": "Ref($close, 20)/$close", + "ROC30": "Ref($close, 30)/$close", + "ROC60": "Ref($close, 60)/$close", + "MA5": "Mean($close, 5)/$close", + "MA10": "Mean($close, 10)/$close", + "MA20": "Mean($close, 20)/$close", + "MA30": "Mean($close, 30)/$close", + "MA60": "Mean($close, 60)/$close", + "STD5": "Std($close, 5)/$close", + "STD10": "Std($close, 10)/$close", + "STD20": "Std($close, 20)/$close", + "STD30": "Std($close, 30)/$close", + "STD60": "Std($close, 60)/$close", + "BETA5": "Slope($close, 5)/$close", + "BETA10": "Slope($close, 10)/$close", + "BETA20": "Slope($close, 20)/$close", + "BETA30": "Slope($close, 30)/$close", + "BETA60": "Slope($close, 60)/$close", + "RSQR5": "Rsquare($close, 5)", + "RSQR10": "Rsquare($close, 10)", + "RSQR20": "Rsquare($close, 20)", + "RSQR30": "Rsquare($close, 30)", + "RSQR60": "Rsquare($close, 60)", + "RESI5": "Resi($close, 5)/$close", + "RESI10": "Resi($close, 10)/$close", + "RESI20": "Resi($close, 20)/$close", + "RESI30": "Resi($close, 30)/$close", + "RESI60": "Resi($close, 60)/$close", + "MAX5": "Max($high, 5)/$close", + "MAX10": "Max($high, 10)/$close", + "MAX20": "Max($high, 20)/$close", + "MAX30": "Max($high, 30)/$close", + "MAX60": "Max($high, 60)/$close", + "MIN5": "Min($low, 5)/$close", + "MIN10": "Min($low, 10)/$close", + "MIN20": "Min($low, 20)/$close", + "MIN30": "Min($low, 30)/$close", + "MIN60": "Min($low, 60)/$close", + "QTLU5": "Quantile($close, 5, 0.8)/$close", + "QTLU10": "Quantile($close, 10, 0.8)/$close", + "QTLU20": "Quantile($close, 20, 0.8)/$close", + "QTLU30": "Quantile($close, 30, 0.8)/$close", + "QTLU60": "Quantile($close, 60, 0.8)/$close", + "QTLD5": "Quantile($close, 5, 0.2)/$close", + "QTLD10": "Quantile($close, 10, 0.2)/$close", + "QTLD20": "Quantile($close, 20, 0.2)/$close", + "QTLD30": "Quantile($close, 30, 0.2)/$close", + "QTLD60": "Quantile($close, 60, 0.2)/$close", + "RANK5": "Rank($close, 5)", + "RANK10": "Rank($close, 10)", + "RANK20": "Rank($close, 20)", + "RANK30": "Rank($close, 30)", + "RANK60": "Rank($close, 60)", + "RSV5": "($close-Min($low, 5))/(Max($high, 5)-Min($low, 5)+1e-12)", + "RSV10": "($close-Min($low, 10))/(Max($high, 10)-Min($low, 10)+1e-12)", + "RSV20": "($close-Min($low, 20))/(Max($high, 20)-Min($low, 20)+1e-12)", + "RSV30": "($close-Min($low, 30))/(Max($high, 30)-Min($low, 30)+1e-12)", + "RSV60": "($close-Min($low, 60))/(Max($high, 60)-Min($low, 60)+1e-12)", + "IMAX5": "IdxMax($high, 5)/5", + "IMAX10": "IdxMax($high, 10)/10", + "IMAX20": "IdxMax($high, 20)/20", + "IMAX30": "IdxMax($high, 30)/30", + "IMAX60": "IdxMax($high, 60)/60", + "IMIN5": "IdxMin($low, 5)/5", + "IMIN10": "IdxMin($low, 10)/10", + "IMIN20": "IdxMin($low, 20)/20", + "IMIN30": "IdxMin($low, 30)/30", + "IMIN60": "IdxMin($low, 60)/60", + "IMXD5": "(IdxMax($high, 5)-IdxMin($low, 5))/5", + "IMXD10": "(IdxMax($high, 10)-IdxMin($low, 10))/10", + "IMXD20": "(IdxMax($high, 20)-IdxMin($low, 20))/20", + "IMXD30": "(IdxMax($high, 30)-IdxMin($low, 30))/30", + "IMXD60": "(IdxMax($high, 60)-IdxMin($low, 60))/60", + "CORR5": "Corr($close, Log($volume+1), 5)", + "CORR10": "Corr($close, Log($volume+1), 10)", + "CORR20": "Corr($close, Log($volume+1), 20)", + "CORR30": "Corr($close, Log($volume+1), 30)", + "CORR60": "Corr($close, Log($volume+1), 60)", + "CORD5": "Corr($close/Ref($close,1), Log($volume/Ref($volume, 1)+1), 5)", + "CORD10": "Corr($close/Ref($close,1), Log($volume/Ref($volume, 1)+1), 10)", + "CORD20": "Corr($close/Ref($close,1), Log($volume/Ref($volume, 1)+1), 20)", + "CORD30": "Corr($close/Ref($close,1), Log($volume/Ref($volume, 1)+1), 30)", + "CORD60": "Corr($close/Ref($close,1), Log($volume/Ref($volume, 1)+1), 60)", + "CNTP5": "Mean($close>Ref($close, 1), 5)", + "CNTP10": "Mean($close>Ref($close, 1), 10)", + "CNTP20": "Mean($close>Ref($close, 1), 20)", + "CNTP30": "Mean($close>Ref($close, 1), 30)", + "CNTP60": "Mean($close>Ref($close, 1), 60)", + "CNTN5": "Mean($closeRef($close, 1), 5)-Mean($closeRef($close, 1), 10)-Mean($closeRef($close, 1), 20)-Mean($closeRef($close, 1), 30)-Mean($closeRef($close, 1), 60)-Mean($close = [ + { + path: '/', + name: 'Home', + component: () => import('../views/Home.vue'), + meta: { + keepAlive: true, //此页面需要缓存 + requiresFrontEndAuth: true, + footerBg: "#F6FAFF" + }, + }, + { + path: '/Playground', + name: 'Playground', + component: () => import('../views/Playground.vue'), + meta: { + keepAlive: false, //此页面需要缓存 + requiresFrontEndAuth: true, + footerBg: "#fff" + }, + }, + { + path: '/PlaygroundPage', + name: 'PlaygroundPage', + component: () => import('../views/PlaygroundPage.vue'), + meta: { + keepAlive: false, //此页面需要缓存 + requiresFrontEndAuth: true, + footerBg: "#fff" + }, + } + // { + // path: '/Login', + // name: 'Login', + // component: () => import('../views/Login.vue'), + // meta: { + // keepAlive: false, //此页面需要缓存 + // requiresFrontEndAuth: false + // }, + // } +] + +const router = createRouter({ + history: createWebHashHistory(), + routes +}) + +// 前端添加密码,防止release流程未走完,外部人员访问 +// router.beforeEach((to, from, next) => { +// console.log(from) +// if (!!to.meta && to.meta.requiresFrontEndAuth === false) { +// //这里判断用户是否登录,验证本地存储是否有token +// next(); +// return; +// } +// if (!sessionStorage.getItem("token")) { // 判断当前的token是否存在 +// next({ +// name: 'Login', +// query: { redirect: to.fullPath } +// }) +// } else { +// next(); +// } +// }) + +export default router \ No newline at end of file diff --git a/web/src/shims-vue.d.ts b/web/src/shims-vue.d.ts new file mode 100644 index 00000000..3804a43e --- /dev/null +++ b/web/src/shims-vue.d.ts @@ -0,0 +1,6 @@ +/* eslint-disable */ +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} diff --git a/web/src/style.css b/web/src/style.css new file mode 100644 index 00000000..5a10fa0e --- /dev/null +++ b/web/src/style.css @@ -0,0 +1,139 @@ +:root { + font-family: "Segoe UI"; + line-height: 1.5; + font-weight: 400; + --text-color: #2B2B2B; + --nav-hover-color: #9E9E9E; + --nav-default-color: #C3CEDC; + --sub-text-color: #3B3B3B; + --intro-text-color: #626C80; + --no-active-text-color: #626C80; + --footer-text-color: rgba(37, 37, 37, 0.78); + --btn-color: #353535; + --text-white-color: #fff; + --bg-white: #FFFFFF; + --bg-grey: #F6FAFF; + --bg-white-blue-color: #f8f9ff; + --bg-black: #2B2B2B; + --border-color: #2B2B2B; + --step-default-color: #C5D2E6; + --step-current-color: #4361EE; + --step-active-color: #626C80; + --blue-border-color: #4895EF; + --card-border-color: #2667FF; + --card-bg-hover-color: #EEEFFF; + --wg-shadow-color: #EDF0FF; + --border-width: 1px; +} + +html { + font-size: 16px; +} + +body { + width: 100%; + height: 100%; + margin: 0; + min-width: 320px; + min-height: 100vh; + background: var(--bg-white); + overflow: hidden; +} + +.component { + flex: 1; + min-height: 0; + overflow: auto; +} + +button:focus, +button:focus-visible { + outline: none; +} + +.card { + padding: 2em; +} + +#app { + width: 100%; + height: 100%; + margin: 0 auto; + font-family: Segoe UI; +} + +.container { + width: 100%; + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; + box-sizing: border-box; +} + +.gradient-border { + --border-width: 2px; + --border-radius: 11px; + position: relative; + background: #fff; + border-radius: calc(var(--border-radius) - 2px); + &::after { + position: absolute; + content: ""; + top: calc(-1 * var(--border-width)); + left: calc(-1 * var(--border-width)); + z-index: -1; + width: calc(100% + var(--border-width) * 2); + height: calc(100% + var(--border-width) * 2); + background: linear-gradient( to bottom, #2768FF, #9D42FF); + background-position: 0 50%; + border-radius: var(--border-radius); + box-shadow: 8px 11px 30px 0px var(--wg-shadow-color); + } +} + +.gradient-big-border { + --border-width: 2px; + --border-radius: 20px; + position: relative; + background: #fff; + border-radius: calc(var(--border-radius) - 2px); + &::after { + position: absolute; + content: ""; + top: calc(-1 * var(--border-width)); + left: calc(-1 * var(--border-width)); + z-index: -1; + width: calc(100% + var(--border-width) * 2); + height: calc(100% + var(--border-width) * 2); + background: linear-gradient( to bottom, #2768FF, #9D42FF); + background-position: 0 50%; + border-radius: var(--border-radius); + box-shadow: 8px 11px 30px 0px var(--wg-shadow-color); + } +} + +.el-popper__arrow, +.el-popper__arrow:before { + width: 15px !important; + height: 15px !important; +} + +.el-popper[data-popper-placement^=bottom]>.el-popper__arrow { + top: -8px !important; +} + +.el-popper[data-popper-placement^=left]>.el-popper__arrow { + right: -8px !important; +} + +.el-popper { + border-radius: 11px !important; +} +.markdown-body li { + list-style: unset; + } + + +/* .process-popper { + border-radius: 4px !important; +} */ \ No newline at end of file diff --git a/web/src/utils/api.js b/web/src/utils/api.js new file mode 100644 index 00000000..51c00943 --- /dev/null +++ b/web/src/utils/api.js @@ -0,0 +1,57 @@ +import request from './request'; + +export const url = typeof window !== 'undefined' ? `${window.location.origin}/` : '/'; + +export function uploadFile(data, config = {}) { + return request({ + url: url + "upload", + method: 'post', + headers: { + 'Content-Type': 'multipart/form-data', + }, + // onUploadProgress: progressEvent => { + // // this.uploadPercentage = parseInt(Math.round((progressEvent.loaded / progressEvent.total) * 100)); + // console.log(progressEvent) + // }, + data: data, + ...config + }) +} + +export function trace(data) { + return request({ + url: url + "trace", + method: 'post', + headers: { + 'Content-Type': 'application/json' + }, + data: data + }) +} + +export function control(data) { + return request({ + url: url + "control", + method: 'post', + headers: { + 'Content-Type': 'application/json' + }, + data: data + }) +} + +export function submitUserInteraction(data) { + return request({ + url: url + "user_interaction/submit", + method: 'post', + headers: { + 'Content-Type': 'application/json' + }, + data: data + }) +} + +export function getStdoutDownloadUrl(traceId) { + const query = new URLSearchParams({ id: traceId }); + return url + "stdout?" + query.toString(); +} \ No newline at end of file diff --git a/web/src/utils/crypto.js b/web/src/utils/crypto.js new file mode 100644 index 00000000..0011677a --- /dev/null +++ b/web/src/utils/crypto.js @@ -0,0 +1,29 @@ +//crypto.js文件内容 +import CryptoJS from 'crypto-js' +export default { // 加密 + /** + * @description: 加密 + * @param {*} word + * @param {*} keyStr + */ + set(word, keyStr) { + keyStr = keyStr || 'abcdef0123456789' // 16位的密钥,自己定义,和下面的密钥要相同 + var srcs = CryptoJS.enc.Utf8.parse(word) // 字符串到数组转换,解析明文 + var key = CryptoJS.enc.Utf8.parse(keyStr) // 字符串到数组转换,解析秘钥 + // mode:加密方式;padding:填充方式;iv便宜向量(可选) + var encrypted = CryptoJS.AES.encrypt(srcs, key, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 }) + return encrypted.toString() // 加密后的结果是对象,要转换为文本 + }, + + /** + * @description: 解密 + * @param {*} word + * @param {*} keyStr + */ + get(word, keyStr) { + keyStr = keyStr || 'abcdef0123456789' + var key = CryptoJS.enc.Utf8.parse(keyStr) // 字符串到数组转换 + var decrypt = CryptoJS.AES.decrypt(word, key, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 }) + return CryptoJS.enc.Utf8.stringify(decrypt).toString() // 数组到字符串转换 + } +} \ No newline at end of file diff --git a/web/src/utils/getAssets.ts b/web/src/utils/getAssets.ts new file mode 100644 index 00000000..d3340d47 --- /dev/null +++ b/web/src/utils/getAssets.ts @@ -0,0 +1,5 @@ +// 获取assets静态资源 +const getAssetsFile = (url: string) => { + return new URL(`../assets/images/${url}`, import.meta.url).href +} +export default getAssetsFile; \ No newline at end of file diff --git a/web/src/utils/request.js b/web/src/utils/request.js new file mode 100644 index 00000000..7f42f4e3 --- /dev/null +++ b/web/src/utils/request.js @@ -0,0 +1,37 @@ +import axios from 'axios' + +axios.defaults.headers.post['Content-Type'] = 'application/json' + +const service = axios.create({ + baseURL: '' +}) +service.defaults.timeout = 5 * 60 * 1000; + +// request拦截器 +service.interceptors.request.use( + config => { + if (config.data) {} + if (config.params) { + // console.log('request: ', config.params) + } + return config + }, + error => { + console.log('error-request: ', error) + return error + } +) + +// respone拦截器 +service.interceptors.response.use( + response => { + return response.data + }, + error => { + console.log('error-response: ', error) + console.log('error-response: ', error.response) + return error.response + } +) + +export default service \ No newline at end of file diff --git a/web/src/utils/snap.svg-min.js b/web/src/utils/snap.svg-min.js new file mode 100644 index 00000000..df28c464 --- /dev/null +++ b/web/src/utils/snap.svg-min.js @@ -0,0 +1,21 @@ +// Snap.svg 0.5.0 +// +// Copyright (c) 2013 – 2017 Adobe Systems Incorporated. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// build: 2017-02-06 + +!function(a){var b,c,d="0.5.0",e="hasOwnProperty",f=/[\.\/]/,g=/\s*,\s*/,h="*",i=function(a,b){return a-b},j={n:{}},k=function(){for(var a=0,b=this.length;b>a;a++)if("undefined"!=typeof this[a])return this[a]},l=function(){for(var a=this.length;--a;)if("undefined"!=typeof this[a])return this[a]},m=Object.prototype.toString,n=String,o=Array.isArray||function(a){return a instanceof Array||"[object Array]"==m.call(a)};eve=function(a,d){var e,f=c,g=Array.prototype.slice.call(arguments,2),h=eve.listeners(a),j=0,m=[],n={},o=[],p=b;o.firstDefined=k,o.lastDefined=l,b=a,c=0;for(var q=0,r=h.length;r>q;q++)"zIndex"in h[q]&&(m.push(h[q].zIndex),h[q].zIndex<0&&(n[h[q].zIndex]=h[q]));for(m.sort(i);m[j]<0;)if(e=n[m[j++]],o.push(e.apply(d,g)),c)return c=f,o;for(q=0;r>q;q++)if(e=h[q],"zIndex"in e)if(e.zIndex==m[j]){if(o.push(e.apply(d,g)),c)break;do if(j++,e=n[m[j]],e&&o.push(e.apply(d,g)),c)break;while(e)}else n[e.zIndex]=e;else if(o.push(e.apply(d,g)),c)break;return c=f,b=p,o},eve._events=j,eve.listeners=function(a){var b,c,d,e,g,i,k,l,m=o(a)?a:a.split(f),n=j,p=[n],q=[];for(e=0,g=m.length;g>e;e++){for(l=[],i=0,k=p.length;k>i;i++)for(n=p[i].n,c=[n[m[e]],n[h]],d=2;d--;)b=c[d],b&&(l.push(b),q=q.concat(b.f||[]));p=l}return q},eve.separator=function(a){a?(a=n(a).replace(/(?=[\.\^\]\[\-])/g,"\\"),a="["+a+"]",f=new RegExp(a)):f=/[\.\/]/},eve.on=function(a,b){if("function"!=typeof b)return function(){};for(var c=o(a)?o(a[0])?a:[a]:n(a).split(g),d=0,e=c.length;e>d;d++)!function(a){for(var c,d=o(a)?a:n(a).split(f),e=j,g=0,h=d.length;h>g;g++)e=e.n,e=e.hasOwnProperty(d[g])&&e[d[g]]||(e[d[g]]={n:{}});for(e.f=e.f||[],g=0,h=e.f.length;h>g;g++)if(e.f[g]==b){c=!0;break}!c&&e.f.push(b)}(c[d]);return function(a){+a==+a&&(b.zIndex=+a)}},eve.f=function(a){var b=[].slice.call(arguments,1);return function(){eve.apply(null,[a,null].concat(b).concat([].slice.call(arguments,0)))}},eve.stop=function(){c=1},eve.nt=function(a){var c=o(b)?b.join("."):b;return a?new RegExp("(?:\\.|\\/|^)"+a+"(?:\\.|\\/|$)").test(c):c},eve.nts=function(){return o(b)?b:b.split(f)},eve.off=eve.unbind=function(a,b){if(!a)return void(eve._events=j={n:{}});var c=o(a)?o(a[0])?a:[a]:n(a).split(g);if(c.length>1)for(var d=0,i=c.length;i>d;d++)eve.off(c[d],b);else{c=o(a)?a:n(a).split(f);var k,l,m,d,i,p,q,r=[j],s=[];for(d=0,i=c.length;i>d;d++)for(p=0;pd;d++)for(k=r[d];k.n;){if(b){if(k.f){for(p=0,q=k.f.length;q>p;p++)if(k.f[p]==b){k.f.splice(p,1);break}!k.f.length&&delete k.f}for(l in k.n)if(k.n[e](l)&&k.n[l].f){var t=k.n[l].f;for(p=0,q=t.length;q>p;p++)if(t[p]==b){t.splice(p,1);break}!t.length&&delete k.n[l].f}}else{delete k.f;for(l in k.n)k.n[e](l)&&k.n[l].f&&delete k.n[l].f}k=k.n}a:for(d=0,i=s.length;i>d;d++){k=s[d];for(l in k.n[k.name].f)continue a;for(l in k.n[k.name].n)continue a;delete k.n[k.name]}}},eve.once=function(a,b){var c=function(){return eve.off(a,c),b.apply(this,arguments)};return eve.on(a,c)},eve.version=d,eve.toString=function(){return"You are running Eve "+d},"undefined"!=typeof module&&module.exports?module.exports=eve:"function"==typeof define&&define.amd?define("eve",[],function(){return eve}):a.eve=eve}(this),function(a,b){if("function"==typeof define&&define.amd)define(["eve"],function(c){return b(a,c)});else if("undefined"!=typeof exports){var c=require("eve");module.exports=b(a,c)}else b(a,a.eve)}(window||this,function(a,b){var c=function(b){var c,d={},e=a.requestAnimationFrame||a.webkitRequestAnimationFrame||a.mozRequestAnimationFrame||a.oRequestAnimationFrame||a.msRequestAnimationFrame||function(a){return setTimeout(a,16,(new Date).getTime()),!0},f=Array.isArray||function(a){return a instanceof Array||"[object Array]"==Object.prototype.toString.call(a)},g=0,h="M"+(+new Date).toString(36),i=function(){return h+(g++).toString(36)},j=Date.now||function(){return+new Date},k=function(a){var b=this;if(null==a)return b.s;var c=b.s-a;b.b+=b.dur*c,b.B+=b.dur*c,b.s=a},l=function(a){var b=this;return null==a?b.spd:void(b.spd=a)},m=function(a){var b=this;return null==a?b.dur:(b.s=b.s*a/b.dur,void(b.dur=a))},n=function(){var a=this;delete d[a.id],a.update(),b("mina.stop."+a.id,a)},o=function(){var a=this;a.pdif||(delete d[a.id],a.update(),a.pdif=a.get()-a.b)},p=function(){var a=this;a.pdif&&(a.b=a.get()-a.pdif,delete a.pdif,d[a.id]=a,r())},q=function(){var a,b=this;if(f(b.start)){a=[];for(var c=0,d=b.start.length;d>c;c++)a[c]=+b.start[c]+(b.end[c]-b.start[c])*b.easing(b.s)}else a=+b.start+(b.end-b.start)*b.easing(b.s);b.set(a)},r=function(a){if(!a)return void(c||(c=e(r)));var f=0;for(var g in d)if(d.hasOwnProperty(g)){var h=d[g],i=h.get();f++,h.s=(i-h.b)/(h.dur/h.spd),h.s>=1&&(delete d[g],h.s=1,f--,function(a){setTimeout(function(){b("mina.finish."+a.id,a)})}(h)),h.update()}c=f?e(r):!1},s=function(a,b,c,e,f,g,h){var j={id:i(),start:a,end:b,b:c,s:0,dur:e-c,spd:1,get:f,set:g,easing:h||s.linear,status:k,speed:l,duration:m,stop:n,pause:o,resume:p,update:q};d[j.id]=j;var t,u=0;for(t in d)if(d.hasOwnProperty(t)&&(u++,2==u))break;return 1==u&&r(),j};return s.time=j,s.getById=function(a){return d[a]||null},s.linear=function(a){return a},s.easeout=function(a){return Math.pow(a,1.7)},s.easein=function(a){return Math.pow(a,.48)},s.easeinout=function(a){if(1==a)return 1;if(0==a)return 0;var b=.48-a/1.04,c=Math.sqrt(.1734+b*b),d=c-b,e=Math.pow(Math.abs(d),1/3)*(0>d?-1:1),f=-c-b,g=Math.pow(Math.abs(f),1/3)*(0>f?-1:1),h=e+g+.5;return 3*(1-h)*h*h+h*h*h},s.backin=function(a){if(1==a)return 1;var b=1.70158;return a*a*((b+1)*a-b)},s.backout=function(a){if(0==a)return 0;a-=1;var b=1.70158;return a*a*((b+1)*a+b)+1},s.elastic=function(a){return a==!!a?a:Math.pow(2,-10*a)*Math.sin((a-.075)*(2*Math.PI)/.3)+1},s.bounce=function(a){var b,c=7.5625,d=2.75;return 1/d>a?b=c*a*a:2/d>a?(a-=1.5/d,b=c*a*a+.75):2.5/d>a?(a-=2.25/d,b=c*a*a+.9375):(a-=2.625/d,b=c*a*a+.984375),b},a.mina=s,s}("undefined"==typeof b?function(){}:b),d=function(a){function c(a,b){if(a){if(a.nodeType)return w(a);if(e(a,"array")&&c.set)return c.set.apply(c,a);if(a instanceof s)return a;if(null==b)return a=y.doc.querySelector(String(a)),w(a)}return a=null==a?"100%":a,b=null==b?"100%":b,new v(a,b)}function d(a,b){if(b){if("#text"==a&&(a=y.doc.createTextNode(b.text||b["#text"]||"")),"#comment"==a&&(a=y.doc.createComment(b.text||b["#text"]||"")),"string"==typeof a&&(a=d(a)),"string"==typeof b)return 1==a.nodeType?"xlink:"==b.substring(0,6)?a.getAttributeNS(T,b.substring(6)):"xml:"==b.substring(0,4)?a.getAttributeNS(U,b.substring(4)):a.getAttribute(b):"text"==b?a.nodeValue:null;if(1==a.nodeType){for(var c in b)if(b[z](c)){var e=A(b[c]);e?"xlink:"==c.substring(0,6)?a.setAttributeNS(T,c.substring(6),e):"xml:"==c.substring(0,4)?a.setAttributeNS(U,c.substring(4),e):a.setAttribute(c,e):a.removeAttribute(c)}}else"text"in b&&(a.nodeValue=b.text)}else a=y.doc.createElementNS(U,a);return a}function e(a,b){return b=A.prototype.toLowerCase.call(b),"finite"==b?isFinite(a):"array"==b&&(a instanceof Array||Array.isArray&&Array.isArray(a))?!0:"null"==b&&null===a||b==typeof a&&null!==a||"object"==b&&a===Object(a)||J.call(a).slice(8,-1).toLowerCase()==b}function f(a){if("function"==typeof a||Object(a)!==a)return a;var b=new a.constructor;for(var c in a)a[z](c)&&(b[c]=f(a[c]));return b}function h(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return a.push(a.splice(c,1)[0])}function i(a,b,c){function d(){var e=Array.prototype.slice.call(arguments,0),f=e.join("␀"),g=d.cache=d.cache||{},i=d.count=d.count||[];return g[z](f)?(h(i,f),c?c(g[f]):g[f]):(i.length>=1e3&&delete g[i.shift()],i.push(f),g[f]=a.apply(b,e),c?c(g[f]):g[f])}return d}function j(a,b,c,d,e,f){if(null==e){var g=a-c,h=b-d;return g||h?(180+180*D.atan2(-h,-g)/H+360)%360:0}return j(a,b,e,f)-j(c,d,e,f)}function k(a){return a%360*H/180}function l(a){return 180*a/H%360}function m(a){var b=[];return a=a.replace(/(?:^|\s)(\w+)\(([^)]+)\)/g,function(a,c,d){return d=d.split(/\s*,\s*|\s+/),"rotate"==c&&1==d.length&&d.push(0,0),"scale"==c&&(d.length>2?d=d.slice(0,2):2==d.length&&d.push(0,0),1==d.length&&d.push(d[0],0,0)),"skewX"==c?b.push(["m",1,0,D.tan(k(d[0])),1,0,0]):"skewY"==c?b.push(["m",1,D.tan(k(d[0])),0,1,0,0]):b.push([c.charAt(0)].concat(d)),a}),b}function n(a,b){var d=aa(a),e=new c.Matrix;if(d)for(var f=0,g=d.length;g>f;f++){var h,i,j,k,l,m=d[f],n=m.length,o=A(m[0]).toLowerCase(),p=m[0]!=o,q=p?e.invert():0;"t"==o&&2==n?e.translate(m[1],0):"t"==o&&3==n?p?(h=q.x(0,0),i=q.y(0,0),j=q.x(m[1],m[2]),k=q.y(m[1],m[2]),e.translate(j-h,k-i)):e.translate(m[1],m[2]):"r"==o?2==n?(l=l||b,e.rotate(m[1],l.x+l.width/2,l.y+l.height/2)):4==n&&(p?(j=q.x(m[2],m[3]),k=q.y(m[2],m[3]),e.rotate(m[1],j,k)):e.rotate(m[1],m[2],m[3])):"s"==o?2==n||3==n?(l=l||b,e.scale(m[1],m[n-1],l.x+l.width/2,l.y+l.height/2)):4==n?p?(j=q.x(m[2],m[3]),k=q.y(m[2],m[3]),e.scale(m[1],m[1],j,k)):e.scale(m[1],m[1],m[2],m[3]):5==n&&(p?(j=q.x(m[3],m[4]),k=q.y(m[3],m[4]),e.scale(m[1],m[2],j,k)):e.scale(m[1],m[2],m[3],m[4])):"m"==o&&7==n&&e.add(m[1],m[2],m[3],m[4],m[5],m[6])}return e}function o(a){var b=a.node.ownerSVGElement&&w(a.node.ownerSVGElement)||a.node.parentNode&&w(a.node.parentNode)||c.select("svg")||c(0,0),d=b.select("defs"),e=null==d?!1:d.node;return e||(e=u("defs",b.node).node),e}function p(a){return a.node.ownerSVGElement&&w(a.node.ownerSVGElement)||c.select("svg")}function q(a,b,c){function e(a){if(null==a)return I;if(a==+a)return a;d(j,{width:a});try{return j.getBBox().width}catch(b){return 0}}function f(a){if(null==a)return I;if(a==+a)return a;d(j,{height:a});try{return j.getBBox().height}catch(b){return 0}}function g(d,e){null==b?i[d]=e(a.attr(d)||0):d==b&&(i=e(null==c?a.attr(d)||0:c))}var h=p(a).node,i={},j=h.querySelector(".svg---mgr");switch(j||(j=d("rect"),d(j,{x:-9e9,y:-9e9,width:10,height:10,"class":"svg---mgr",fill:"none"}),h.appendChild(j)),a.type){case"rect":g("rx",e),g("ry",f);case"image":g("width",e),g("height",f);case"text":g("x",e),g("y",f);break;case"circle":g("cx",e),g("cy",f),g("r",e);break;case"ellipse":g("cx",e),g("cy",f),g("rx",e),g("ry",f);break;case"line":g("x1",e),g("x2",e),g("y1",f),g("y2",f);break;case"marker":g("refX",e),g("markerWidth",e),g("refY",f),g("markerHeight",f);break;case"radialGradient":g("fx",e),g("fy",f);break;case"tspan":g("dx",e),g("dy",f);break;default:g(b,e)}return h.removeChild(j),i}function r(a){e(a,"array")||(a=Array.prototype.slice.call(arguments,0));for(var b=0,c=0,d=this.node;this[b];)delete this[b++];for(b=0;bc;c++){var e={type:a[c].type,attr:a[c].attr()},f=a[c].children();b.push(e),f.length&&x(f,e.childNodes=[])}}c.version="0.5.1",c.toString=function(){return"Snap v"+this.version},c._={};var y={win:a.window,doc:a.window.document};c._.glob=y;var z="hasOwnProperty",A=String,B=parseFloat,C=parseInt,D=Math,E=D.max,F=D.min,G=D.abs,H=(D.pow,D.PI),I=(D.round,""),J=Object.prototype.toString,K=/^\s*((#[a-f\d]{6})|(#[a-f\d]{3})|rgba?\(\s*([\d\.]+%?\s*,\s*[\d\.]+%?\s*,\s*[\d\.]+%?(?:\s*,\s*[\d\.]+%?)?)\s*\)|hsba?\(\s*([\d\.]+(?:deg|\xb0|%)?\s*,\s*[\d\.]+%?\s*,\s*[\d\.]+(?:%?\s*,\s*[\d\.]+)?%?)\s*\)|hsla?\(\s*([\d\.]+(?:deg|\xb0|%)?\s*,\s*[\d\.]+%?\s*,\s*[\d\.]+(?:%?\s*,\s*[\d\.]+)?%?)\s*\))\s*$/i,L=(c._.separator=/[,\s]+/,/[\s]*,[\s]*/),M={hs:1,rg:1},N=/([a-z])[\s,]*((-?\d*\.?\d*(?:e[\-+]?\d+)?[\s]*,?[\s]*)+)/gi,O=/([rstm])[\s,]*((-?\d*\.?\d*(?:e[\-+]?\d+)?[\s]*,?[\s]*)+)/gi,P=/(-?\d*\.?\d*(?:e[\-+]?\d+)?)[\s]*,?[\s]*/gi,Q=0,R="S"+(+new Date).toString(36),S=function(a){return(a&&a.type?a.type:I)+R+(Q++).toString(36)},T="http://www.w3.org/1999/xlink",U="http://www.w3.org/2000/svg",V={};c.url=function(a){return"url('#"+a+"')"};c._.$=d,c._.id=S,c.format=function(){var a=/\{([^\}]+)\}/g,b=/(?:(?:^|\.)(.+?)(?=\[|\.|$|\()|\[('|")(.+?)\2\])(\(\))?/g,c=function(a,c,d){var e=d;return c.replace(b,function(a,b,c,d,f){b=b||d,e&&(b in e&&(e=e[b]),"function"==typeof e&&f&&(e=e()))}),e=(null==e||e==d?a:e)+""};return function(b,d){return A(b).replace(a,function(a,b){return c(a,b,d)})}}(),c._.clone=f,c._.cacher=i,c.rad=k,c.deg=l,c.sin=function(a){return D.sin(c.rad(a))},c.tan=function(a){return D.tan(c.rad(a))},c.cos=function(a){return D.cos(c.rad(a))},c.asin=function(a){return c.deg(D.asin(a))},c.acos=function(a){return c.deg(D.acos(a))},c.atan=function(a){return c.deg(D.atan(a))},c.atan2=function(a){return c.deg(D.atan2(a))},c.angle=j,c.len=function(a,b,d,e){return Math.sqrt(c.len2(a,b,d,e))},c.len2=function(a,b,c,d){return(a-c)*(a-c)+(b-d)*(b-d)},c.closestPoint=function(a,b,c){function d(a){var d=a.x-b,e=a.y-c;return d*d+e*e}for(var e,f,g,h,i=a.node,j=i.getTotalLength(),k=j/i.pathSegList.numberOfItems*.125,l=1/0,m=0;j>=m;m+=k)(h=d(g=i.getPointAtLength(m))).5;){var n,o,p,q,r,s;(p=f-k)>=0&&(r=d(n=i.getPointAtLength(p)))f)return b-f;if(f>a-c)return b-f+a}return b},c.getRGB=i(function(a){if(!a||(a=A(a)).indexOf("-")+1)return{r:-1,g:-1,b:-1,hex:"none",error:1,toString:Z};if("none"==a)return{r:-1,g:-1,b:-1,hex:"none",toString:Z};if(!(M[z](a.toLowerCase().substring(0,2))||"#"==a.charAt())&&(a=W(a)),!a)return{r:-1,g:-1,b:-1,hex:"none",error:1,toString:Z};var b,d,f,g,h,i,j=a.match(K);return j?(j[2]&&(f=C(j[2].substring(5),16),d=C(j[2].substring(3,5),16),b=C(j[2].substring(1,3),16)),j[3]&&(f=C((h=j[3].charAt(3))+h,16),d=C((h=j[3].charAt(2))+h,16),b=C((h=j[3].charAt(1))+h,16)),j[4]&&(i=j[4].split(L),b=B(i[0]),"%"==i[0].slice(-1)&&(b*=2.55),d=B(i[1]),"%"==i[1].slice(-1)&&(d*=2.55),f=B(i[2]),"%"==i[2].slice(-1)&&(f*=2.55),"rgba"==j[1].toLowerCase().slice(0,4)&&(g=B(i[3])),i[3]&&"%"==i[3].slice(-1)&&(g/=100)),j[5]?(i=j[5].split(L),b=B(i[0]),"%"==i[0].slice(-1)&&(b/=100),d=B(i[1]),"%"==i[1].slice(-1)&&(d/=100),f=B(i[2]),"%"==i[2].slice(-1)&&(f/=100),("deg"==i[0].slice(-3)||"°"==i[0].slice(-1))&&(b/=360),"hsba"==j[1].toLowerCase().slice(0,4)&&(g=B(i[3])),i[3]&&"%"==i[3].slice(-1)&&(g/=100),c.hsb2rgb(b,d,f,g)):j[6]?(i=j[6].split(L),b=B(i[0]),"%"==i[0].slice(-1)&&(b/=100),d=B(i[1]),"%"==i[1].slice(-1)&&(d/=100),f=B(i[2]),"%"==i[2].slice(-1)&&(f/=100),("deg"==i[0].slice(-3)||"°"==i[0].slice(-1))&&(b/=360),"hsla"==j[1].toLowerCase().slice(0,4)&&(g=B(i[3])),i[3]&&"%"==i[3].slice(-1)&&(g/=100),c.hsl2rgb(b,d,f,g)):(b=F(D.round(b),255),d=F(D.round(d),255),f=F(D.round(f),255),g=F(E(g,0),1),j={r:b,g:d,b:f,toString:Z},j.hex="#"+(16777216|f|d<<8|b<<16).toString(16).slice(1),j.opacity=e(g,"finite")?g:1,j)):{r:-1,g:-1,b:-1,hex:"none",error:1,toString:Z}},c),c.hsb=i(function(a,b,d){return c.hsb2rgb(a,b,d).hex}),c.hsl=i(function(a,b,d){return c.hsl2rgb(a,b,d).hex}),c.rgb=i(function(a,b,c,d){if(e(d,"finite")){var f=D.round;return"rgba("+[f(a),f(b),f(c),+d.toFixed(2)]+")"}return"#"+(16777216|c|b<<8|a<<16).toString(16).slice(1)});var W=function(a){var b=y.doc.getElementsByTagName("head")[0]||y.doc.getElementsByTagName("svg")[0],c="rgb(255, 0, 0)";return(W=i(function(a){if("red"==a.toLowerCase())return c;b.style.color=c,b.style.color=a;var d=y.doc.defaultView.getComputedStyle(b,I).getPropertyValue("color");return d==c?null:d}))(a)},X=function(){return"hsb("+[this.h,this.s,this.b]+")"},Y=function(){return"hsl("+[this.h,this.s,this.l]+")"},Z=function(){return 1==this.opacity||null==this.opacity?this.hex:"rgba("+[this.r,this.g,this.b,this.opacity]+")"},$=function(a,b,d){if(null==b&&e(a,"object")&&"r"in a&&"g"in a&&"b"in a&&(d=a.b,b=a.g,a=a.r),null==b&&e(a,string)){var f=c.getRGB(a);a=f.r,b=f.g,d=f.b}return(a>1||b>1||d>1)&&(a/=255,b/=255,d/=255),[a,b,d]},_=function(a,b,d,f){a=D.round(255*a),b=D.round(255*b),d=D.round(255*d);var g={r:a,g:b,b:d,opacity:e(f,"finite")?f:1,hex:c.rgb(a,b,d),toString:Z};return e(f,"finite")&&(g.opacity=f),g};c.color=function(a){var b;return e(a,"object")&&"h"in a&&"s"in a&&"b"in a?(b=c.hsb2rgb(a),a.r=b.r,a.g=b.g,a.b=b.b,a.opacity=1,a.hex=b.hex):e(a,"object")&&"h"in a&&"s"in a&&"l"in a?(b=c.hsl2rgb(a),a.r=b.r,a.g=b.g,a.b=b.b,a.opacity=1,a.hex=b.hex):(e(a,"string")&&(a=c.getRGB(a)),e(a,"object")&&"r"in a&&"g"in a&&"b"in a&&!("error"in a)?(b=c.rgb2hsl(a),a.h=b.h,a.s=b.s,a.l=b.l,b=c.rgb2hsb(a),a.v=b.b):(a={hex:"none"},a.r=a.g=a.b=a.h=a.s=a.v=a.l=-1,a.error=1)),a.toString=Z,a},c.hsb2rgb=function(a,b,c,d){e(a,"object")&&"h"in a&&"s"in a&&"b"in a&&(c=a.b,b=a.s,d=a.o,a=a.h),a*=360;var f,g,h,i,j;return a=a%360/60,j=c*b,i=j*(1-G(a%2-1)),f=g=h=c-j,a=~~a,f+=[j,i,0,0,i,j][a],g+=[i,j,j,i,0,0][a],h+=[0,0,i,j,j,i][a],_(f,g,h,d)},c.hsl2rgb=function(a,b,c,d){e(a,"object")&&"h"in a&&"s"in a&&"l"in a&&(c=a.l,b=a.s,a=a.h),(a>1||b>1||c>1)&&(a/=360,b/=100,c/=100),a*=360;var f,g,h,i,j;return a=a%360/60,j=2*b*(.5>c?c:1-c),i=j*(1-G(a%2-1)),f=g=h=c-j/2,a=~~a,f+=[j,i,0,0,i,j][a],g+=[i,j,j,i,0,0][a],h+=[0,0,i,j,j,i][a],_(f,g,h,d)},c.rgb2hsb=function(a,b,c){c=$(a,b,c),a=c[0],b=c[1],c=c[2];var d,e,f,g;return f=E(a,b,c),g=f-F(a,b,c),d=0==g?null:f==a?(b-c)/g:f==b?(c-a)/g+2:(a-b)/g+4,d=(d+360)%6*60/360,e=0==g?0:g/f,{h:d,s:e,b:f,toString:X}},c.rgb2hsl=function(a,b,c){c=$(a,b,c),a=c[0],b=c[1],c=c[2];var d,e,f,g,h,i;return g=E(a,b,c),h=F(a,b,c),i=g-h,d=0==i?null:g==a?(b-c)/i:g==b?(c-a)/i+2:(a-b)/i+4,d=(d+360)%6*60/360,f=(g+h)/2,e=0==i?0:.5>f?i/(2*f):i/(2-2*f),{h:d,s:e,l:f,toString:Y}},c.parsePathString=function(a){if(!a)return null;var b=c.path(a);if(b.arr)return c.path.clone(b.arr);var d={a:7,c:6,o:2,h:1,l:2,m:2,r:4,q:4,s:4,t:2,v:1,u:3,z:0},f=[];return e(a,"array")&&e(a[0],"array")&&(f=c.path.clone(a)),f.length||A(a).replace(N,function(a,b,c){var e=[],g=b.toLowerCase();if(c.replace(P,function(a,b){b&&e.push(+b)}),"m"==g&&e.length>2&&(f.push([b].concat(e.splice(0,2))),g="l",b="m"==b?"l":"L"),"o"==g&&1==e.length&&f.push([b,e[0]]),"r"==g)f.push([b].concat(e));else for(;e.length>=d[g]&&(f.push([b].concat(e.splice(0,d[g]))),d[g]););}),f.toString=c.path.toString,b.arr=c.path.clone(f),f};var aa=c.parseTransformString=function(a){if(!a)return null;var b=[];return e(a,"array")&&e(a[0],"array")&&(b=c.path.clone(a)),b.length||A(a).replace(O,function(a,c,d){var e=[];c.toLowerCase();d.replace(P,function(a,b){b&&e.push(+b)}),b.push([c].concat(e))}),b.toString=c.path.toString,b};c._.svgTransform2string=m,c._.rgTransform=/^[a-z][\s]*-?\.?\d/i,c._.transform2matrix=n,c._unit2px=q;y.doc.contains||y.doc.compareDocumentPosition?function(a,b){var c=9==a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a==d||!(!d||1!=d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)for(;b;)if(b=b.parentNode,b==a)return!0;return!1};c._.getSomeDefs=o,c._.getSomeSVG=p,c.select=function(a){return a=A(a).replace(/([^\\]):/g,"$1\\:"),w(y.doc.querySelector(a))},c.selectAll=function(a){for(var b=y.doc.querySelectorAll(a),d=(c.set||Array)(),e=0;ei;i++)h[g[i].nodeName]=g[i].nodeValue;return h}if(e(a,"string")){if(!(arguments.length>1))return b("snap.util.getattr."+a,d).firstDefined();var k={};k[a]=c,a=k}for(var l in a)a[z](l)&&b("snap.util.attr."+l,d,a[l]);return d},c.parse=function(a){var b=y.doc.createDocumentFragment(),c=!0,d=y.doc.createElement("div");if(a=A(a),a.match(/^\s*<\s*svg(?:\s|>)/)||(a=""+a+"",c=!1),d.innerHTML=a,a=d.getElementsByTagName("svg")[0])if(c)b=a;else for(;a.firstChild;)b.appendChild(a.firstChild);return new t(b)},c.fragment=function(){for(var a=Array.prototype.slice.call(arguments,0),b=y.doc.createDocumentFragment(),d=0,e=a.length;e>d;d++){var f=a[d];f.node&&f.node.nodeType&&b.appendChild(f.node),f.nodeType&&b.appendChild(f),"string"==typeof f&&b.appendChild(c.parse(f).node)}return new t(b)},c._.make=u,c._.wrap=w,v.prototype.el=function(a,b){var c=u(a,this.node);return b&&c.attr(b),c},s.prototype.children=function(){for(var a=[],b=this.node.childNodes,d=0,e=b.length;e>d;d++)a[d]=c(b[d]);return a},s.prototype.toJSON=function(){var a=[];return x([this],a),a[0]},b.on("snap.util.getattr",function(){var a=b.nt();a=a.substring(a.lastIndexOf(".")+1);var c=a.replace(/[A-Z]/g,function(a){return"-"+a.toLowerCase()});return ba[z](c)?this.node.ownerDocument.defaultView.getComputedStyle(this.node,null).getPropertyValue(c):d(this.node,a)});var ba={"alignment-baseline":0,"baseline-shift":0,clip:0,"clip-path":0,"clip-rule":0,color:0,"color-interpolation":0,"color-interpolation-filters":0,"color-profile":0,"color-rendering":0,cursor:0,direction:0,display:0,"dominant-baseline":0,"enable-background":0,fill:0,"fill-opacity":0,"fill-rule":0,filter:0,"flood-color":0,"flood-opacity":0,font:0,"font-family":0,"font-size":0,"font-size-adjust":0,"font-stretch":0,"font-style":0,"font-variant":0,"font-weight":0,"glyph-orientation-horizontal":0,"glyph-orientation-vertical":0,"image-rendering":0,kerning:0,"letter-spacing":0,"lighting-color":0,marker:0,"marker-end":0,"marker-mid":0,"marker-start":0,mask:0,opacity:0,overflow:0,"pointer-events":0,"shape-rendering":0,"stop-color":0,"stop-opacity":0,stroke:0,"stroke-dasharray":0,"stroke-dashoffset":0,"stroke-linecap":0,"stroke-linejoin":0,"stroke-miterlimit":0,"stroke-opacity":0,"stroke-width":0,"text-anchor":0,"text-decoration":0,"text-rendering":0,"unicode-bidi":0,visibility:0,"word-spacing":0,"writing-mode":0};b.on("snap.util.attr",function(a){var c=b.nt(),e={};c=c.substring(c.lastIndexOf(".")+1),e[c]=a;var f=c.replace(/-(\w)/gi,function(a,b){return b.toUpperCase()}),g=c.replace(/[A-Z]/g,function(a){return"-"+a.toLowerCase()});ba[z](g)?this.node.style[f]=null==a?I:a:d(this.node,e)}),function(a){}(v.prototype),c.ajax=function(a,c,d,f){var g=new XMLHttpRequest,h=S();if(g){if(e(c,"function"))f=d,d=c,c=null;else if(e(c,"object")){var i=[];for(var j in c)c.hasOwnProperty(j)&&i.push(encodeURIComponent(j)+"="+encodeURIComponent(c[j]));c=i.join("&")}return g.open(c?"POST":"GET",a,!0),c&&(g.setRequestHeader("X-Requested-With","XMLHttpRequest"),g.setRequestHeader("Content-type","application/x-www-form-urlencoded")),d&&(b.once("snap.ajax."+h+".0",d),b.once("snap.ajax."+h+".200",d),b.once("snap.ajax."+h+".304",d)),g.onreadystatechange=function(){4==g.readyState&&b("snap.ajax."+h+"."+g.status,f,g)},4==g.readyState?g:(g.send(c),g)}},c.load=function(a,b,d){c.ajax(a,function(a){var e=c.parse(a.responseText);d?b.call(d,e):b(e)})};var ca=function(a){var b=a.getBoundingClientRect(),c=a.ownerDocument,d=c.body,e=c.documentElement,f=e.clientTop||d.clientTop||0,h=e.clientLeft||d.clientLeft||0,i=b.top+(g.win.pageYOffset||e.scrollTop||d.scrollTop)-f,j=b.left+(g.win.pageXOffset||e.scrollLeft||d.scrollLeft)-h;return{y:i,x:j}};return c.getElementByPoint=function(a,b){var c=this,d=(c.canvas,y.doc.elementFromPoint(a,b));if(y.win.opera&&"svg"==d.tagName){var e=ca(d),f=d.createSVGRect();f.x=a-e.x,f.y=b-e.y,f.width=f.height=1;var g=d.getIntersectionList(f,null);g.length&&(d=g[g.length-1])}return d?w(d):null},c.plugin=function(a){a(c,s,v,y,t)},y.win.Snap=c,c}(a||this);return d.plugin(function(c,d,e,f,g){function h(a,b){if(null==b){var d=!0;if(b="linearGradient"==a.type||"radialGradient"==a.type?a.node.getAttribute("gradientTransform"):"pattern"==a.type?a.node.getAttribute("patternTransform"):a.node.getAttribute("transform"),!b)return new c.Matrix;b=c._.svgTransform2string(b)}else b=c._.rgTransform.test(b)?m(b).replace(/\.{3}|\u2026/g,a._.transform||""):c._.svgTransform2string(b),l(b,"array")&&(b=c.path?c.path.toString.call(b):m(b)),a._.transform=b;var e=c._.transform2matrix(b,a.getBBox(1));return d?e:void(a.matrix=e)}function i(a){function b(a,b){var d=o(a.node,b);d=d&&d.match(g),d=d&&d[2],d&&"#"==d.charAt()&&(d=d.substring(1),d&&(i[d]=(i[d]||[]).concat(function(d){var e={};e[b]=c.url(d),o(a.node,e)})))}function d(a){var b=o(a.node,"xlink:href");b&&"#"==b.charAt()&&(b=b.substring(1),b&&(i[b]=(i[b]||[]).concat(function(b){a.attr("xlink:href","#"+b)})))}for(var e,f=a.selectAll("*"),g=/^\s*url\(("|'|)(.*)\1\)\s*$/,h=[],i={},j=0,k=f.length;k>j;j++){e=f[j],b(e,"fill"),b(e,"stroke"),b(e,"filter"),b(e,"mask"),b(e,"clip-path"),d(e);var l=o(e.node,"id");l&&(o(e.node,{id:e.id}),h.push({old:l,id:e.id}))}for(j=0,k=h.length;k>j;j++){var m=i[h[j].old];if(m)for(var n=0,p=m.length;p>n;n++)m[n](h[j].id)}}function j(a){return function(){var b=a?"<"+this.type:"",c=this.node.attributes,d=this.node.childNodes;if(a)for(var e=0,f=c.length;f>e;e++)b+=" "+c[e].name+'="'+c[e].value.replace(/"/g,'\\"')+'"';if(d.length){for(a&&(b+=">"),e=0,f=d.length;f>e;e++)3==d[e].nodeType?b+=d[e].nodeValue:1==d[e].nodeType&&(b+=s(d[e]).toString());a&&(b+="")}else a&&(b+="/>");return b}}var k=d.prototype,l=c.is,m=String,n=c._unit2px,o=c._.$,p=c._.make,q=c._.getSomeDefs,r="hasOwnProperty",s=c._.wrap;k.getBBox=function(a){if("tspan"==this.type)return c._.box(this.node.getClientRects().item(0));if(!c.Matrix||!c.path)return this.node.getBBox();var b=this,d=new c.Matrix;if(b.removed)return c._.box();for(;"use"==b.type;)if(a||(d=d.add(b.transform().localMatrix.translate(b.attr("x")||0,b.attr("y")||0))),b.original)b=b.original;else{var e=b.attr("xlink:href");b=b.original=b.node.ownerDocument.getElementById(e.substring(e.indexOf("#")+1))}var f=b._,g=c.path.get[b.type]||c.path.get.deflt;try{return a?(f.bboxwt=g?c.path.getBBox(b.realPath=g(b)):c._.box(b.node.getBBox()),c._.box(f.bboxwt)):(b.realPath=g(b),b.matrix=b.transform().localMatrix,f.bbox=c.path.getBBox(c.path.map(b.realPath,d.add(b.matrix))),c._.box(f.bbox))}catch(h){return c._.box()}};var t=function(){return this.string};k.transform=function(a){var b=this._;if(null==a){for(var d,e=this,f=new c.Matrix(this.node.getCTM()),g=h(this),i=[g],j=new c.Matrix,k=g.toTransformString(),l=m(g)==m(this.matrix)?m(b.transform):k;"svg"!=e.type&&(e=e.parent());)i.push(h(e));for(d=i.length;d--;)j.add(i[d]);return{string:l,globalMatrix:f,totalMatrix:j,localMatrix:g,diffMatrix:f.clone().add(g.invert()),global:f.toTransformString(),total:j.toTransformString(),local:k,toString:t}}return a instanceof c.Matrix?(this.matrix=a,this._.transform=a.toTransformString()):h(this,a),this.node&&("linearGradient"==this.type||"radialGradient"==this.type?o(this.node,{gradientTransform:this.matrix}):"pattern"==this.type?o(this.node,{patternTransform:this.matrix}):o(this.node,{transform:this.matrix})),this},k.parent=function(){return s(this.node.parentNode)},k.append=k.add=function(a){if(a){if("set"==a.type){var b=this;return a.forEach(function(a){b.add(a)}),this}a=s(a),this.node.appendChild(a.node),a.paper=this.paper}return this},k.appendTo=function(a){return a&&(a=s(a),a.append(this)),this},k.prepend=function(a){if(a){if("set"==a.type){var b,c=this;return a.forEach(function(a){b?b.after(a):c.prepend(a),b=a}),this}a=s(a);var d=a.parent();this.node.insertBefore(a.node,this.node.firstChild),this.add&&this.add(),a.paper=this.paper,this.parent()&&this.parent().add(),d&&d.add()}return this},k.prependTo=function(a){return a=s(a),a.prepend(this),this},k.before=function(a){if("set"==a.type){var b=this;return a.forEach(function(a){var c=a.parent();b.node.parentNode.insertBefore(a.node,b.node),c&&c.add()}),this.parent().add(),this}a=s(a);var c=a.parent();return this.node.parentNode.insertBefore(a.node,this.node),this.parent()&&this.parent().add(),c&&c.add(),a.paper=this.paper,this},k.after=function(a){a=s(a);var b=a.parent();return this.node.nextSibling?this.node.parentNode.insertBefore(a.node,this.node.nextSibling):this.node.parentNode.appendChild(a.node),this.parent()&&this.parent().add(),b&&b.add(),a.paper=this.paper,this},k.insertBefore=function(a){a=s(a);var b=this.parent();return a.node.parentNode.insertBefore(this.node,a.node),this.paper=a.paper,b&&b.add(),a.parent()&&a.parent().add(),this},k.insertAfter=function(a){a=s(a);var b=this.parent();return a.node.parentNode.insertBefore(this.node,a.node.nextSibling),this.paper=a.paper,b&&b.add(),a.parent()&&a.parent().add(),this},k.remove=function(){var a=this.parent();return this.node.parentNode&&this.node.parentNode.removeChild(this.node),delete this.paper,this.removed=!0,a&&a.add(),this},k.select=function(a){return s(this.node.querySelector(a))},k.selectAll=function(a){for(var b=this.node.querySelectorAll(a),d=(c.set||Array)(),e=0;e{contents}',{x:+b.x.toFixed(3),y:+b.y.toFixed(3),width:+b.width.toFixed(3),height:+b.height.toFixed(3),contents:this.outerSVG()}); +return"data:image/svg+xml;base64,"+btoa(unescape(encodeURIComponent(d)))}},g.prototype.select=k.select,g.prototype.selectAll=k.selectAll}),d.plugin(function(a,d,e,f,g){function h(a,b,c){return function(d){var e=d.slice(a,b);return 1==e.length&&(e=e[0]),c?c(e):e}}var i=d.prototype,j=a.is,k=String,l="hasOwnProperty",m=function(a,b,d,e){"function"!=typeof d||d.length||(e=d,d=c.linear),this.attr=a,this.dur=b,d&&(this.easing=d),e&&(this.callback=e)};a._.Animation=m,a.animation=function(a,b,c,d){return new m(a,b,c,d)},i.inAnim=function(){var a=this,b=[];for(var c in a.anims)a.anims[l](c)&&!function(a){b.push({anim:new m(a._attrs,a.dur,a.easing,a._callback),mina:a,curStatus:a.status(),status:function(b){return a.status(b)},stop:function(){a.stop()}})}(a.anims[c]);return b},a.animate=function(a,d,e,f,g,h){"function"!=typeof g||g.length||(h=g,g=c.linear);var i=c.time(),j=c(a,d,i,i+f,c.time,e,g);return h&&b.once("mina.finish."+j.id,h),j},i.stop=function(){for(var a=this.inAnim(),b=0,c=a.length;c>b;b++)a[b].stop();return this},i.animate=function(a,d,e,f){"function"!=typeof e||e.length||(f=e,e=c.linear),a instanceof m&&(f=a.callback,e=a.easing,d=a.dur,a=a.attr);var g,i,n,o,p=[],q=[],r={},s=this;for(var t in a)if(a[l](t)){s.equal?(o=s.equal(t,k(a[t])),g=o.from,i=o.to,n=o.f):(g=+s.attr(t),i=+a[t]);var u=j(g,"array")?g.length:1;r[t]=h(p.length,p.length+u,n),p=p.concat(g),q=q.concat(i)}var v=c.time(),w=c(p,q,v,v+d,c.time,function(a){var b={};for(var c in r)r[l](c)&&(b[c]=r[c](a));s.attr(b)},e);return s.anims[w.id]=w,w._attrs=a,w._callback=f,b("snap.animcreated."+s.id,w),b.once("mina.finish."+w.id,function(){b.off("mina.*."+w.id),delete s.anims[w.id],f&&f.call(s)}),b.once("mina.stop."+w.id,function(){b.off("mina.*."+w.id),delete s.anims[w.id]}),s}}),d.plugin(function(a,b,c,d,e){function f(a,b,c,d,e,f){return null==b&&"[object SVGMatrix]"==g.call(a)?(this.a=a.a,this.b=a.b,this.c=a.c,this.d=a.d,this.e=a.e,void(this.f=a.f)):void(null!=a?(this.a=+a,this.b=+b,this.c=+c,this.d=+d,this.e=+e,this.f=+f):(this.a=1,this.b=0,this.c=0,this.d=1,this.e=0,this.f=0))}var g=Object.prototype.toString,h=String,i=Math,j="";!function(b){function c(a){return a[0]*a[0]+a[1]*a[1]}function d(a){var b=i.sqrt(c(a));a[0]&&(a[0]/=b),a[1]&&(a[1]/=b)}b.add=function(a,b,c,d,e,g){if(a&&a instanceof f)return this.add(a.a,a.b,a.c,a.d,a.e,a.f);var h=a*this.a+b*this.c,i=a*this.b+b*this.d;return this.e+=e*this.a+g*this.c,this.f+=e*this.b+g*this.d,this.c=c*this.a+d*this.c,this.d=c*this.b+d*this.d,this.a=h,this.b=i,this},f.prototype.multLeft=function(a,b,c,d,e,g){if(a&&a instanceof f)return this.multLeft(a.a,a.b,a.c,a.d,a.e,a.f);var h=a*this.a+c*this.b,i=a*this.c+c*this.d,j=a*this.e+c*this.f+e;return this.b=b*this.a+d*this.b,this.d=b*this.c+d*this.d,this.f=b*this.e+d*this.f+g,this.a=h,this.c=i,this.e=j,this},b.invert=function(){var a=this,b=a.a*a.d-a.b*a.c;return new f(a.d/b,-a.b/b,-a.c/b,a.a/b,(a.c*a.f-a.d*a.e)/b,(a.b*a.e-a.a*a.f)/b)},b.clone=function(){return new f(this.a,this.b,this.c,this.d,this.e,this.f)},b.translate=function(a,b){return this.e+=a*this.a+b*this.c,this.f+=a*this.b+b*this.d,this},b.scale=function(a,b,c,d){return null==b&&(b=a),(c||d)&&this.translate(c,d),this.a*=a,this.b*=a,this.c*=b,this.d*=b,(c||d)&&this.translate(-c,-d),this},b.rotate=function(b,c,d){b=a.rad(b),c=c||0,d=d||0;var e=+i.cos(b).toFixed(9),f=+i.sin(b).toFixed(9);return this.add(e,f,-f,e,c,d),this.add(1,0,0,1,-c,-d)},b.skewX=function(a){return this.skew(a,0)},b.skewY=function(a){return this.skew(0,a)},b.skew=function(b,c){b=b||0,c=c||0,b=a.rad(b),c=a.rad(c);var d=i.tan(b).toFixed(9),e=i.tan(c).toFixed(9);return this.add(1,e,d,1,0,0)},b.x=function(a,b){return a*this.a+b*this.c+this.e},b.y=function(a,b){return a*this.b+b*this.d+this.f},b.get=function(a){return+this[h.fromCharCode(97+a)].toFixed(4)},b.toString=function(){return"matrix("+[this.get(0),this.get(1),this.get(2),this.get(3),this.get(4),this.get(5)].join()+")"},b.offset=function(){return[this.e.toFixed(4),this.f.toFixed(4)]},b.determinant=function(){return this.a*this.d-this.b*this.c},b.split=function(){var b={};b.dx=this.e,b.dy=this.f;var e=[[this.a,this.b],[this.c,this.d]];b.scalex=i.sqrt(c(e[0])),d(e[0]),b.shear=e[0][0]*e[1][0]+e[0][1]*e[1][1],e[1]=[e[1][0]-e[0][0]*b.shear,e[1][1]-e[0][1]*b.shear],b.scaley=i.sqrt(c(e[1])),d(e[1]),b.shear/=b.scaley,this.determinant()<0&&(b.scalex=-b.scalex);var f=e[0][1],g=e[1][1];return 0>g?(b.rotate=a.deg(i.acos(g)),0>f&&(b.rotate=360-b.rotate)):b.rotate=a.deg(i.asin(f)),b.isSimple=!(+b.shear.toFixed(9)||b.scalex.toFixed(9)!=b.scaley.toFixed(9)&&b.rotate),b.isSuperSimple=!+b.shear.toFixed(9)&&b.scalex.toFixed(9)==b.scaley.toFixed(9)&&!b.rotate,b.noRotation=!+b.shear.toFixed(9)&&!b.rotate,b},b.toTransformString=function(a){var b=a||this.split();return+b.shear.toFixed(9)?"m"+[this.get(0),this.get(1),this.get(2),this.get(3),this.get(4),this.get(5)]:(b.scalex=+b.scalex.toFixed(4),b.scaley=+b.scaley.toFixed(4),b.rotate=+b.rotate.toFixed(4),(b.dx||b.dy?"t"+[+b.dx.toFixed(4),+b.dy.toFixed(4)]:j)+(b.rotate?"r"+[+b.rotate.toFixed(4),0,0]:j)+(1!=b.scalex||1!=b.scaley?"s"+[b.scalex,b.scaley,0,0]:j))}}(f.prototype),a.Matrix=f,a.matrix=function(a,b,c,d,e,g){return new f(a,b,c,d,e,g)}}),d.plugin(function(a,c,d,e,f){function g(d){return function(e){if(b.stop(),e instanceof f&&1==e.node.childNodes.length&&("radialGradient"==e.node.firstChild.tagName||"linearGradient"==e.node.firstChild.tagName||"pattern"==e.node.firstChild.tagName)&&(e=e.node.firstChild,n(this).appendChild(e),e=l(e)),e instanceof c)if("radialGradient"==e.type||"linearGradient"==e.type||"pattern"==e.type){e.node.id||p(e.node,{id:e.id});var g=q(e.node.id)}else g=e.attr(d);else if(g=a.color(e),g.error){var h=a(n(this).ownerSVGElement).gradient(e);h?(h.node.id||p(h.node,{id:h.id}),g=q(h.node.id)):g=e}else g=r(g);var i={};i[d]=g,p(this.node,i),this.node.style[d]=t}}function h(a){b.stop(),a==+a&&(a+="px"),this.node.style.fontSize=a}function i(a){for(var b=[],c=a.childNodes,d=0,e=c.length;e>d;d++){var f=c[d];3==f.nodeType&&b.push(f.nodeValue),"tspan"==f.tagName&&(1==f.childNodes.length&&3==f.firstChild.nodeType?b.push(f.firstChild.nodeValue):b.push(i(f)))}return b}function j(){return b.stop(),this.node.style.fontSize}var k=a._.make,l=a._.wrap,m=a.is,n=a._.getSomeDefs,o=/^url\((['"]?)([^)]+)\1\)$/,p=a._.$,q=a.url,r=String,s=a._.separator,t="";a.deurl=function(a){var b=String(a).match(o);return b?b[2]:a},b.on("snap.util.attr.mask",function(a){if(a instanceof c||a instanceof f){if(b.stop(),a instanceof f&&1==a.node.childNodes.length&&(a=a.node.firstChild,n(this).appendChild(a),a=l(a)),"mask"==a.type)var d=a;else d=k("mask",n(this)),d.node.appendChild(a.node);!d.node.id&&p(d.node,{id:d.id}),p(this.node,{mask:q(d.id)})}}),function(a){b.on("snap.util.attr.clip",a),b.on("snap.util.attr.clip-path",a),b.on("snap.util.attr.clipPath",a)}(function(a){if(a instanceof c||a instanceof f){b.stop();for(var d,e=a.node;e;){if("clipPath"===e.nodeName){d=new c(e);break}if("svg"===e.nodeName){d=void 0;break}e=e.parentNode}d||(d=k("clipPath",n(this)),d.node.appendChild(a.node),!d.node.id&&p(d.node,{id:d.id})),p(this.node,{"clip-path":q(d.node.id||d.id)})}}),b.on("snap.util.attr.fill",g("fill")),b.on("snap.util.attr.stroke",g("stroke"));var u=/^([lr])(?:\(([^)]*)\))?(.*)$/i;b.on("snap.util.grad.parse",function(a){function b(a,b){for(var c=(b-h)/(a-i),d=i;a>d;d++)f[d].offset=+(+h+c*(d-i)).toFixed(2);i=a,h=b}a=r(a);var c=a.match(u);if(!c)return null;var d=c[1],e=c[2],f=c[3];e=e.split(/\s*,\s*/).map(function(a){return+a==a?+a:a}),1==e.length&&0==e[0]&&(e=[]),f=f.split("-"),f=f.map(function(a){a=a.split(":");var b={color:a[0]};return a[1]&&(b.offset=parseFloat(a[1])),b});var g=f.length,h=0,i=0;g--;for(var j=0;g>j;j++)"offset"in f[j]&&b(j,f[j].offset);return f[g].offset=f[g].offset||100,b(g,f[g].offset),{type:d,params:e,stops:f}}),b.on("snap.util.attr.d",function(c){b.stop(),m(c,"array")&&m(c[0],"array")&&(c=a.path.toString.call(c)),c=r(c),c.match(/[ruo]/i)&&(c=a.path.toAbsolute(c)),p(this.node,{d:c})})(-1),b.on("snap.util.attr.#text",function(a){b.stop(),a=r(a);for(var c=e.doc.createTextNode(a);this.node.firstChild;)this.node.removeChild(this.node.firstChild);this.node.appendChild(c)})(-1),b.on("snap.util.attr.path",function(a){b.stop(),this.attr({d:a})})(-1),b.on("snap.util.attr.class",function(a){b.stop(),this.node.className.baseVal=a})(-1),b.on("snap.util.attr.viewBox",function(a){var c;c=m(a,"object")&&"x"in a?[a.x,a.y,a.width,a.height].join(" "):m(a,"array")?a.join(" "):a,p(this.node,{viewBox:c}),b.stop()})(-1),b.on("snap.util.attr.transform",function(a){this.transform(a),b.stop()})(-1),b.on("snap.util.attr.r",function(a){"rect"==this.type&&(b.stop(),p(this.node,{rx:a,ry:a}))})(-1),b.on("snap.util.attr.textpath",function(a){if(b.stop(),"text"==this.type){var d,e,f;if(!a&&this.textPath){for(e=this.textPath;e.node.firstChild;)this.node.appendChild(e.node.firstChild);return e.remove(),void delete this.textPath}if(m(a,"string")){var g=n(this),h=l(g.parentNode).path(a);g.appendChild(h.node),d=h.id,h.attr({id:d})}else a=l(a),a instanceof c&&(d=a.attr("id"),d||(d=a.id,a.attr({id:d})));if(d)if(e=this.textPath,f=this.node,e)e.attr({"xlink:href":"#"+d});else{for(e=p("textPath",{"xlink:href":"#"+d});f.firstChild;)e.appendChild(f.firstChild);f.appendChild(e),this.textPath=l(e)}}})(-1),b.on("snap.util.attr.text",function(a){if("text"==this.type){for(var c=this.node,d=function(a){var b=p("tspan");if(m(a,"array"))for(var c=0;c1&&(a=Array.prototype.slice.call(arguments,0));var b={};return i(a,"object")&&!i(a,"array")?b=a:null!=a&&(b={points:a}),this.el("polyline",b)},h.polygon=function(a){arguments.length>1&&(a=Array.prototype.slice.call(arguments,0));var b={};return i(a,"object")&&!i(a,"array")?b=a:null!=a&&(b={points:a}),this.el("polygon",b)},function(){function d(){return this.selectAll("stop")}function e(a,b){var d=l("stop"),e={offset:+b+"%"};a=c.color(a),e["stop-color"]=a.hex,a.opacity<1&&(e["stop-opacity"]=a.opacity),l(d,e);for(var f,g=this.stops(),h=0;hb){this.node.insertBefore(d,g[h].node),f=!0;break}}return f||this.node.appendChild(d),this}function f(){if("linearGradient"==this.type){var a=l(this.node,"x1")||0,b=l(this.node,"x2")||1,d=l(this.node,"y1")||0,e=l(this.node,"y2")||0;return c._.box(a,d,math.abs(b-a),math.abs(e-d))}var f=this.node.cx||.5,g=this.node.cy||.5,h=this.node.r||0;return c._.box(f-h,g-h,2*h,2*h)}function g(a){var d=a,e=this.stops();if("string"==typeof a&&(d=b("snap.util.grad.parse",null,"l(0,0,0,1)"+a).firstDefined().stops),c.is(d,"array")){for(var f=0;fh;h++){var i=f[h];d.addStop(i.color,i.offset)}return d}function j(a,b,h,i,j){var k=c._.make("linearGradient",a);return k.stops=d,k.addStop=e,k.getBBox=f,k.setStops=g,null!=b&&l(k.node,{x1:b,y1:h,x2:i,y2:j}),k}function k(a,b,g,h,i,j){var k=c._.make("radialGradient",a);return k.stops=d,k.addStop=e,k.getBBox=f,null!=b&&l(k.node,{cx:b,cy:g,r:h}),null!=i&&null!=j&&l(k.node,{fx:i,fy:j}),k}var l=c._.$;h.gradient=function(a){return i(this.defs,a)},h.gradientLinear=function(a,b,c,d){return j(this.defs,a,b,c,d)},h.gradientRadial=function(a,b,c,d,e){return k(this.defs,a,b,c,d,e)},h.toString=function(){var a,b=this.node.ownerDocument,d=b.createDocumentFragment(),e=b.createElement("div"),f=this.node.cloneNode(!0);return d.appendChild(e),e.appendChild(f),c._.$(f,{xmlns:"http://www.w3.org/2000/svg"}),a=e.innerHTML,d.removeChild(d.firstChild),a},h.toDataURL=function(){return a&&a.btoa?"data:image/svg+xml;base64,"+btoa(unescape(encodeURIComponent(this))):void 0},h.clear=function(){for(var a,b=this.node.firstChild;b;)a=b.nextSibling,"defs"!=b.tagName?b.parentNode.removeChild(b):h.clear.call({node:b}),b=a}}()}),d.plugin(function(a,b,c,d){function e(a){var b=e.ps=e.ps||{};return b[a]?b[a].sleep=100:b[a]={sleep:100},setTimeout(function(){for(var c in b)b[M](c)&&c!=a&&(b[c].sleep--,!b[c].sleep&&delete b[c])}),b[a]}function f(a,b,c,d){return null==a&&(a=b=c=d=0),null==b&&(b=a.y,c=a.width,d=a.height,a=a.x),{x:a,y:b,width:c,w:c,height:d,h:d,x2:a+c,y2:b+d,cx:a+c/2,cy:b+d/2,r1:P.min(c,d)/2,r2:P.max(c,d)/2,r0:P.sqrt(c*c+d*d)/2,path:y(a,b,c,d),vb:[a,b,c,d].join(" ")}}function g(){return this.join(",").replace(N,"$1")}function h(a){var b=L(a);return b.toString=g,b}function i(a,b,c,d,e,f,g,h,i){return null==i?p(a,b,c,d,e,f,g,h):k(a,b,c,d,e,f,g,h,q(a,b,c,d,e,f,g,h,i))}function j(c,d){function e(a){return+(+a).toFixed(3)}return a._.cacher(function(a,f,g){a instanceof b&&(a=a.attr("d")),a=G(a);for(var h,j,l,m,n,o="",p={},q=0,r=0,s=a.length;s>r;r++){if(l=a[r],"M"==l[0])h=+l[1],j=+l[2];else{if(m=i(h,j,l[1],l[2],l[3],l[4],l[5],l[6]),q+m>f){if(d&&!p.start){if(n=i(h,j,l[1],l[2],l[3],l[4],l[5],l[6],f-q),o+=["C"+e(n.start.x),e(n.start.y),e(n.m.x),e(n.m.y),e(n.x),e(n.y)],g)return o;p.start=o,o=["M"+e(n.x),e(n.y)+"C"+e(n.n.x),e(n.n.y),e(n.end.x),e(n.end.y),e(l[5]),e(l[6])].join(),q+=m,h=+l[5],j=+l[6];continue}if(!c&&!d)return n=i(h,j,l[1],l[2],l[3],l[4],l[5],l[6],f-q)}q+=m,h=+l[5],j=+l[6]}o+=l.shift()+l}return p.end=o,n=c?q:d?p:k(h,j,l[0],l[1],l[2],l[3],l[4],l[5],1)},null,a._.clone)}function k(a,b,c,d,e,f,g,h,i){var j=1-i,k=T(j,3),l=T(j,2),m=i*i,n=m*i,o=k*a+3*l*i*c+3*j*i*i*e+n*g,p=k*b+3*l*i*d+3*j*i*i*f+n*h,q=a+2*i*(c-a)+m*(e-2*c+a),r=b+2*i*(d-b)+m*(f-2*d+b),s=c+2*i*(e-c)+m*(g-2*e+c),t=d+2*i*(f-d)+m*(h-2*f+d),u=j*a+i*c,v=j*b+i*d,w=j*e+i*g,x=j*f+i*h,y=90-180*P.atan2(q-s,r-t)/Q;return{x:o,y:p,m:{x:q,y:r},n:{x:s,y:t},start:{x:u,y:v},end:{x:w,y:x},alpha:y}}function l(b,c,d,e,g,h,i,j){a.is(b,"array")||(b=[b,c,d,e,g,h,i,j]);var k=F.apply(null,b);return f(k.min.x,k.min.y,k.max.x-k.min.x,k.max.y-k.min.y)}function m(a,b,c){return b>=a.x&&b<=a.x+a.width&&c>=a.y&&c<=a.y+a.height}function n(a,b){return a=f(a),b=f(b),m(b,a.x,a.y)||m(b,a.x2,a.y)||m(b,a.x,a.y2)||m(b,a.x2,a.y2)||m(a,b.x,b.y)||m(a,b.x2,b.y)||m(a,b.x,b.y2)||m(a,b.x2,b.y2)||(a.xb.x||b.xa.x)&&(a.yb.y||b.ya.y)}function o(a,b,c,d,e){var f=-3*b+9*c-9*d+3*e,g=a*f+6*b-12*c+6*d;return a*g-3*b+3*c}function p(a,b,c,d,e,f,g,h,i){null==i&&(i=1),i=i>1?1:0>i?0:i;for(var j=i/2,k=12,l=[-.1252,.1252,-.3678,.3678,-.5873,.5873,-.7699,.7699,-.9041,.9041,-.9816,.9816],m=[.2491,.2491,.2335,.2335,.2032,.2032,.1601,.1601,.1069,.1069,.0472,.0472],n=0,p=0;k>p;p++){var q=j*l[p]+j,r=o(q,a,c,e,g),s=o(q,b,d,f,h),t=r*r+s*s;n+=m[p]*P.sqrt(t)}return j*n}function q(a,b,c,d,e,f,g,h,i){if(!(0>i||p(a,b,c,d,e,f,g,h)n;)l/=2,m+=(i>j?1:-1)*l,j=p(a,b,c,d,e,f,g,h,m);return m}}function r(a,b,c,d,e,f,g,h){if(!(S(a,c)S(e,g)||S(b,d)S(f,h))){var i=(a*d-b*c)*(e-g)-(a-c)*(e*h-f*g),j=(a*d-b*c)*(f-h)-(b-d)*(e*h-f*g),k=(a-c)*(f-h)-(b-d)*(e-g);if(k){var l=i/k,m=j/k,n=+l.toFixed(2),o=+m.toFixed(2);if(!(n<+R(a,c).toFixed(2)||n>+S(a,c).toFixed(2)||n<+R(e,g).toFixed(2)||n>+S(e,g).toFixed(2)||o<+R(b,d).toFixed(2)||o>+S(b,d).toFixed(2)||o<+R(f,h).toFixed(2)||o>+S(f,h).toFixed(2)))return{x:l,y:m}}}}function s(a,b,c){var d=l(a),e=l(b);if(!n(d,e))return c?0:[];for(var f=p.apply(0,a),g=p.apply(0,b),h=~~(f/8),i=~~(g/8),j=[],m=[],o={},q=c?0:[],s=0;h+1>s;s++){var t=k.apply(0,a.concat(s/h));j.push({x:t.x,y:t.y,t:s/h})}for(s=0;i+1>s;s++)t=k.apply(0,b.concat(s/i)),m.push({x:t.x,y:t.y,t:s/i});for(s=0;h>s;s++)for(var u=0;i>u;u++){var v=j[s],w=j[s+1],x=m[u],y=m[u+1],z=U(w.x-v.x)<.001?"y":"x",A=U(y.x-x.x)<.001?"y":"x",B=r(v.x,v.y,w.x,w.y,x.x,x.y,y.x,y.y);if(B){if(o[B.x.toFixed(4)]==B.y.toFixed(4))continue;o[B.x.toFixed(4)]=B.y.toFixed(4);var C=v.t+U((B[z]-v[z])/(w[z]-v[z]))*(w.t-v.t),D=x.t+U((B[A]-x[A])/(y[A]-x[A]))*(y.t-x.t);C>=0&&1>=C&&D>=0&&1>=D&&(c?q++:q.push({x:B.x,y:B.y,t1:C,t2:D}))}}return q}function t(a,b){return v(a,b)}function u(a,b){return v(a,b,1)}function v(a,b,c){a=G(a),b=G(b);for(var d,e,f,g,h,i,j,k,l,m,n=c?0:[],o=0,p=a.length;p>o;o++){var q=a[o];if("M"==q[0])d=h=q[1],e=i=q[2];else{"C"==q[0]?(l=[d,e].concat(q.slice(1)),d=l[6],e=l[7]):(l=[d,e,d,e,h,i,h,i],d=h,e=i);for(var r=0,t=b.length;t>r;r++){var u=b[r];if("M"==u[0])f=j=u[1],g=k=u[2];else{"C"==u[0]?(m=[f,g].concat(u.slice(1)),f=m[6],g=m[7]):(m=[f,g,f,g,j,k,j,k],f=j,g=k);var v=s(l,m,c);if(c)n+=v;else{for(var w=0,x=v.length;x>w;w++)v[w].segment1=o,v[w].segment2=r,v[w].bez1=l,v[w].bez2=m;n=n.concat(v)}}}}}return n}function w(a,b,c){var d=x(a);return m(d,b,c)&&v(a,[["M",b,c],["H",d.x2+10]],1)%2==1}function x(a){var b=e(a);if(b.bbox)return L(b.bbox);if(!a)return f();a=G(a);for(var c,d=0,g=0,h=[],i=[],j=0,k=a.length;k>j;j++)if(c=a[j],"M"==c[0])d=c[1],g=c[2],h.push(d),i.push(g);else{var l=F(d,g,c[1],c[2],c[3],c[4],c[5],c[6]);h=h.concat(l.min.x,l.max.x),i=i.concat(l.min.y,l.max.y),d=c[5],g=c[6]}var m=R.apply(0,h),n=R.apply(0,i),o=S.apply(0,h),p=S.apply(0,i),q=f(m,n,o-m,p-n);return b.bbox=L(q),q}function y(a,b,c,d,e){if(e)return[["M",+a+ +e,b],["l",c-2*e,0],["a",e,e,0,0,1,e,e],["l",0,d-2*e],["a",e,e,0,0,1,-e,e],["l",2*e-c,0],["a",e,e,0,0,1,-e,-e],["l",0,2*e-d],["a",e,e,0,0,1,e,-e],["z"]];var f=[["M",a,b],["l",c,0],["l",0,d],["l",-c,0],["z"]];return f.toString=g,f}function z(a,b,c,d,e){if(null==e&&null==d&&(d=c),a=+a,b=+b,c=+c,d=+d,null!=e)var f=Math.PI/180,h=a+c*Math.cos(-d*f),i=a+c*Math.cos(-e*f),j=b+c*Math.sin(-d*f),k=b+c*Math.sin(-e*f),l=[["M",h,j],["A",c,c,0,+(e-d>180),0,i,k]];else l=[["M",a,b],["m",0,-d],["a",c,d,0,1,1,0,2*d],["a",c,d,0,1,1,0,-2*d],["z"]];return l.toString=g,l}function A(b){var c=e(b),d=String.prototype.toLowerCase;if(c.rel)return h(c.rel);a.is(b,"array")&&a.is(b&&b[0],"array")||(b=a.parsePathString(b));var f=[],i=0,j=0,k=0,l=0,m=0;"M"==b[0][0]&&(i=b[0][1],j=b[0][2],k=i,l=j,m++,f.push(["M",i,j]));for(var n=m,o=b.length;o>n;n++){var p=f[n]=[],q=b[n];if(q[0]!=d.call(q[0]))switch(p[0]=d.call(q[0]),p[0]){case"a":p[1]=q[1],p[2]=q[2],p[3]=q[3],p[4]=q[4],p[5]=q[5],p[6]=+(q[6]-i).toFixed(3),p[7]=+(q[7]-j).toFixed(3);break;case"v":p[1]=+(q[1]-j).toFixed(3);break;case"m":k=q[1],l=q[2];default:for(var r=1,s=q.length;s>r;r++)p[r]=+(q[r]-(r%2?i:j)).toFixed(3)}else{p=f[n]=[],"m"==q[0]&&(k=q[1]+i,l=q[2]+j);for(var t=0,u=q.length;u>t;t++)f[n][t]=q[t]}var v=f[n].length;switch(f[n][0]){case"z":i=k,j=l;break;case"h":i+=+f[n][v-1];break;case"v":j+=+f[n][v-1];break;default:i+=+f[n][v-2],j+=+f[n][v-1]}}return f.toString=g,c.rel=h(f),f}function B(b){var c=e(b);if(c.abs)return h(c.abs);if(K(b,"array")&&K(b&&b[0],"array")||(b=a.parsePathString(b)),!b||!b.length)return[["M",0,0]];var d,f=[],i=0,j=0,k=0,l=0,m=0;"M"==b[0][0]&&(i=+b[0][1],j=+b[0][2],k=i,l=j,m++,f[0]=["M",i,j]);for(var n,o,p=3==b.length&&"M"==b[0][0]&&"R"==b[1][0].toUpperCase()&&"Z"==b[2][0].toUpperCase(),q=m,r=b.length;r>q;q++){if(f.push(n=[]),o=b[q],d=o[0],d!=d.toUpperCase())switch(n[0]=d.toUpperCase(),n[0]){case"A":n[1]=o[1],n[2]=o[2],n[3]=o[3],n[4]=o[4],n[5]=o[5],n[6]=+o[6]+i,n[7]=+o[7]+j;break;case"V":n[1]=+o[1]+j;break;case"H":n[1]=+o[1]+i;break;case"R":for(var s=[i,j].concat(o.slice(1)),t=2,u=s.length;u>t;t++)s[t]=+s[t]+i,s[++t]=+s[t]+j;f.pop(),f=f.concat(I(s,p));break;case"O":f.pop(),s=z(i,j,o[1],o[2]),s.push(s[0]),f=f.concat(s);break;case"U":f.pop(),f=f.concat(z(i,j,o[1],o[2],o[3])),n=["U"].concat(f[f.length-1].slice(-2));break;case"M":k=+o[1]+i,l=+o[2]+j;default:for(t=1,u=o.length;u>t;t++)n[t]=+o[t]+(t%2?i:j)}else if("R"==d)s=[i,j].concat(o.slice(1)),f.pop(),f=f.concat(I(s,p)),n=["R"].concat(o.slice(-2));else if("O"==d)f.pop(),s=z(i,j,o[1],o[2]),s.push(s[0]),f=f.concat(s);else if("U"==d)f.pop(),f=f.concat(z(i,j,o[1],o[2],o[3])),n=["U"].concat(f[f.length-1].slice(-2));else for(var v=0,w=o.length;w>v;v++)n[v]=o[v];if(d=d.toUpperCase(),"O"!=d)switch(n[0]){case"Z":i=+k,j=+l;break;case"H":i=n[1];break;case"V":j=n[1];break;case"M":k=n[n.length-2],l=n[n.length-1];default:i=n[n.length-2],j=n[n.length-1]}}return f.toString=g,c.abs=h(f),f}function C(a,b,c,d){return[a,b,c,d,c,d]}function D(a,b,c,d,e,f){var g=1/3,h=2/3;return[g*a+h*c,g*b+h*d,g*e+h*c,g*f+h*d,e,f]}function E(b,c,d,e,f,g,h,i,j,k){var l,m=120*Q/180,n=Q/180*(+f||0),o=[],p=a._.cacher(function(a,b,c){var d=a*P.cos(c)-b*P.sin(c),e=a*P.sin(c)+b*P.cos(c);return{x:d,y:e}});if(!d||!e)return[b,c,i,j,i,j];if(k)y=k[0],z=k[1],w=k[2],x=k[3];else{l=p(b,c,-n),b=l.x,c=l.y,l=p(i,j,-n),i=l.x,j=l.y;var q=(P.cos(Q/180*f),P.sin(Q/180*f),(b-i)/2),r=(c-j)/2,s=q*q/(d*d)+r*r/(e*e);s>1&&(s=P.sqrt(s),d=s*d,e=s*e);var t=d*d,u=e*e,v=(g==h?-1:1)*P.sqrt(U((t*u-t*r*r-u*q*q)/(t*r*r+u*q*q))),w=v*d*r/e+(b+i)/2,x=v*-e*q/d+(c+j)/2,y=P.asin(((c-x)/e).toFixed(9)),z=P.asin(((j-x)/e).toFixed(9));y=w>b?Q-y:y,z=w>i?Q-z:z,0>y&&(y=2*Q+y),0>z&&(z=2*Q+z),h&&y>z&&(y-=2*Q),!h&&z>y&&(z-=2*Q)}var A=z-y;if(U(A)>m){var B=z,C=i,D=j;z=y+m*(h&&z>y?1:-1),i=w+d*P.cos(z),j=x+e*P.sin(z),o=E(i,j,d,e,f,0,h,C,D,[z,B,w,x])}A=z-y;var F=P.cos(y),G=P.sin(y),H=P.cos(z),I=P.sin(z),J=P.tan(A/4),K=4/3*d*J,L=4/3*e*J,M=[b,c],N=[b+K*G,c-L*F],O=[i+K*I,j-L*H],R=[i,j];if(N[0]=2*M[0]-N[0],N[1]=2*M[1]-N[1],k)return[N,O,R].concat(o);o=[N,O,R].concat(o).join().split(",");for(var S=[],T=0,V=o.length;V>T;T++)S[T]=T%2?p(o[T-1],o[T],n).y:p(o[T],o[T+1],n).x;return S}function F(a,b,c,d,e,f,g,h){for(var i,j,k,l,m,n,o,p,q=[],r=[[],[]],s=0;2>s;++s)if(0==s?(j=6*a-12*c+6*e,i=-3*a+9*c-9*e+3*g,k=3*c-3*a):(j=6*b-12*d+6*f,i=-3*b+9*d-9*f+3*h,k=3*d-3*b),U(i)<1e-12){if(U(j)<1e-12)continue;l=-k/j,l>0&&1>l&&q.push(l)}else o=j*j-4*k*i,p=P.sqrt(o),0>o||(m=(-j+p)/(2*i),m>0&&1>m&&q.push(m),n=(-j-p)/(2*i),n>0&&1>n&&q.push(n));for(var t,u=q.length,v=u;u--;)l=q[u],t=1-l,r[0][u]=t*t*t*a+3*t*t*l*c+3*t*l*l*e+l*l*l*g,r[1][u]=t*t*t*b+3*t*t*l*d+3*t*l*l*f+l*l*l*h;return r[0][v]=a,r[1][v]=b,r[0][v+1]=g,r[1][v+1]=h,r[0].length=r[1].length=v+2,{min:{x:R.apply(0,r[0]),y:R.apply(0,r[1])},max:{x:S.apply(0,r[0]),y:S.apply(0,r[1])}}}function G(a,b){var c=!b&&e(a);if(!b&&c.curve)return h(c.curve);for(var d=B(a),f=b&&B(b),g={x:0,y:0,bx:0,by:0,X:0,Y:0,qx:null,qy:null},i={x:0,y:0,bx:0,by:0,X:0,Y:0,qx:null,qy:null},j=(function(a,b,c){var d,e;if(!a)return["C",b.x,b.y,b.x,b.y,b.x,b.y];switch(!(a[0]in{T:1,Q:1})&&(b.qx=b.qy=null),a[0]){case"M":b.X=a[1],b.Y=a[2];break;case"A":a=["C"].concat(E.apply(0,[b.x,b.y].concat(a.slice(1))));break;case"S":"C"==c||"S"==c?(d=2*b.x-b.bx,e=2*b.y-b.by):(d=b.x,e=b.y),a=["C",d,e].concat(a.slice(1));break;case"T":"Q"==c||"T"==c?(b.qx=2*b.x-b.qx,b.qy=2*b.y-b.qy):(b.qx=b.x,b.qy=b.y),a=["C"].concat(D(b.x,b.y,b.qx,b.qy,a[1],a[2]));break;case"Q":b.qx=a[1],b.qy=a[2],a=["C"].concat(D(b.x,b.y,a[1],a[2],a[3],a[4]));break;case"L":a=["C"].concat(C(b.x,b.y,a[1],a[2]));break;case"H":a=["C"].concat(C(b.x,b.y,a[1],b.y));break;case"V":a=["C"].concat(C(b.x,b.y,b.x,a[1]));break;case"Z":a=["C"].concat(C(b.x,b.y,b.X,b.Y))}return a}),k=function(a,b){if(a[b].length>7){a[b].shift();for(var c=a[b];c.length;)m[b]="A",f&&(n[b]="A"),a.splice(b++,0,["C"].concat(c.splice(0,6)));a.splice(b,1),r=S(d.length,f&&f.length||0)}},l=function(a,b,c,e,g){a&&b&&"M"==a[g][0]&&"M"!=b[g][0]&&(b.splice(g,0,["M",e.x,e.y]),c.bx=0,c.by=0,c.x=a[g][1],c.y=a[g][2],r=S(d.length,f&&f.length||0))},m=[],n=[],o="",p="",q=0,r=S(d.length,f&&f.length||0);r>q;q++){d[q]&&(o=d[q][0]),"C"!=o&&(m[q]=o,q&&(p=m[q-1])),d[q]=j(d[q],g,p),"A"!=m[q]&&"C"==o&&(m[q]="C"),k(d,q),f&&(f[q]&&(o=f[q][0]),"C"!=o&&(n[q]=o,q&&(p=n[q-1])),f[q]=j(f[q],i,p),"A"!=n[q]&&"C"==o&&(n[q]="C"),k(f,q)),l(d,f,g,i,q),l(f,d,i,g,q);var s=d[q],t=f&&f[q],u=s.length,v=f&&t.length;g.x=s[u-2],g.y=s[u-1],g.bx=O(s[u-4])||g.x,g.by=O(s[u-3])||g.y,i.bx=f&&(O(t[v-4])||i.x),i.by=f&&(O(t[v-3])||i.y),i.x=f&&t[v-2],i.y=f&&t[v-1]}return f||(c.curve=h(d)),f?[d,f]:d}function H(a,b){if(!b)return a;var c,d,e,f,g,h,i;for(a=G(a),e=0,g=a.length;g>e;e++)for(i=a[e],f=1,h=i.length;h>f;f+=2)c=b.x(i[f],i[f+1]),d=b.y(i[f],i[f+1]),i[f]=c,i[f+1]=d;return a}function I(a,b){for(var c=[],d=0,e=a.length;e-2*!b>d;d+=2){var f=[{x:+a[d-2],y:+a[d-1]},{x:+a[d],y:+a[d+1]},{x:+a[d+2],y:+a[d+3]},{x:+a[d+4],y:+a[d+5]}];b?d?e-4==d?f[3]={x:+a[0],y:+a[1]}:e-2==d&&(f[2]={x:+a[0],y:+a[1]},f[3]={x:+a[2],y:+a[3]}):f[0]={x:+a[e-2],y:+a[e-1]}:e-4==d?f[3]=f[2]:d||(f[0]={x:+a[d],y:+a[d+1]}),c.push(["C",(-f[0].x+6*f[1].x+f[2].x)/6,(-f[0].y+6*f[1].y+f[2].y)/6,(f[1].x+6*f[2].x-f[3].x)/6,(f[1].y+6*f[2].y-f[3].y)/6,f[2].x,f[2].y])}return c}var J=b.prototype,K=a.is,L=a._.clone,M="hasOwnProperty",N=/,?([a-z]),?/gi,O=parseFloat,P=Math,Q=P.PI,R=P.min,S=P.max,T=P.pow,U=P.abs,V=j(1),W=j(),X=j(0,1),Y=a._unit2px,Z={path:function(a){return a.attr("path")},circle:function(a){var b=Y(a);return z(b.cx,b.cy,b.r)},ellipse:function(a){var b=Y(a);return z(b.cx||0,b.cy||0,b.rx,b.ry); +},rect:function(a){var b=Y(a);return y(b.x||0,b.y||0,b.width,b.height,b.rx,b.ry)},image:function(a){var b=Y(a);return y(b.x||0,b.y||0,b.width,b.height)},line:function(a){return"M"+[a.attr("x1")||0,a.attr("y1")||0,a.attr("x2"),a.attr("y2")]},polyline:function(a){return"M"+a.attr("points")},polygon:function(a){return"M"+a.attr("points")+"z"},deflt:function(a){var b=a.node.getBBox();return y(b.x,b.y,b.width,b.height)}};a.path=e,a.path.getTotalLength=V,a.path.getPointAtLength=W,a.path.getSubpath=function(a,b,c){if(this.getTotalLength(a)-c<1e-6)return X(a,b).end;var d=X(a,c,1);return b?X(d,b).end:d},J.getTotalLength=function(){return this.node.getTotalLength?this.node.getTotalLength():void 0},J.getPointAtLength=function(a){return W(this.attr("d"),a)},J.getSubpath=function(b,c){return a.path.getSubpath(this.attr("d"),b,c)},a._.box=f,a.path.findDotsAtSegment=k,a.path.bezierBBox=l,a.path.isPointInsideBBox=m,a.closest=function(b,c,d,e){for(var g=100,h=f(b-g/2,c-g/2,g,g),i=[],j=d[0].hasOwnProperty("x")?function(a){return{x:d[a].x,y:d[a].y}}:function(a){return{x:d[a],y:e[a]}},k=0;1e6>=g&&!k;){for(var l=0,n=d.length;n>l;l++){var o=j(l);if(m(h,o.x,o.y)){k++,i.push(o);break}}k||(g*=2,h=f(b-g/2,c-g/2,g,g))}if(1e6!=g){var p,q=1/0;for(l=0,n=i.length;n>l;l++){var r=a.len(b,c,i[l].x,i[l].y);q>r&&(q=r,i[l].len=r,p=i[l])}return p}},a.path.isBBoxIntersect=n,a.path.intersection=t,a.path.intersectionNumber=u,a.path.isPointInside=w,a.path.getBBox=x,a.path.get=Z,a.path.toRelative=A,a.path.toAbsolute=B,a.path.toCubic=G,a.path.map=H,a.path.toString=g,a.path.clone=h}),d.plugin(function(a,d,e,f){var g=Math.max,h=Math.min,i=function(a){if(this.items=[],this.bindings={},this.length=0,this.type="set",a)for(var b=0,c=a.length;c>b;b++)a[b]&&(this[this.items.length]=this.items[this.items.length]=a[b],this.length++)},j=i.prototype;j.push=function(){for(var a,b,c=0,d=arguments.length;d>c;c++)a=arguments[c],a&&(b=this.items.length,this[b]=this.items[b]=a,this.length++);return this},j.pop=function(){return this.length&&delete this[this.length--],this.items.pop()},j.forEach=function(a,b){for(var c=0,d=this.items.length;d>c;c++)if(a.call(b,this.items[c],c)===!1)return this;return this},j.animate=function(d,e,f,g){"function"!=typeof f||f.length||(g=f,f=c.linear),d instanceof a._.Animation&&(g=d.callback,f=d.easing,e=f.dur,d=d.attr);var h=arguments;if(a.is(d,"array")&&a.is(h[h.length-1],"array"))var i=!0;var j,k=function(){j?this.b=j:j=this.b},l=0,m=this,n=g&&function(){++l==m.length&&g.call(this)};return this.forEach(function(a,c){b.once("snap.animcreated."+a.id,k),i?h[c]&&a.animate.apply(a,h[c]):a.animate(d,e,f,n)})},j.remove=function(){for(;this.length;)this.pop().remove();return this},j.bind=function(a,b,c){var d={};if("function"==typeof b)this.bindings[a]=b;else{var e=c||a;this.bindings[a]=function(a){d[e]=a,b.attr(d)}}return this},j.attr=function(a){var b={};for(var c in a)this.bindings[c]?this.bindings[c](a[c]):b[c]=a[c];for(var d=0,e=this.items.length;e>d;d++)this.items[d].attr(b);return this},j.clear=function(){for(;this.length;)this.pop()},j.splice=function(a,b,c){a=0>a?g(this.length+a,0):a,b=g(0,h(this.length-a,b));var d,e=[],f=[],j=[];for(d=2;dd;d++)f.push(this[a+d]);for(;dd?j[d]:e[d-k];for(d=this.items.length=this.length-=b-k;this[d];)delete this[d++];return new i(f)},j.exclude=function(a){for(var b=0,c=this.length;c>b;b++)if(this[b]==a)return this.splice(b,1),!0;return!1},j.insertAfter=function(a){for(var b=this.items.length;b--;)this.items[b].insertAfter(a);return this},j.getBBox=function(){for(var a=[],b=[],c=[],d=[],e=this.items.length;e--;)if(!this.items[e].removed){var f=this.items[e].getBBox();a.push(f.x),b.push(f.y),c.push(f.x+f.width),d.push(f.y+f.height)}return a=h.apply(0,a),b=h.apply(0,b),c=g.apply(0,c),d=g.apply(0,d),{x:a,y:b,x2:c,y2:d,width:c-a,height:d-b,cx:a+(c-a)/2,cy:b+(d-b)/2}},j.clone=function(a){a=new i;for(var b=0,c=this.items.length;c>b;b++)a.push(this.items[b].clone());return a},j.toString=function(){return"Snap‘s set"},j.type="set",a.Set=i,a.set=function(){var a=new i;return arguments.length&&a.push.apply(a,Array.prototype.slice.call(arguments,0)),a}}),d.plugin(function(a,c,d,e){function f(a){var b=a[0];switch(b.toLowerCase()){case"t":return[b,0,0];case"m":return[b,1,0,0,1,0,0];case"r":return 4==a.length?[b,0,a[2],a[3]]:[b,0];case"s":return 5==a.length?[b,1,1,a[3],a[4]]:3==a.length?[b,1,1]:[b,1]}}function g(b,c,d){b=b||new a.Matrix,c=c||new a.Matrix,b=a.parseTransformString(b.toTransformString())||[],c=a.parseTransformString(c.toTransformString())||[];for(var e,g,h,i,j=Math.max(b.length,c.length),k=[],n=[],o=0;j>o;o++){if(h=b[o]||f(c[o]),i=c[o]||f(h),h[0]!=i[0]||"r"==h[0].toLowerCase()&&(h[2]!=i[2]||h[3]!=i[3])||"s"==h[0].toLowerCase()&&(h[3]!=i[3]||h[4]!=i[4])){b=a._.transform2matrix(b,d()),c=a._.transform2matrix(c,d()),k=[["m",b.a,b.b,b.c,b.d,b.e,b.f]],n=[["m",c.a,c.b,c.c,c.d,c.e,c.f]];break}for(k[o]=[],n[o]=[],e=0,g=Math.max(h.length,i.length);g>e;e++)e in h&&(k[o][e]=h[e]),e in i&&(n[o][e]=i[e])}return{from:m(k),to:m(n),f:l(k)}}function h(a){return a}function i(a){return function(b){return+b.toFixed(3)+a}}function j(a){return a.join(" ")}function k(b){return a.rgb(b[0],b[1],b[2],b[3])}function l(a){var b,c,d,e,f,g,h=0,i=[];for(b=0,c=a.length;c>b;b++){for(f="[",g=['"'+a[b][0]+'"'],d=1,e=a[b].length;e>d;d++)g[d]="val["+h++ +"]";f+=g+"]",i[b]=f}return Function("val","return Snap.path.toString.call(["+i+"])")}function m(a){for(var b=[],c=0,d=a.length;d>c;c++)for(var e=1,f=a[c].length;f>e;e++)b.push(a[c][e]);return b}function n(a){return isFinite(a)}function o(b,c){return a.is(b,"array")&&a.is(c,"array")?b.toString()==c.toString():!1}var p={},q=/[%a-z]+$/i,r=String;p.stroke=p.fill="colour",c.prototype.equal=function(a,c){return b("snap.util.equal",this,a,c).firstDefined()},b.on("snap.util.equal",function(b,c){var d,e,f=r(this.attr(b)||""),s=this;if("colour"==p[b])return d=a.color(f),e=a.color(c),{from:[d.r,d.g,d.b,d.opacity],to:[e.r,e.g,e.b,e.opacity],f:k};if("viewBox"==b)return d=this.attr(b).vb.split(" ").map(Number),e=c.split(" ").map(Number),{from:d,to:e,f:j};if("transform"==b||"gradientTransform"==b||"patternTransform"==b)return"string"==typeof c&&(c=r(c).replace(/\.{3}|\u2026/g,f)),f=this.matrix,c=a._.rgTransform.test(c)?a._.transform2matrix(c,this.getBBox()):a._.transform2matrix(a._.svgTransform2string(c),this.getBBox()),g(f,c,function(){return s.getBBox(1)});if("d"==b||"path"==b)return d=a.path.toCubic(f,c),{from:m(d[0]),to:m(d[1]),f:l(d[0])};if("points"==b)return d=r(f).split(a._.separator),e=r(c).split(a._.separator),{from:d,to:e,f:function(a){return a}};if(n(f)&&n(c))return{from:parseFloat(f),to:parseFloat(c),f:h};var t=f.match(q),u=r(c).match(q);return t&&o(t,u)?{from:parseFloat(f),to:parseFloat(c),f:i(t)}:{from:this.asPX(b),to:this.asPX(b,c),f:h}})}),d.plugin(function(a,c,d,e){for(var f=c.prototype,g="hasOwnProperty",h=("createTouch"in e.doc),i=["click","dblclick","mousedown","mousemove","mouseout","mouseover","mouseup","touchstart","touchmove","touchend","touchcancel"],j={mousedown:"touchstart",mousemove:"touchmove",mouseup:"touchend"},k=(function(a,b){var c="y"==a?"scrollTop":"scrollLeft",d=b&&b.node?b.node.ownerDocument:e.doc;return d[c in d.documentElement?"documentElement":"body"][c]}),l=function(){return this.originalEvent.preventDefault()},m=function(){return this.originalEvent.stopPropagation()},n=function(a,b,c,d){var e=h&&j[b]?j[b]:b,f=function(e){var f=k("y",d),i=k("x",d);if(h&&j[g](b))for(var n=0,o=e.targetTouches&&e.targetTouches.length;o>n;n++)if(e.targetTouches[n].target==a||a.contains(e.targetTouches[n].target)){var p=e;e=e.targetTouches[n],e.originalEvent=p,e.preventDefault=l,e.stopPropagation=m;break}var q=e.clientX+i,r=e.clientY+f;return c.call(d,e,q,r)};return b!==e&&a.addEventListener(b,f,!1),a.addEventListener(e,f,!1),function(){return b!==e&&a.removeEventListener(b,f,!1),a.removeEventListener(e,f,!1),!0}},o=[],p=function(a){for(var c,d=a.clientX,e=a.clientY,f=k("y"),g=k("x"),i=o.length;i--;){if(c=o[i],h){for(var j,l=a.touches&&a.touches.length;l--;)if(j=a.touches[l],j.identifier==c.el._drag.id||c.el.node.contains(j.target)){d=j.clientX,e=j.clientY,(a.originalEvent?a.originalEvent:a).preventDefault();break}}else a.preventDefault();var m=c.el.node;m.nextSibling,m.parentNode,m.style.display;d+=g,e+=f,b("snap.drag.move."+c.el.id,c.move_scope||c.el,d-c.el._drag.x,e-c.el._drag.y,d,e,a)}},q=function(c){a.unmousemove(p).unmouseup(q);for(var d,e=o.length;e--;)d=o[e],d.el._drag={},b("snap.drag.end."+d.el.id,d.end_scope||d.start_scope||d.move_scope||d.el,c),b.off("snap.drag.*."+d.el.id);o=[]},r=i.length;r--;)!function(b){a[b]=f[b]=function(c,d){if(a.is(c,"function"))this.events=this.events||[],this.events.push({name:b,f:c,unbind:n(this.node||document,b,c,d||this)});else for(var e=0,f=this.events.length;f>e;e++)if(this.events[e].name==b)try{this.events[e].f.call(this)}catch(g){}return this},a["un"+b]=f["un"+b]=function(a){for(var c=this.events||[],d=c.length;d--;)if(c[d].name==b&&(c[d].f==a||!a))return c[d].unbind(),c.splice(d,1),!c.length&&delete this.events,this;return this}}(i[r]);f.hover=function(a,b,c,d){return this.mouseover(a,c).mouseout(b,d||c)},f.unhover=function(a,b){return this.unmouseover(a).unmouseout(b)};var s=[];f.drag=function(c,d,e,f,g,h){function i(i,j,l){(i.originalEvent||i).preventDefault(),k._drag.x=j,k._drag.y=l,k._drag.id=i.identifier,!o.length&&a.mousemove(p).mouseup(q),o.push({el:k,move_scope:f,start_scope:g,end_scope:h}),d&&b.on("snap.drag.start."+k.id,d),c&&b.on("snap.drag.move."+k.id,c),e&&b.on("snap.drag.end."+k.id,e),b("snap.drag.start."+k.id,g||f||k,j,l,i)}function j(a,c,d){b("snap.draginit."+k.id,k,a,c,d)}var k=this;if(!arguments.length){var l;return k.drag(function(a,b){this.attr({transform:l+(l?"T":"t")+[a,b]})},function(){l=this.transform().local})}return b.on("snap.draginit."+k.id,i),k._drag={},s.push({el:k,start:i,init:j}),k.mousedown(j),k},f.undrag=function(){for(var c=s.length;c--;)s[c].el==this&&(this.unmousedown(s[c].init),s.splice(c,1),b.unbind("snap.drag.*."+this.id),b.unbind("snap.draginit."+this.id));return!s.length&&a.unmousemove(p).unmouseup(q),this}}),d.plugin(function(a,c,d,e){var f=(c.prototype,d.prototype),g=/^\s*url\((.+)\)/,h=String,i=a._.$;a.filter={},f.filter=function(b){var d=this;"svg"!=d.type&&(d=d.paper);var e=a.parse(h(b)),f=a._.id(),g=(d.node.offsetWidth,d.node.offsetHeight,i("filter"));return i(g,{id:f,filterUnits:"userSpaceOnUse"}),g.appendChild(e.node),d.defs.appendChild(g),new c(g)},b.on("snap.util.getattr.filter",function(){b.stop();var c=i(this.node,"filter");if(c){var d=h(c).match(g);return d&&a.select(d[1])}}),b.on("snap.util.attr.filter",function(d){if(d instanceof c&&"filter"==d.type){b.stop();var e=d.node.id;e||(i(d.node,{id:d.id}),e=d.id),i(this.node,{filter:a.url(e)})}d&&"none"!=d||(b.stop(),this.node.removeAttribute("filter"))}),a.filter.blur=function(b,c){null==b&&(b=2);var d=null==c?b:[b,c];return a.format('',{def:d})},a.filter.blur.toString=function(){return this()},a.filter.shadow=function(b,c,d,e,f){return null==f&&(null==e?(f=d,d=4,e="#000"):(f=e,e=d,d=4)),null==d&&(d=4),null==f&&(f=1),null==b&&(b=0,c=2),null==c&&(c=b),e=a.color(e),a.format('',{color:e,dx:b,dy:c,blur:d,opacity:f})},a.filter.shadow.toString=function(){return this()},a.filter.grayscale=function(b){return null==b&&(b=1),a.format('',{a:.2126+.7874*(1-b),b:.7152-.7152*(1-b),c:.0722-.0722*(1-b),d:.2126-.2126*(1-b),e:.7152+.2848*(1-b),f:.0722-.0722*(1-b),g:.2126-.2126*(1-b),h:.0722+.9278*(1-b)})},a.filter.grayscale.toString=function(){return this()},a.filter.sepia=function(b){return null==b&&(b=1),a.format('',{a:.393+.607*(1-b),b:.769-.769*(1-b),c:.189-.189*(1-b),d:.349-.349*(1-b),e:.686+.314*(1-b),f:.168-.168*(1-b),g:.272-.272*(1-b),h:.534-.534*(1-b),i:.131+.869*(1-b)})},a.filter.sepia.toString=function(){return this()},a.filter.saturate=function(b){return null==b&&(b=1),a.format('',{amount:1-b})},a.filter.saturate.toString=function(){return this()},a.filter.hueRotate=function(b){return b=b||0,a.format('',{angle:b})},a.filter.hueRotate.toString=function(){return this()},a.filter.invert=function(b){return null==b&&(b=1),a.format('',{amount:b,amount2:1-b})},a.filter.invert.toString=function(){return this()},a.filter.brightness=function(b){return null==b&&(b=1),a.format('',{amount:b})},a.filter.brightness.toString=function(){return this()},a.filter.contrast=function(b){return null==b&&(b=1),a.format('',{amount:b,amount2:.5-b/2})},a.filter.contrast.toString=function(){return this()}}),d.plugin(function(a,b,c,d,e){var f=a._.box,g=a.is,h=/^[^a-z]*([tbmlrc])/i,i=function(){return"T"+this.dx+","+this.dy};b.prototype.getAlign=function(a,b){null==b&&g(a,"string")&&(b=a,a=null),a=a||this.paper;var c=a.getBBox?a.getBBox():f(a),d=this.getBBox(),e={};switch(b=b&&b.match(h),b=b?b[1].toLowerCase():"c"){case"t":e.dx=0,e.dy=c.y-d.y;break;case"b":e.dx=0,e.dy=c.y2-d.y2;break;case"m":e.dx=0,e.dy=c.cy-d.cy;break;case"l":e.dx=c.x-d.x,e.dy=0;break;case"r":e.dx=c.x2-d.x2,e.dy=0;break;default:e.dx=c.cx-d.cx,e.dy=0}return e.toString=i,e},b.prototype.align=function(a,b){return this.transform("..."+this.getAlign(a,b))}}),d.plugin(function(b,c,d,e){function f(a){a=a.split(/(?=#)/);var b=new String(a[5]);return b[50]=a[0],b[100]=a[1],b[200]=a[2],b[300]=a[3],b[400]=a[4],b[500]=a[5],b[600]=a[6],b[700]=a[7],b[800]=a[8],b[900]=a[9],a[10]&&(b.A100=a[10],b.A200=a[11],b.A400=a[12],b.A700=a[13]),b}var g="#ffebee#ffcdd2#ef9a9a#e57373#ef5350#f44336#e53935#d32f2f#c62828#b71c1c#ff8a80#ff5252#ff1744#d50000",h="#FCE4EC#F8BBD0#F48FB1#F06292#EC407A#E91E63#D81B60#C2185B#AD1457#880E4F#FF80AB#FF4081#F50057#C51162",i="#F3E5F5#E1BEE7#CE93D8#BA68C8#AB47BC#9C27B0#8E24AA#7B1FA2#6A1B9A#4A148C#EA80FC#E040FB#D500F9#AA00FF",j="#EDE7F6#D1C4E9#B39DDB#9575CD#7E57C2#673AB7#5E35B1#512DA8#4527A0#311B92#B388FF#7C4DFF#651FFF#6200EA",k="#E8EAF6#C5CAE9#9FA8DA#7986CB#5C6BC0#3F51B5#3949AB#303F9F#283593#1A237E#8C9EFF#536DFE#3D5AFE#304FFE",l="#E3F2FD#BBDEFB#90CAF9#64B5F6#64B5F6#2196F3#1E88E5#1976D2#1565C0#0D47A1#82B1FF#448AFF#2979FF#2962FF",m="#E1F5FE#B3E5FC#81D4FA#4FC3F7#29B6F6#03A9F4#039BE5#0288D1#0277BD#01579B#80D8FF#40C4FF#00B0FF#0091EA",n="#E0F7FA#B2EBF2#80DEEA#4DD0E1#26C6DA#00BCD4#00ACC1#0097A7#00838F#006064#84FFFF#18FFFF#00E5FF#00B8D4",o="#E0F2F1#B2DFDB#80CBC4#4DB6AC#26A69A#009688#00897B#00796B#00695C#004D40#A7FFEB#64FFDA#1DE9B6#00BFA5",p="#E8F5E9#C8E6C9#A5D6A7#81C784#66BB6A#4CAF50#43A047#388E3C#2E7D32#1B5E20#B9F6CA#69F0AE#00E676#00C853",q="#F1F8E9#DCEDC8#C5E1A5#AED581#9CCC65#8BC34A#7CB342#689F38#558B2F#33691E#CCFF90#B2FF59#76FF03#64DD17",r="#F9FBE7#F0F4C3#E6EE9C#DCE775#D4E157#CDDC39#C0CA33#AFB42B#9E9D24#827717#F4FF81#EEFF41#C6FF00#AEEA00",s="#FFFDE7#FFF9C4#FFF59D#FFF176#FFEE58#FFEB3B#FDD835#FBC02D#F9A825#F57F17#FFFF8D#FFFF00#FFEA00#FFD600",t="#FFF8E1#FFECB3#FFE082#FFD54F#FFCA28#FFC107#FFB300#FFA000#FF8F00#FF6F00#FFE57F#FFD740#FFC400#FFAB00",u="#FFF3E0#FFE0B2#FFCC80#FFB74D#FFA726#FF9800#FB8C00#F57C00#EF6C00#E65100#FFD180#FFAB40#FF9100#FF6D00",v="#FBE9E7#FFCCBC#FFAB91#FF8A65#FF7043#FF5722#F4511E#E64A19#D84315#BF360C#FF9E80#FF6E40#FF3D00#DD2C00",w="#EFEBE9#D7CCC8#BCAAA4#A1887F#8D6E63#795548#6D4C41#5D4037#4E342E#3E2723",x="#FAFAFA#F5F5F5#EEEEEE#E0E0E0#BDBDBD#9E9E9E#757575#616161#424242#212121",y="#ECEFF1#CFD8DC#B0BEC5#90A4AE#78909C#607D8B#546E7A#455A64#37474F#263238";b.mui={},b.flat={},b.mui.red=f(g),b.mui.pink=f(h),b.mui.purple=f(i),b.mui.deeppurple=f(j),b.mui.indigo=f(k),b.mui.blue=f(l),b.mui.lightblue=f(m),b.mui.cyan=f(n),b.mui.teal=f(o),b.mui.green=f(p),b.mui.lightgreen=f(q),b.mui.lime=f(r),b.mui.yellow=f(s),b.mui.amber=f(t),b.mui.orange=f(u),b.mui.deeporange=f(v),b.mui.brown=f(w),b.mui.grey=f(x),b.mui.bluegrey=f(y),b.flat.turquoise="#1abc9c",b.flat.greensea="#16a085",b.flat.sunflower="#f1c40f",b.flat.orange="#f39c12",b.flat.emerland="#2ecc71",b.flat.nephritis="#27ae60",b.flat.carrot="#e67e22",b.flat.pumpkin="#d35400",b.flat.peterriver="#3498db",b.flat.belizehole="#2980b9",b.flat.alizarin="#e74c3c",b.flat.pomegranate="#c0392b",b.flat.amethyst="#9b59b6",b.flat.wisteria="#8e44ad",b.flat.clouds="#ecf0f1",b.flat.silver="#bdc3c7",b.flat.wetasphalt="#34495e",b.flat.midnightblue="#2c3e50",b.flat.concrete="#95a5a6",b.flat.asbestos="#7f8c8d",b.importMUIColors=function(){for(var c in b.mui)b.mui.hasOwnProperty(c)&&(a[c]=b.mui[c])}}),d}); \ No newline at end of file diff --git a/web/src/views/Home.vue b/web/src/views/Home.vue new file mode 100644 index 00000000..0e4cfba2 --- /dev/null +++ b/web/src/views/Home.vue @@ -0,0 +1,297 @@ + + + + diff --git a/web/src/views/Login.vue b/web/src/views/Login.vue new file mode 100644 index 00000000..72ba0bc6 --- /dev/null +++ b/web/src/views/Login.vue @@ -0,0 +1,82 @@ + + + + diff --git a/web/src/views/Playground.vue b/web/src/views/Playground.vue new file mode 100644 index 00000000..167b625e --- /dev/null +++ b/web/src/views/Playground.vue @@ -0,0 +1,1882 @@ + + + + diff --git a/web/src/views/Playground1.vue b/web/src/views/Playground1.vue new file mode 100644 index 00000000..bdedc34f --- /dev/null +++ b/web/src/views/Playground1.vue @@ -0,0 +1,109 @@ + + + + diff --git a/web/src/views/PlaygroundPage.vue b/web/src/views/PlaygroundPage.vue new file mode 100644 index 00000000..8ce11e3e --- /dev/null +++ b/web/src/views/PlaygroundPage.vue @@ -0,0 +1,1802 @@ + + + + diff --git a/web/src/views/ResultPage.vue b/web/src/views/ResultPage.vue new file mode 100644 index 00000000..a9767414 --- /dev/null +++ b/web/src/views/ResultPage.vue @@ -0,0 +1,1013 @@ + + + + diff --git a/web/src/vite-env.d.ts b/web/src/vite-env.d.ts new file mode 100644 index 00000000..ee98b282 --- /dev/null +++ b/web/src/vite-env.d.ts @@ -0,0 +1,7 @@ +/// +/* eslint-disable */ +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} \ No newline at end of file diff --git a/web/vite.config.ts b/web/vite.config.ts new file mode 100644 index 00000000..f3a434ee --- /dev/null +++ b/web/vite.config.ts @@ -0,0 +1,49 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import AutoImport from 'unplugin-auto-import/vite' +import Components from 'unplugin-vue-components/vite' +import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' +import path from 'path' +import { createSvgIconsPlugin } from 'vite-plugin-svg-icons' +// import commonjs from '@rollup/plugin-commonjs' +// import nodePolyfills from 'rollup-plugin-node-polyfills' + +const pathResolve = (pathStr: string) => { + return path.resolve(__dirname, pathStr) +} + +// https://vitejs.dev/config/ +export default defineConfig({ + base: "./", + plugins: [ + vue(), + createSvgIconsPlugin({ + iconDirs: [pathResolve('./src/assets/icon')], + symbolId: 'icon-[dir]-[name]', + }), + AutoImport({ + resolvers: [ElementPlusResolver()], + }), + Components({ + resolvers: [ElementPlusResolver()], + }), + ], + define: { + 'global': 'window' // 设置 global 为 window 解决一些兼容问题 + }, + server: { + host: true, + port: 8080, // 使用的端口号 + open: true, // 是否自动打开浏览器 + watch: { + usePolling: true, // 实时监听 + interval: 1000 // 监听的间隔时间(ms) + }, + }, + resolve: { + alias: { + '@': pathResolve('./src') + } + } +}) + diff --git a/web/vite.svg b/web/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/web/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file